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:00 UTC

[01/51] [partial] nifi-registry git commit: NIFIREG-201 Refactoring project structure to better isolate extensions

Repository: nifi-registry
Updated Branches:
  refs/heads/master 9258ad55e -> 6f26290d7


http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/6f26290d/nifi-registry-core/nifi-registry-web-ui/src/main/webapp/components/administration/users/sidenav/manage-user/nf-registry-manage-user.html
----------------------------------------------------------------------
diff --git a/nifi-registry-core/nifi-registry-web-ui/src/main/webapp/components/administration/users/sidenav/manage-user/nf-registry-manage-user.html b/nifi-registry-core/nifi-registry-web-ui/src/main/webapp/components/administration/users/sidenav/manage-user/nf-registry-manage-user.html
new file mode 100644
index 0000000..e1fb346
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-web-ui/src/main/webapp/components/administration/users/sidenav/manage-user/nf-registry-manage-user.html
@@ -0,0 +1,211 @@
+<!--
+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.
+-->
+
+<div fxFill>
+    <div fxLayout="row" fxLayoutAlign="space-between center" class="pad-top-sm pad-bottom-md pad-left-md pad-right-md">
+        <span class="md-card-title ellipsis">{{nfRegistryService.user.identity}}</span>
+        <button mat-icon-button id="nf-registry-manage-user-close-side-nav" (click)="closeSideNav()">
+            <mat-icon color="primary">close</mat-icon>
+        </button>
+    </div>
+    <div class="sidenav-content">
+        <div class="pad-bottom-md pad-left-md pad-right-md" flex fxLayoutAlign="start center">
+            <mat-input-container flex>
+                <input #usernameInput
+                       matInput
+                       [disabled]="!nfRegistryService.currentUser.resourcePermissions.tenants.canWrite || (nfRegistryService.currentUser.identity === nfRegistryService.user.identity) || !nfRegistryService.user.configurable"
+                       placeholder="Identity/User Name"
+                       value="{{nfRegistryService.user.identity}}"
+                       [(ngModel)]="_username">
+            </mat-input-container>
+            <button [disabled]="nfRegistryService.user.identity === _username || (nfRegistryService.currentUser.identity === nfRegistryService.user.identity) || !nfRegistryService.user.configurable"
+                    (click)="updateUserName(usernameInput.value)"
+                    id="nf-registry-manage-user-save-side-nav"
+                    class="input-button"
+                    color="fds-regular"
+                    mat-raised-button>
+                Save
+            </button>
+        </div>
+        <div class="pad-bottom-md pad-left-md pad-right-md" flex fxLayout="column" fxLayoutAlign="space-between start">
+            <div>
+            <span class="header">Special Privileges
+                <i matTooltip="Additional permissions that allow a user to manage or access certain aspects of the registry."
+                   class="pad-left-sm fa fa-question-circle-o help-icon"></i>
+            </span>
+            </div>
+            <mat-checkbox
+                    [disabled]="!canEditSpecialPrivileges()"
+                    [checked]="nfRegistryService.user.resourcePermissions.buckets.canRead && nfRegistryService.user.resourcePermissions.buckets.canWrite && nfRegistryService.user.resourcePermissions.buckets.canDelete"
+                    (change)="toggleUserManageBucketsPrivileges($event)">
+            <span class="description">Can manage buckets<i
+                    matTooltip="Allow a user to manage all buckets in the registry, as well as provide the user access to all buckets from a connected system (e.g., NiFi)."
+                    class="pad-left-sm fa fa-question-circle-o help-icon"></i></span>
+            </mat-checkbox>
+            <div flex fxLayout="row" fxLayoutAlign="space-around center">
+                <mat-checkbox class="pad-left-md"
+                              [disabled]="!canEditSpecialPrivileges()"
+                              [(checked)]="nfRegistryService.user.resourcePermissions.buckets.canRead"
+                              (change)="toggleUserManageBucketsPrivileges($event, 'read')">
+                    <span class="description">Read</span>
+                </mat-checkbox>
+                <mat-checkbox class="pad-left-md"
+                              [disabled]="!canEditSpecialPrivileges()"
+                              [(checked)]="nfRegistryService.user.resourcePermissions.buckets.canWrite"
+                              (change)="toggleUserManageBucketsPrivileges($event, 'write')">
+                    <span class="description">Write</span>
+                </mat-checkbox>
+                <mat-checkbox class="pad-left-md"
+                              [disabled]="!canEditSpecialPrivileges()"
+                              [(checked)]="nfRegistryService.user.resourcePermissions.buckets.canDelete"
+                              (change)="toggleUserManageBucketsPrivileges($event, 'delete')">
+                    <span class="description">Delete</span>
+                </mat-checkbox>
+            </div>
+            <mat-checkbox
+                    [disabled]="!canEditSpecialPrivileges()"
+                    [checked]="nfRegistryService.user.resourcePermissions.tenants.canRead && nfRegistryService.user.resourcePermissions.tenants.canWrite && nfRegistryService.user.resourcePermissions.tenants.canDelete"
+                    (change)="toggleUserManageTenantsPrivileges($event)">
+            <span class="description">Can manage users<i
+                    matTooltip="Allow a user to manage all registry users and groups."
+                    class="pad-left-sm fa fa-question-circle-o help-icon"></i></span>
+            </mat-checkbox>
+            <div flex fxLayout="row" fxLayoutAlign="space-around center">
+                <mat-checkbox class="pad-left-md"
+                              [disabled]="!canEditSpecialPrivileges()"
+                              [(checked)]="nfRegistryService.user.resourcePermissions.tenants.canRead"
+                              (change)="toggleUserManageTenantsPrivileges($event, 'read')">
+                    <span class="description">Read</span>
+                </mat-checkbox>
+                <mat-checkbox class="pad-left-md"
+                              [disabled]="!canEditSpecialPrivileges()"
+                              [(checked)]="nfRegistryService.user.resourcePermissions.tenants.canWrite"
+                              (change)="toggleUserManageTenantsPrivileges($event, 'write')">
+                    <span class="description">Write</span>
+                </mat-checkbox>
+                <mat-checkbox class="pad-left-md"
+                              [disabled]="!canEditSpecialPrivileges()"
+                              [(checked)]="nfRegistryService.user.resourcePermissions.tenants.canDelete"
+                              (change)="toggleUserManageTenantsPrivileges($event, 'delete')">
+                    <span class="description">Delete</span>
+                </mat-checkbox>
+            </div>
+            <mat-checkbox
+                    [disabled]="!canEditSpecialPrivileges()"
+                    [checked]="nfRegistryService.user.resourcePermissions.policies.canRead && nfRegistryService.user.resourcePermissions.policies.canWrite && nfRegistryService.user.resourcePermissions.policies.canDelete"
+                    (change)="toggleUserManagePoliciesPrivileges($event)">
+            <span class="description">Can manage policies<i
+                    matTooltip="Allow a user to grant all registry users read, write, and delete permission to a bucket."
+                    class="pad-left-sm fa fa-question-circle-o help-icon"></i></span>
+            </mat-checkbox>
+            <div flex fxLayout="row" fxLayoutAlign="space-around center">
+                <mat-checkbox class="pad-left-md"
+                              [disabled]="!canEditSpecialPrivileges()"
+                              [(checked)]="nfRegistryService.user.resourcePermissions.policies.canRead"
+                              (change)="toggleUserManagePoliciesPrivileges($event, 'read')">
+                    <span class="description">Read</span>
+                </mat-checkbox>
+                <mat-checkbox class="pad-left-md"
+                              [disabled]="!canEditSpecialPrivileges()"
+                              [(checked)]="nfRegistryService.user.resourcePermissions.policies.canWrite"
+                              (change)="toggleUserManagePoliciesPrivileges($event, 'write')">
+                    <span class="description">Write</span>
+                </mat-checkbox>
+                <mat-checkbox class="pad-left-md"
+                              [disabled]="!canEditSpecialPrivileges()"
+                              [(checked)]="nfRegistryService.user.resourcePermissions.policies.canDelete"
+                              (change)="toggleUserManagePoliciesPrivileges($event, 'delete')">
+                    <span class="description">Delete</span>
+                </mat-checkbox>
+            </div>
+            <mat-checkbox
+                    [disabled]="!canEditSpecialPrivileges()"
+                    [checked]="nfRegistryService.user.resourcePermissions.proxy.canWrite"
+                    (change)="toggleUserManageProxyPrivileges($event)">
+            <span class="description">Can proxy user requests<i
+                    matTooltip="Allow a connected system (e.g., NiFi) to process requests of authorized users of that system."
+                    class="pad-left-sm fa fa-question-circle-o help-icon"></i></span>
+            </mat-checkbox>
+        </div>
+        <mat-button-toggle-group name="nifi-registry-manage-user-perspective" class="pad-left-md tab-toggle-group">
+            <mat-button-toggle [checked]="manageUserPerspective === 'membership'"
+                               value="membership"
+                               class="uppercase"
+                               (change)="manageUserPerspective = 'membership'"
+                               i18n="Group membership tab, user management sidenav|View the groups to which this user belongs.@@nf-admin-user-management-sidenav-membership-tab-title">
+                Membership
+            </mat-button-toggle>
+        </mat-button-toggle-group>
+        <div *ngIf="manageUserPerspective === 'membership'">
+            <div *ngIf="nfRegistryService.user.userGroups" class="pad-top-md pad-bottom-sm pad-left-md pad-right-md">
+                <div flex fxLayout="row" fxLayoutAlign="space-between center">
+                    <span class="md-card-title">Membership ({{nfRegistryService.user.userGroups.length}})</span>
+                    <button color="fds-secondary"
+                            [disabled]="!nfRegistryService.currentUser.resourcePermissions.tenants.canWrite || canAddNonConfigurableUserToGroup()"
+                            mat-raised-button
+                            (click)="addUserToGroups()">
+                        Add To Group
+                    </button>
+                </div>
+                <div id="nifi-registry-user-membership-list-container-column-header" fxLayout="row"
+                     fxLayoutAlign="space-between center" class="td-data-table">
+                    <div class="td-data-table-column" (click)="sortGroups(column)"
+                         *ngFor="let column of userGroupsColumns"
+                         fxFlex="{{column.width}}">
+                        {{column.label}}
+                        <i *ngIf="column.active && column.sortable && column.sortOrder === 'ASC'" class="fa fa-caret-up"
+                           aria-hidden="true"></i>
+                        <i *ngIf="column.active && column.sortable && column.sortOrder === 'DESC'"
+                           class="fa fa-caret-down"
+                           aria-hidden="true"></i>
+                    </div>
+                </div>
+                <div id="nifi-registry-user-membership-list-container">
+                    <div fxLayout="row" fxLayoutAlign="space-between center" class="td-data-table-row"
+                         [ngClass]="{'selected' : row.checked}" *ngFor="let row of filteredUserGroups"
+                         (click)="row.checked = !row.checked">
+                        <div class="td-data-table-cell" *ngFor="let column of userGroupsColumns"
+                             fxFlex="{{column.width}}">
+                            <div class="ellipsis"
+                                 matTooltip="{{column.format ? column.format(row[column.name]) : row[column.name]}}">
+                                <i class="fa fa-users push-right-sm" aria-hidden="true"></i>{{column.format ?
+                                column.format(row[column.name]) : row[column.name]}}
+                            </div>
+                        </div>
+                        <div class="td-data-table-cell">
+                            <div>
+                                <button (click)="removeUserFromGroup(row);row.checked = !row.checked;"
+                                        [disabled]="!nfRegistryService.currentUser.resourcePermissions.tenants.canWrite || row.configurable === false"
+                                        matTooltip="'Remove user from group'" mat-icon-button color="accent">
+                                    <i class="fa fa-trash" aria-hidden="true"></i>
+                                </button>
+                            </div>
+                        </div>
+                    </div>
+                </div>
+                <div class="mat-padding" *ngIf="nfRegistryService.user.userGroups.length === 0" layout="row"
+                     layout-align="center center">
+                    <h3>This user does not belong to any groups yet.</h3>
+                </div>
+            </div>
+        </div>
+    </div>
+    <button id="nf-registry-user-permissions-side-nav-container" class="push-right-md" mat-raised-button
+            color="fds-primary"
+            (click)="closeSideNav()">Close
+    </button>
+</div>


[17/51] [partial] nifi-registry git commit: NIFIREG-201 Refactoring project structure to better isolate extensions

Posted by kd...@apache.org.
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();
+    }
+}


[09/51] [partial] nifi-registry git commit: NIFIREG-201 Refactoring project structure to better isolate extensions

Posted by kd...@apache.org.
http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/6f26290d/nifi-registry-core/nifi-registry-web-api/src/test/resources/db/clearDB.sql
----------------------------------------------------------------------
diff --git a/nifi-registry-core/nifi-registry-web-api/src/test/resources/db/clearDB.sql b/nifi-registry-core/nifi-registry-web-api/src/test/resources/db/clearDB.sql
new file mode 100644
index 0000000..7661df5
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-web-api/src/test/resources/db/clearDB.sql
@@ -0,0 +1,19 @@
+-- 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.
+
+DELETE FROM FLOW_SNAPSHOT;
+DELETE FROM FLOW;
+DELETE FROM BUCKET_ITEM;
+DELETE FROM BUCKET;

http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/6f26290d/nifi-registry-core/nifi-registry-web-api/src/test/resources/keys/README.md
----------------------------------------------------------------------
diff --git a/nifi-registry-core/nifi-registry-web-api/src/test/resources/keys/README.md b/nifi-registry-core/nifi-registry-web-api/src/test/resources/keys/README.md
new file mode 100644
index 0000000..c3059cf
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-web-api/src/test/resources/keys/README.md
@@ -0,0 +1,47 @@
+<!--
+  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.
+-->
+# Integration Test Keys
+
+The integration tests that run a secure NiFi require keystores and truststores for the server and client in order
+to establish a two-way TLS connection.
+
+The keys/certs for these tests were generated with the tls-toolkit included with NiFi Toolkit v1.4.0.
+
+The steps for generating replacements are:
+
+    # use NiFi tls-toolkit to generate CA, server key/cert, client key/cert
+    ./nifi-toolkit-1.4.0/bin/tls-toolkit.sh standalone --certificateAuthorityHostname localhost --hostnames localhost --nifiDnSuffix ", OU=nifi" --keyStorePassword localhostKeystorePassword --trustStorePassword localhostTruststorePassword --clientCertDn "CN=user1, OU=nifi" --clientCertPassword u1Pass --days 3650 --outputDirectory nifireg-integrationtest
+
+    # change to tls-toolkit output directory
+    cd ./nifireg-integrationtest
+
+    # copy server's key/trust stores
+    mkdir keys
+    cp localhost/keystore.jks keys/localhost-ks.jks
+    cp localhost/truststore.jks keys/localhost-ts.jks
+
+    # create a Java Key Store (JKS) from the client key
+    keytool -importkeystore -destkeystore keys/client-ks.jks -deststorepass clientKeystorePassword -destkeypass u1Pass -srckeystore CN=user1_OU=nifi.p12 -srcstorepass u1Pass -srcstoretype PKCS12
+
+
+You should now have a directory with the following contents:
+
+    keys/
+     +-- client-ks.jks      # client keystore: keystorePass=clientKeystorePassword, keyPass=u1Pass
+     +-- localhost-ks.jks   # server keystore: keystorePass=localhostKeystorePassword, keyPass=localhostKeystorePassword
+     +-- localhost-ts.jks   # server/client truststore (contains CA): truststorePass=localhostTruststorePassword
+
+Copy these files to the test/resources/keys/ directory.
+

http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/6f26290d/nifi-registry-core/nifi-registry-web-api/src/test/resources/keys/client-ks.jks
----------------------------------------------------------------------
diff --git a/nifi-registry-core/nifi-registry-web-api/src/test/resources/keys/client-ks.jks b/nifi-registry-core/nifi-registry-web-api/src/test/resources/keys/client-ks.jks
new file mode 100644
index 0000000..f2e0a1a
Binary files /dev/null and b/nifi-registry-core/nifi-registry-web-api/src/test/resources/keys/client-ks.jks differ

http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/6f26290d/nifi-registry-core/nifi-registry-web-api/src/test/resources/keys/localhost-ks.jks
----------------------------------------------------------------------
diff --git a/nifi-registry-core/nifi-registry-web-api/src/test/resources/keys/localhost-ks.jks b/nifi-registry-core/nifi-registry-web-api/src/test/resources/keys/localhost-ks.jks
new file mode 100644
index 0000000..7421aaa
Binary files /dev/null and b/nifi-registry-core/nifi-registry-web-api/src/test/resources/keys/localhost-ks.jks differ

http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/6f26290d/nifi-registry-core/nifi-registry-web-api/src/test/resources/keys/localhost-ts.jks
----------------------------------------------------------------------
diff --git a/nifi-registry-core/nifi-registry-web-api/src/test/resources/keys/localhost-ts.jks b/nifi-registry-core/nifi-registry-web-api/src/test/resources/keys/localhost-ts.jks
new file mode 100644
index 0000000..21eb2c0
Binary files /dev/null and b/nifi-registry-core/nifi-registry-web-api/src/test/resources/keys/localhost-ts.jks differ

http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/6f26290d/nifi-registry-core/nifi-registry-web-docs/pom.xml
----------------------------------------------------------------------
diff --git a/nifi-registry-core/nifi-registry-web-docs/pom.xml b/nifi-registry-core/nifi-registry-web-docs/pom.xml
new file mode 100644
index 0000000..da2b600
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-web-docs/pom.xml
@@ -0,0 +1,68 @@
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
+    <!--
+      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.
+    -->
+    <modelVersion>4.0.0</modelVersion>
+    <parent>
+        <groupId>org.apache.nifi.registry</groupId>
+        <artifactId>nifi-registry-core</artifactId>
+        <version>0.3.0-SNAPSHOT</version>
+    </parent>
+    <artifactId>nifi-registry-web-docs</artifactId>
+    <packaging>war</packaging>
+
+    <properties>
+        <maven.javadoc.skip>true</maven.javadoc.skip>
+        <source.skip>true</source.skip>
+    </properties>
+
+    <build>
+        <plugins>
+            <plugin>
+                <groupId>org.apache.rat</groupId>
+                <artifactId>apache-rat-plugin</artifactId>
+                <configuration>
+                    <excludes combine.children="append">
+                        <exclude>src/main/webapp/js/jquery.min.js</exclude>
+                    </excludes>
+                </configuration>
+            </plugin>
+        </plugins>
+    </build>
+
+    <dependencies>
+        <dependency>
+            <groupId>org.apache.nifi.registry</groupId>
+            <artifactId>nifi-registry-utils</artifactId>
+            <version>0.3.0-SNAPSHOT</version>
+            <scope>provided</scope>
+        </dependency>
+        <dependency>
+            <groupId>org.apache.commons</groupId>
+            <artifactId>commons-lang3</artifactId>
+            <scope>provided</scope>
+        </dependency>
+        <dependency>
+            <groupId>javax.servlet</groupId>
+            <artifactId>javax.servlet-api</artifactId>
+        </dependency>
+        <!-- Needed this dependency to resolve the taglib inside documentation.jsp, otherwise an error is encountered
+                "The absolute uri: http://java.sun.com/jsp/jstl/core cannot be resolved" -->
+        <dependency>
+            <groupId>javax.servlet.jsp.jstl</groupId>
+            <artifactId>jstl</artifactId>
+            <version>1.2</version>
+        </dependency>
+    </dependencies>
+</project>

http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/6f26290d/nifi-registry-core/nifi-registry-web-docs/src/main/java/org/apache/nifi/registry/web/docs/DocumentationController.java
----------------------------------------------------------------------
diff --git a/nifi-registry-core/nifi-registry-web-docs/src/main/java/org/apache/nifi/registry/web/docs/DocumentationController.java b/nifi-registry-core/nifi-registry-web-docs/src/main/java/org/apache/nifi/registry/web/docs/DocumentationController.java
new file mode 100644
index 0000000..3283c73
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-web-docs/src/main/java/org/apache/nifi/registry/web/docs/DocumentationController.java
@@ -0,0 +1,57 @@
+/*
+ * 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.docs;
+
+import javax.servlet.ServletConfig;
+import javax.servlet.ServletContext;
+import javax.servlet.ServletException;
+import javax.servlet.http.HttpServlet;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+import java.io.IOException;
+
+/**
+ *
+ */
+public class DocumentationController extends HttpServlet {
+
+    private static final int GENERAL_LINK_COUNT = 4;
+    private static final int DEVELOPER_LINK_COUNT = 2;
+
+    // context for accessing the extension mapping
+    private ServletContext servletContext;
+
+    @Override
+    public void init(final ServletConfig config) throws ServletException {
+        super.init(config);
+        servletContext = config.getServletContext();
+    }
+
+    /**
+     *
+     * @param request servlet request
+     * @param response servlet response
+     * @throws ServletException if a servlet-specific error occurs
+     * @throws IOException if an I/O error occurs
+     */
+    @Override
+    protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
+        // forward appropriately
+        request.getRequestDispatcher("/WEB-INF/jsp/documentation.jsp").forward(request, response);
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/6f26290d/nifi-registry-core/nifi-registry-web-docs/src/main/resources/META-INF/LICENSE
----------------------------------------------------------------------
diff --git a/nifi-registry-core/nifi-registry-web-docs/src/main/resources/META-INF/LICENSE b/nifi-registry-core/nifi-registry-web-docs/src/main/resources/META-INF/LICENSE
new file mode 100644
index 0000000..10865c2
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-web-docs/src/main/resources/META-INF/LICENSE
@@ -0,0 +1,223 @@
+
+                                 Apache License
+                           Version 2.0, January 2004
+                        http://www.apache.org/licenses/
+
+   TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+   1. Definitions.
+
+      "License" shall mean the terms and conditions for use, reproduction,
+      and distribution as defined by Sections 1 through 9 of this document.
+
+      "Licensor" shall mean the copyright owner or entity authorized by
+      the copyright owner that is granting the License.
+
+      "Legal Entity" shall mean the union of the acting entity and all
+      other entities that control, are controlled by, or are under common
+      control with that entity. For the purposes of this definition,
+      "control" means (i) the power, direct or indirect, to cause the
+      direction or management of such entity, whether by contract or
+      otherwise, or (ii) ownership of fifty percent (50%) or more of the
+      outstanding shares, or (iii) beneficial ownership of such entity.
+
+      "You" (or "Your") shall mean an individual or Legal Entity
+      exercising permissions granted by this License.
+
+      "Source" form shall mean the preferred form for making modifications,
+      including but not limited to software source code, documentation
+      source, and configuration files.
+
+      "Object" form shall mean any form resulting from mechanical
+      transformation or translation of a Source form, including but
+      not limited to compiled object code, generated documentation,
+      and conversions to other media types.
+
+      "Work" shall mean the work of authorship, whether in Source or
+      Object form, made available under the License, as indicated by a
+      copyright notice that is included in or attached to the work
+      (an example is provided in the Appendix below).
+
+      "Derivative Works" shall mean any work, whether in Source or Object
+      form, that is based on (or derived from) the Work and for which the
+      editorial revisions, annotations, elaborations, or other modifications
+      represent, as a whole, an original work of authorship. For the purposes
+      of this License, Derivative Works shall not include works that remain
+      separable from, or merely link (or bind by name) to the interfaces of,
+      the Work and Derivative Works thereof.
+
+      "Contribution" shall mean any work of authorship, including
+      the original version of the Work and any modifications or additions
+      to that Work or Derivative Works thereof, that is intentionally
+      submitted to Licensor for inclusion in the Work by the copyright owner
+      or by an individual or Legal Entity authorized to submit on behalf of
+      the copyright owner. For the purposes of this definition, "submitted"
+      means any form of electronic, verbal, or written communication sent
+      to the Licensor or its representatives, including but not limited to
+      communication on electronic mailing lists, source code control systems,
+      and issue tracking systems that are managed by, or on behalf of, the
+      Licensor for the purpose of discussing and improving the Work, but
+      excluding communication that is conspicuously marked or otherwise
+      designated in writing by the copyright owner as "Not a Contribution."
+
+      "Contributor" shall mean Licensor and any individual or Legal Entity
+      on behalf of whom a Contribution has been received by Licensor and
+      subsequently incorporated within the Work.
+
+   2. Grant of Copyright License. Subject to the terms and conditions of
+      this License, each Contributor hereby grants to You a perpetual,
+      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+      copyright license to reproduce, prepare Derivative Works of,
+      publicly display, publicly perform, sublicense, and distribute the
+      Work and such Derivative Works in Source or Object form.
+
+   3. Grant of Patent License. Subject to the terms and conditions of
+      this License, each Contributor hereby grants to You a perpetual,
+      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+      (except as stated in this section) patent license to make, have made,
+      use, offer to sell, sell, import, and otherwise transfer the Work,
+      where such license applies only to those patent claims licensable
+      by such Contributor that are necessarily infringed by their
+      Contribution(s) alone or by combination of their Contribution(s)
+      with the Work to which such Contribution(s) was submitted. If You
+      institute patent litigation against any entity (including a
+      cross-claim or counterclaim in a lawsuit) alleging that the Work
+      or a Contribution incorporated within the Work constitutes direct
+      or contributory patent infringement, then any patent licenses
+      granted to You under this License for that Work shall terminate
+      as of the date such litigation is filed.
+
+   4. Redistribution. You may reproduce and distribute copies of the
+      Work or Derivative Works thereof in any medium, with or without
+      modifications, and in Source or Object form, provided that You
+      meet the following conditions:
+
+      (a) You must give any other recipients of the Work or
+          Derivative Works a copy of this License; and
+
+      (b) You must cause any modified files to carry prominent notices
+          stating that You changed the files; and
+
+      (c) You must retain, in the Source form of any Derivative Works
+          that You distribute, all copyright, patent, trademark, and
+          attribution notices from the Source form of the Work,
+          excluding those notices that do not pertain to any part of
+          the Derivative Works; and
+
+      (d) If the Work includes a "NOTICE" text file as part of its
+          distribution, then any Derivative Works that You distribute must
+          include a readable copy of the attribution notices contained
+          within such NOTICE file, excluding those notices that do not
+          pertain to any part of the Derivative Works, in at least one
+          of the following places: within a NOTICE text file distributed
+          as part of the Derivative Works; within the Source form or
+          documentation, if provided along with the Derivative Works; or,
+          within a display generated by the Derivative Works, if and
+          wherever such third-party notices normally appear. The contents
+          of the NOTICE file are for informational purposes only and
+          do not modify the License. You may add Your own attribution
+          notices within Derivative Works that You distribute, alongside
+          or as an addendum to the NOTICE text from the Work, provided
+          that such additional attribution notices cannot be construed
+          as modifying the License.
+
+      You may add Your own copyright statement to Your modifications and
+      may provide additional or different license terms and conditions
+      for use, reproduction, or distribution of Your modifications, or
+      for any such Derivative Works as a whole, provided Your use,
+      reproduction, and distribution of the Work otherwise complies with
+      the conditions stated in this License.
+
+   5. Submission of Contributions. Unless You explicitly state otherwise,
+      any Contribution intentionally submitted for inclusion in the Work
+      by You to the Licensor shall be under the terms and conditions of
+      this License, without any additional terms or conditions.
+      Notwithstanding the above, nothing herein shall supersede or modify
+      the terms of any separate license agreement you may have executed
+      with Licensor regarding such Contributions.
+
+   6. Trademarks. This License does not grant permission to use the trade
+      names, trademarks, service marks, or product names of the Licensor,
+      except as required for reasonable and customary use in describing the
+      origin of the Work and reproducing the content of the NOTICE file.
+
+   7. Disclaimer of Warranty. Unless required by applicable law or
+      agreed to in writing, Licensor provides the Work (and each
+      Contributor provides its Contributions) on an "AS IS" BASIS,
+      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+      implied, including, without limitation, any warranties or conditions
+      of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+      PARTICULAR PURPOSE. You are solely responsible for determining the
+      appropriateness of using or redistributing the Work and assume any
+      risks associated with Your exercise of permissions under this License.
+
+   8. Limitation of Liability. In no event and under no legal theory,
+      whether in tort (including negligence), contract, or otherwise,
+      unless required by applicable law (such as deliberate and grossly
+      negligent acts) or agreed to in writing, shall any Contributor be
+      liable to You for damages, including any direct, indirect, special,
+      incidental, or consequential damages of any character arising as a
+      result of this License or out of the use or inability to use the
+      Work (including but not limited to damages for loss of goodwill,
+      work stoppage, computer failure or malfunction, or any and all
+      other commercial damages or losses), even if such Contributor
+      has been advised of the possibility of such damages.
+
+   9. Accepting Warranty or Additional Liability. While redistributing
+      the Work or Derivative Works thereof, You may choose to offer,
+      and charge a fee for, acceptance of support, warranty, indemnity,
+      or other liability obligations and/or rights consistent with this
+      License. However, in accepting such obligations, You may act only
+      on Your own behalf and on Your sole responsibility, not on behalf
+      of any other Contributor, and only if You agree to indemnify,
+      defend, and hold each Contributor harmless for any liability
+      incurred by, or claims asserted against, such Contributor by reason
+      of your accepting any such warranty or additional liability.
+
+   END OF TERMS AND CONDITIONS
+
+   APPENDIX: How to apply the Apache License to your work.
+
+      To apply the Apache License to your work, attach the following
+      boilerplate notice, with the fields enclosed by brackets "[]"
+      replaced with your own identifying information. (Don't include
+      the brackets!)  The text should be enclosed in the appropriate
+      comment syntax for the file format. We also recommend that a
+      file or class name and description of purpose be included on the
+      same "printed page" as the copyright notice for easier
+      identification within third-party archives.
+
+   Copyright [yyyy] [name of copyright owner]
+
+   Licensed under the Apache License, Version 2.0 (the "License");
+   you may not use this file except in compliance with the License.
+   You may obtain a copy of the License at
+
+       http://www.apache.org/licenses/LICENSE-2.0
+
+   Unless required by applicable law or agreed to in writing, software
+   distributed under the License is distributed on an "AS IS" BASIS,
+   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+   See the License for the specific language governing permissions and
+   limitations under the License.
+
+This product bundles 'JQuery' which is available under and MIT style license.
+    (c) 2005, 2014 jQuery Foundation, Inc.
+
+    Permission is hereby granted, free of charge, to any person obtaining a copy
+    of this software and associated documentation files (the "Software"), to deal
+    in the Software without restriction, including without limitation the rights
+    to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+    copies of the Software, and to permit persons to whom the Software is
+    furnished to do so, subject to the following conditions:
+
+    The above copyright notice and this permission notice shall be included in
+    all copies or substantial portions of the Software.
+
+    THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+    IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+    FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+    AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+    LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+    OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+    THE SOFTWARE.

http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/6f26290d/nifi-registry-core/nifi-registry-web-docs/src/main/resources/META-INF/NOTICE
----------------------------------------------------------------------
diff --git a/nifi-registry-core/nifi-registry-web-docs/src/main/resources/META-INF/NOTICE b/nifi-registry-core/nifi-registry-web-docs/src/main/resources/META-INF/NOTICE
new file mode 100644
index 0000000..d0f21e6
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-web-docs/src/main/resources/META-INF/NOTICE
@@ -0,0 +1,14 @@
+nifi-web-docs
+Copyright 2014-2017 The Apache Software Foundation
+
+This product includes software developed at
+The Apache Software Foundation (http://www.apache.org/).
+
+************************
+Common Development and Distribution License 1.1
+************************
+
+The following binary components are provided under the Common Development and Distribution License 1.1. See project link for details.
+
+    (CDDL 1.1) (GPL2 w/ CPE) Java Servlet API  (javax.servlet:javax.servlet-api:jar:3.1.0 - http://servlet-spec.java.net)
+    (CDDL 1.1) (GPL2 w/ CPE) JavaServer Pages Standard Tag Library  (javax.servlet.jsp.jstl:jstl:jar:1.2 - https://javaee.github.io/jstl-api/)
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/6f26290d/nifi-registry-core/nifi-registry-web-docs/src/main/webapp/WEB-INF/jsp/documentation.jsp
----------------------------------------------------------------------
diff --git a/nifi-registry-core/nifi-registry-web-docs/src/main/webapp/WEB-INF/jsp/documentation.jsp b/nifi-registry-core/nifi-registry-web-docs/src/main/webapp/WEB-INF/jsp/documentation.jsp
new file mode 100644
index 0000000..fa4d574
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-web-docs/src/main/webapp/WEB-INF/jsp/documentation.jsp
@@ -0,0 +1,84 @@
+<%--
+ 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.
+--%>
+<%@ page contentType="text/html" pageEncoding="UTF-8" session="false" %>
+<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
+<!DOCTYPE html>
+<html xmlns="http://www.w3.org/1999/xhtml">
+    <head>
+        <meta name="viewport" content="width=device-width, initial-scale=1.0">
+        <meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
+        <link rel="shortcut icon" href="../nifi/images/nifi16.ico"/>
+        <title>NiFi Registry Documentation</title>
+        <script type="text/javascript" src="js/jquery.min.js"></script>
+        <script type="text/javascript" src="js/application.js"></script>
+        <link href="css/main.css" rel="stylesheet" type="text/css" />
+        <link href="css/component-usage.css" rel="stylesheet" type="text/css" />
+    </head>
+    <body id="documentation-body">
+        <div id="banner-header" class="main-banner-header"></div>
+        <span id="initial-selection-type" style="display: none;">
+            <%= request.getParameter("select") == null ? "" : org.apache.nifi.registry.util.EscapeUtils.escapeHtml(request.getParameter("select")) %>
+        </span>
+        <span id="initial-selection-bundle-group" style="display: none;">
+            <%= request.getParameter("group") == null ? "" : org.apache.nifi.registry.util.EscapeUtils.escapeHtml(request.getParameter("group")) %>
+        </span>
+        <span id="initial-selection-bundle-artifact" style="display: none;">
+            <%= request.getParameter("artifact") == null ? "" : org.apache.nifi.registry.util.EscapeUtils.escapeHtml(request.getParameter("artifact")) %>
+        </span>
+        <span id="initial-selection-bundle-version" style="display: none;">
+            <%= request.getParameter("version") == null ? "" : org.apache.nifi.registry.util.EscapeUtils.escapeHtml(request.getParameter("version")) %>
+        </span>
+        <div id="documentation-header" class="documentation-header">
+            <div id="component-list-toggle-link">-</div>
+            <div id="header-contents">
+                <div id="nf-title">NiFi Registry Documentation</div>
+                <div id="nf-version" class="version"></div>
+                <div id="selected-component"></div>
+            </div>
+        </div>
+        <div id="component-root-container">
+            <div id="component-listing-container">
+                <div id="component-listing" class="component-listing">
+                    <div class="section">
+                        <div class="header">General</div>
+                        <div id="general-links" class="component-links">
+                            <ul>
+                            	<li class="component-item"><a class="document-link admin-guide" href="html/getting-started.html" target="component-usage">Getting Started</a></li>
+                                <li class="component-item"><a class="document-link admin-guide" href="html/user-guide.html" target="component-usage">User Guide</a></li>
+                                <li class="component-item"><a class="document-link admin-guide" href="html/administration-guide.html" target="component-usage">Admin Guide</a></li>
+                            </ul>
+                            <span class="no-matching no-components hidden">No matching guides</span>
+                        </div>
+                    </div>
+                    <div class="section">
+                        <div class="header">Developer</div>
+                        <div id="developer-links" class="component-links">
+                            <ul>
+                                <li class="component-item"><a class="document-link rest-api" href="rest-api/index.html" target="component-usage">Rest Api</a></li>
+                            </ul>
+                            <span class="no-matching no-components hidden">No matching developer guides</span>
+                        </div>
+                    </div>
+                </div>
+            </div>
+            <div id="component-usage-container">
+                <iframe id="component-usage" name="component-usage" frameborder="0" class="component-usage"></iframe>
+            </div>
+        </div>
+        <div id="banner-footer" class="main-banner-footer"></div>
+    </body>
+</html>

http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/6f26290d/nifi-registry-core/nifi-registry-web-docs/src/main/webapp/WEB-INF/jsp/no-documentation-found.jsp
----------------------------------------------------------------------
diff --git a/nifi-registry-core/nifi-registry-web-docs/src/main/webapp/WEB-INF/jsp/no-documentation-found.jsp b/nifi-registry-core/nifi-registry-web-docs/src/main/webapp/WEB-INF/jsp/no-documentation-found.jsp
new file mode 100644
index 0000000..567d0be
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-web-docs/src/main/webapp/WEB-INF/jsp/no-documentation-found.jsp
@@ -0,0 +1,31 @@
+<%--
+ 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.
+--%>
+<%@ page contentType="text/html" pageEncoding="UTF-8" session="false" %>
+<!DOCTYPE html>
+<html>
+    <head>
+        <title>NiFi</title>
+        <meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
+        <meta http-equiv="X-UA-Compatible" content="IE=EmulateIE7"/>
+        <link rel="shortcut icon" href="../nifi/images/nifi16.ico"/>
+        <link href="../../css/component-usage.css" rel="stylesheet" type="text/css" />
+    </head>
+    <body>
+        <h2>Yikes!</h2>
+        <p>Unable to locate the documentation for the selected item.</p>
+    </body>
+</html>
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/6f26290d/nifi-registry-core/nifi-registry-web-docs/src/main/webapp/WEB-INF/web.xml
----------------------------------------------------------------------
diff --git a/nifi-registry-core/nifi-registry-web-docs/src/main/webapp/WEB-INF/web.xml b/nifi-registry-core/nifi-registry-web-docs/src/main/webapp/WEB-INF/web.xml
new file mode 100644
index 0000000..eb6bb67
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-web-docs/src/main/webapp/WEB-INF/web.xml
@@ -0,0 +1,33 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+  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.
+-->
+<web-app version="3.0" xmlns="http://java.sun.com/xml/ns/javaee" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_3_0.xsd">
+    <display-name>nifi-registry-docs</display-name>
+    <error-page>
+        <error-code>404</error-code>
+        <location>/WEB-INF/jsp/no-documentation-found.jsp</location>
+    </error-page>
+    <servlet>
+        <servlet-name>documentation-controller</servlet-name>
+        <servlet-class>org.apache.nifi.registry.web.docs.DocumentationController</servlet-class>
+    </servlet>
+    <servlet-mapping>
+        <servlet-name>documentation-controller</servlet-name>
+        <url-pattern>/documentation</url-pattern>
+    </servlet-mapping>
+    <welcome-file-list>
+        <welcome-file>documentation</welcome-file>
+    </welcome-file-list>
+</web-app>

http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/6f26290d/nifi-registry-core/nifi-registry-web-docs/src/main/webapp/css/component-usage.css
----------------------------------------------------------------------
diff --git a/nifi-registry-core/nifi-registry-web-docs/src/main/webapp/css/component-usage.css b/nifi-registry-core/nifi-registry-web-docs/src/main/webapp/css/component-usage.css
new file mode 100644
index 0000000..1ae578b
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-web-docs/src/main/webapp/css/component-usage.css
@@ -0,0 +1,183 @@
+/*
+ * 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.
+ */
+
+@import "https://fonts.googleapis.com/css?family=Open+Sans:300,300italic,400,400italic,600,600italic|Noto+Serif:400,400italic,700,700italic|Droid+Sans+Mono:400";
+
+html, html a {
+    -webkit-font-smoothing: antialiased;
+    text-shadow: 1px 1px 1px rgba(0,0,0,0.004);
+}
+
+body {
+    margin: 0 auto;
+    display: block;
+    font-family: "Open Sans","DejaVu Sans",sans-serif;
+}
+
+.title {
+    font-weight: bold;
+    color: #7a2518;
+    font-size: 18px;
+}
+
+.hidden {
+    display: none;
+}
+
+/* tables */
+
+table {
+	color:#666;
+	font-size:14px;
+	background:#eaebec;	
+	border:#ccc 1px solid;
+	-webkit-border-radius:3px;
+	border-radius:3px;
+	width: 100%;
+	word-wrap: break-word;
+}
+
+table th {
+	padding:11px 15px 12px 15px;
+	border-top:1px solid #fafafa;
+	border-bottom:1px solid #e0e0e0;
+
+	background: #ededed;
+}
+
+table th:first-child {
+	text-align: left;
+	padding-left:10px;
+}
+
+table th:last-child {
+	text-align: left;
+	padding-left:10px;
+}
+
+table tr:first-child th:first-child {
+	border-top-left-radius:3px;
+}
+
+table tr:first-child th:last-child {
+	border-top-right-radius:3px;
+}
+
+table tr {
+	text-align: center;
+	padding-left:10px;
+}
+
+table td:first-child {
+	text-align: left;
+	padding-left:10px;
+	border-left: 0;
+}
+
+table td:last-child {
+	text-align: left;
+	padding-left:10px;
+	border-left: 0;
+	vertical-align: top;
+	
+}
+
+table td {
+	padding:12px;
+	background: #fafafa;
+}
+
+table tr:last-child td {
+	border-bottom:0;
+}
+
+table tr:last-child td:first-child {
+	border-bottom-left-radius:3px;
+}
+
+table tr:last-child td:last-child {
+	border-bottom-right-radius:3px;
+}
+
+td#allowable-values, td#default-value, td#name, td#value {
+	max-width: 200px;
+}
+
+td#description {
+	vertical-align: middle;
+}
+
+/* links */
+
+a, a:link, a:visited {
+    cursor: pointer;
+    color: #2156a5;
+    text-decoration: none;
+    border: none;
+}
+
+a:hover, a:active {
+    color: #2156a5;
+    text-decoration: none;
+    border: none;
+}
+
+.clear {
+    clear: both;
+}
+
+/* p */
+
+p {
+    font-family: 'Noto Serif', 'DejaVu Serif', serif;
+    font-size: 16px;
+}
+
+p strong {
+    font-weight: bold;
+}
+
+/* ul li */
+td ul {
+	margin: 0px 0px 0px 0px;
+	padding-left: 20px;
+}
+ul li {
+	text-align: left;
+	display: list-item;    
+}
+
+ul li strong {
+    font-weight: bold;
+}
+
+h2 {
+    font-weight: normal;
+    color: #ba3925;
+}
+
+/* pre */
+
+pre {
+    font-size: 14px;
+    background-color: #fefefe;
+    border: 1px solid #ccc;
+    border-left: 6px solid #ccc;
+    color: #555;
+    margin-bottom: 10px;
+    padding: 5px 8px;
+}
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/6f26290d/nifi-registry-core/nifi-registry-web-docs/src/main/webapp/css/main.css
----------------------------------------------------------------------
diff --git a/nifi-registry-core/nifi-registry-web-docs/src/main/webapp/css/main.css b/nifi-registry-core/nifi-registry-web-docs/src/main/webapp/css/main.css
new file mode 100644
index 0000000..8b50064
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-web-docs/src/main/webapp/css/main.css
@@ -0,0 +1,217 @@
+/*
+ * 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.
+ */
+* {
+    margin: 0;
+    padding: 0;
+}
+
+#documentation-body {
+    width: 100%;
+    position: absolute;
+    top: 0;
+    right: 0;
+    bottom: 0;
+    left: 0;
+}
+
+/* banners */
+
+div.main-banner-header {
+    display: none;
+    font-weight: bold;
+    font-size: 1em;
+    text-align: center;
+    line-height: 15px;
+    color: #7e7e7e;
+    margin: 0px auto;
+    width: 100%;
+    height: 1em;
+    background-color: #fff;
+    background-image: url(../images/bgHeader.png);
+    background-position: center;
+    background-repeat: no-repeat;
+}
+
+div.main-banner-footer {
+    display: none;
+    color: #fff;
+    text-align: center;
+    font-weight: bold;
+    font-size: 1em;
+    overflow: visible;
+    background-color: #9eb9c7;
+    background-image: url(../images/bgBannerFoot.png);
+    background-repeat: repeat-x;
+    background-position: left top;
+}
+
+/* documentation */
+
+div.documentation-header {
+    border-bottom: 1px solid #d1dee5;
+    color: #365c6a;
+    font-size: 13px;
+    display: flex;
+}
+
+#component-list-toggle-link {
+    padding: 4px;
+    font-size: 14px;
+    font-weight: bold;
+    color: #264c58;
+    cursor: pointer;
+    width: 12px;
+    text-align: center;
+    align-self: flex-end;
+}
+
+#header-contents {
+    display: flex;
+    flex-wrap: wrap;
+}
+
+#nf-title {
+    font-size: 20px;
+    margin: 5px 5px 0px 5px;
+}
+
+#nf-version {
+    font-size: 14px;
+    margin: 11px 5px 0px 5px;
+    flex-grow: 1;
+}
+
+.version {
+    font-style: italic;
+    color: #aaa;
+}
+
+#selected-component {
+    font-size: 20px;
+    margin: 5px 5px 0px 5px;
+}
+
+/* content flex-box containers */
+
+#component-root-container {
+    display: flex;
+    flex-wrap: wrap;
+    align-items: stretch;
+    width: 100%;
+}
+
+#component-listing-container {
+    flex-grow: 1;
+    min-width: 312px;
+    max-width: 350px;
+    padding: 0px 4px 0px 4px;
+}
+
+#component-usage-container {
+    flex-grow: 4;
+    min-width: 300px;
+    padding: 0px 4px 0px 4px;
+}
+
+/* component listing */
+
+div.component-listing {
+    overflow: auto;
+    font-size: 16px;
+}
+
+div.component-listing div.section {
+    margin-bottom: 15px;
+}
+
+div.component-listing div.header {
+    font-weight: bold;
+    color: #264c58;
+}
+
+div.component-links ul {
+    list-style: none;
+}
+
+li.component-item {
+    padding: 2px;
+    padding-left: 4px;
+    border-left: 8px solid transparent;
+    font-family: "Open Sans","DejaVu Sans",sans-serif;
+    font-size: 15px;
+}
+
+li.component-item a {
+    color: #1e373f;
+}
+
+li.component-item:hover {
+    border-left: 8px solid #d1dee5;
+}
+
+li.component-item:hover a {
+    color: #264c58;
+}
+
+li.component-item.selected {
+    border-left: 8px solid #7098ad;
+}
+
+div.component-links span.no-components {
+    font-style: italic;
+    color: #777;
+}
+
+/* component filter control */
+
+#component-filter-controls {
+}
+
+#component-filter-container {
+    margin-left: 2px;
+}
+
+#component-filter {
+    font-size: 12px;
+    height: 18px;
+    line-height: 20px;
+    width: 98%;
+    float: left;
+}
+
+input.component-filter-list {
+    color: #888;
+    font-style: italic;
+}
+
+#component-filter-stats {
+    font-size: 9px;
+    font-weight: bold;
+    color: #9f6000;
+    clear: left; 
+    line-height: normal;
+    margin-left: 7px;
+}
+
+/* component usage */
+
+#component-usage {
+    overflow: auto;
+    width: 100%;
+    height: 100%;
+    position: absolute;
+}
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/6f26290d/nifi-registry-core/nifi-registry-web-docs/src/main/webapp/images/bgBannerFoot.png
----------------------------------------------------------------------
diff --git a/nifi-registry-core/nifi-registry-web-docs/src/main/webapp/images/bgBannerFoot.png b/nifi-registry-core/nifi-registry-web-docs/src/main/webapp/images/bgBannerFoot.png
new file mode 100755
index 0000000..16a17fe
Binary files /dev/null and b/nifi-registry-core/nifi-registry-web-docs/src/main/webapp/images/bgBannerFoot.png differ

http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/6f26290d/nifi-registry-core/nifi-registry-web-docs/src/main/webapp/images/bgHeader.png
----------------------------------------------------------------------
diff --git a/nifi-registry-core/nifi-registry-web-docs/src/main/webapp/images/bgHeader.png b/nifi-registry-core/nifi-registry-web-docs/src/main/webapp/images/bgHeader.png
new file mode 100644
index 0000000..3cf88c5
Binary files /dev/null and b/nifi-registry-core/nifi-registry-web-docs/src/main/webapp/images/bgHeader.png differ

http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/6f26290d/nifi-registry-core/nifi-registry-web-docs/src/main/webapp/images/bgTableHeader.png
----------------------------------------------------------------------
diff --git a/nifi-registry-core/nifi-registry-web-docs/src/main/webapp/images/bgTableHeader.png b/nifi-registry-core/nifi-registry-web-docs/src/main/webapp/images/bgTableHeader.png
new file mode 100755
index 0000000..8f5e058
Binary files /dev/null and b/nifi-registry-core/nifi-registry-web-docs/src/main/webapp/images/bgTableHeader.png differ

http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/6f26290d/nifi-registry-core/nifi-registry-web-docs/src/main/webapp/js/application.js
----------------------------------------------------------------------
diff --git a/nifi-registry-core/nifi-registry-web-docs/src/main/webapp/js/application.js b/nifi-registry-core/nifi-registry-web-docs/src/main/webapp/js/application.js
new file mode 100644
index 0000000..e0b4558
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-web-docs/src/main/webapp/js/application.js
@@ -0,0 +1,400 @@
+/*
+ * 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.
+ */
+
+/* global top */
+
+$(document).ready(function () {
+
+    var isUndefined = function (obj) {
+        return typeof obj === 'undefined';
+    };
+
+    var isNull = function (obj) {
+        return obj === null;
+    };
+
+    var isDefinedAndNotNull = function (obj) {
+        return !isUndefined(obj) && !isNull(obj);
+    };
+
+    /**
+     * Get the filter text.
+     * 
+     * @returns {unresolved}
+     */
+    var getFilterText = function () {
+        var filter = '';
+        var ruleFilter = $('#component-filter');
+        if (!ruleFilter.hasClass('component-filter-list')) {
+            filter = ruleFilter.val();
+        }
+        return filter;
+    };
+
+    var applyComponentFilter = function (componentContainer) {
+        var matchingComponents = 0;
+        var componentLinks = $(componentContainer).find('a.component-link, a.document-link');
+
+        if (componentLinks.length === 0) {
+            return matchingComponents;
+        }
+
+        // get the filter text
+        var filter = getFilterText();
+        if (filter !== '') {
+            var filterExp = new RegExp(filter, 'i');
+
+            // update the displayed rule count
+            $.each(componentLinks, function (_, componentLink) {
+                var a = $(componentLink);
+                var li = a.closest('li.component-item');
+
+                // get the rule text for matching
+                var componentName = a.text();
+
+                // see if any of the text from this rule matches
+                var componentMatches = componentName.search(filterExp) >= 0;
+
+                // handle whether the rule matches
+                if (componentMatches === true) {
+                    li.show();
+                    matchingComponents++;
+                } else {
+                    // hide the rule
+                    li.hide();
+                }
+            });
+        } else {
+            // ensure every rule is visible
+            componentLinks.closest('li.component-item').show();
+
+            // set the number of displayed rules
+            matchingComponents = componentLinks.length;
+        }
+
+        // show whether there are status if appropriate
+        var noMatching = componentContainer.find('span.no-matching');
+        if (matchingComponents === 0) {
+            noMatching.show();
+        } else {
+            noMatching.hide();
+        }
+
+        return matchingComponents;
+    };
+
+    var applyFilter = function () {
+        var matchingGeneral = applyComponentFilter($('#general-links'));
+        var matchingProcessors = applyComponentFilter($('#processor-links'));
+        var matchingControllerServices = applyComponentFilter($('#controller-service-links'));
+        var matchingReportingTasks = applyComponentFilter($('#reporting-task-links'));
+        var matchingDeveloper = applyComponentFilter($('#developer-links'));
+
+        // update the rule count
+        $('#displayed-components').text(matchingGeneral + matchingProcessors + matchingControllerServices + matchingReportingTasks + matchingDeveloper);
+    };
+
+    var selectComponent = function (selectedExtension, selectedBundleGroup, selectedBundleArtifact, selectedArtifactVersion) {
+        var componentLinks = $('a.component-link');
+
+        // consider each link
+        $.each(componentLinks, function () {
+            var componentLink = $(this);
+            var item = componentLink.closest('li.component-item');
+            var extension = item.find('span.extension-class').text();
+            var group = item.find('span.bundle-group').text();
+            var artifact = item.find('span.bundle-artifact').text();
+            var version = item.find('span.bundle-version').text();
+
+            if (extension === selectedExtension && group === selectedBundleGroup
+                && artifact === selectedBundleArtifact && version === selectedArtifactVersion) {
+
+                // remove all selected styles
+                $('li.component-item').removeClass('selected');
+
+                // select this links item
+                item.addClass('selected');
+
+                // set the header
+                $('#selected-component').text(componentLink.text());
+
+                // stop iteration
+                return false;
+            }
+        });
+    };
+
+    var selectDocument = function (documentName) {
+        var documentLinks = $('a.document-link');
+
+        // consider each link
+        $.each(documentLinks, function () {
+            var documentLink = $(this);
+            if (documentName === $.trim(documentLink.text())) {
+                // remove all selected styles
+                $('li.component-item').removeClass('selected');
+
+                // select this links item
+                documentLink.closest('li.component-item').addClass('selected');
+
+                // set the header
+                $('#selected-component').text(documentLink.text());
+
+                // stop iteration
+                return false;
+            }
+        });
+    };
+
+    // get the banners if we're not in the shell
+    var bannerHeaderHeight = 0;
+    var bannerFooterHeight = 0;
+    var banners = $.Deferred(function (deferred) {
+        if (top === window) {
+            $.ajax({
+                type: 'GET',
+                url: '../nifi-api/flow/banners',
+                dataType: 'json'
+            }).then(function (response) {
+                // ensure the banners response is specified
+                if (isDefinedAndNotNull(response.banners)) {
+                    if (isDefinedAndNotNull(response.banners.headerText) && response.banners.headerText !== '') {
+                        // update the header text
+                        var bannerHeader = $('#banner-header').text(response.banners.headerText).show();
+                        bannerHeaderHeight = bannerHeader.height();
+                    }
+
+                    if (isDefinedAndNotNull(response.banners.footerText) && response.banners.footerText !== '') {
+                        // update the footer text and show it
+                        var bannerFooter = $('#banner-footer').text(response.banners.footerText).show();
+                        bannerFooterHeight = bannerFooter.height();
+                    }
+                }
+
+                deferred.resolve();
+            }, function () {
+                deferred.reject();
+            });
+        } else {
+            deferred.resolve();
+        }
+    }).promise();
+
+    // get the about details
+    var about = $.ajax({
+        type: 'GET',
+        url: '../nifi-api/flow/about',
+        dataType: 'json'
+    }).done(function (response) {
+        var aboutDetails = response.about;
+
+        // set the document title and the about title
+        $('#nf-version').text(aboutDetails.version);
+    });
+
+    // once the banners have loaded, function with remainder of the page
+    $.when(banners, about).always(function () {
+        // define the function for filtering the list
+        $('#component-filter').keyup(function () {
+            applyFilter();
+        }).focus(function () {
+            if ($(this).hasClass('component-filter-list')) {
+                $(this).removeClass('component-filter-list').val('');
+            }
+        }).blur(function () {
+            if ($(this).val() === '') {
+                $(this).addClass('component-filter-list').val('Filter');
+            }
+        }).addClass('component-filter-list').val('Filter');
+
+        // get the component containers to install the window listener
+        var documentationHeader = $('#documentation-header');
+        var componentRootContainer = $('#component-root-container');
+        var componentListingContainer = $('#component-listing-container', componentRootContainer);
+        var componentListing = $('#component-listing', componentListingContainer);
+        var componentFilterControls = $('#component-filter-controls', componentRootContainer);
+        var componentUsageContainer = $('#component-usage-container', componentUsageContainer);
+        var componentUsage = $('#component-usage', componentUsageContainer);
+
+        var componentListingContainerPaddingX = 0;
+        componentListingContainerPaddingX += parseInt(componentListingContainer.css("padding-right"), 10);
+        componentListingContainerPaddingX += parseInt(componentListingContainer.css("padding-left"), 10);
+
+        var componentListingContainerPaddingY = 0;
+        componentListingContainerPaddingY += parseInt(componentListingContainer.css("padding-top"), 10);
+        componentListingContainerPaddingY += parseInt(componentListingContainer.css("padding-bottom"), 10);
+
+        var componentUsageContainerPaddingX = 0;
+        componentUsageContainerPaddingX += parseInt(componentUsageContainer.css("padding-right"), 10);
+        componentUsageContainerPaddingX += parseInt(componentUsageContainer.css("padding-left"), 10);
+
+        var componentUsageContainerPaddingY = 0;
+        componentUsageContainerPaddingY += parseInt(componentUsageContainer.css("padding-top"), 10);
+        componentUsageContainerPaddingY += parseInt(componentUsageContainer.css("padding-bottom"), 10);
+
+        var componentListingContainerMinWidth = parseInt(componentListingContainer.css("min-width"), 10) + componentListingContainerPaddingX;
+        var componentUsageContainerMinWidth = parseInt(componentUsageContainer.css("min-width"), 10) + componentUsageContainerPaddingX;
+        var smallDisplayBoundary = componentListingContainerMinWidth + componentUsageContainerMinWidth;
+
+        var cssComponentListingNormal = { backgroundColor: "#ffffff" };
+        var cssComponentListingSmall = { backgroundColor: "#fbfbfb" };
+
+        // add a window resize listener
+        $(window).resize(function () {
+            // This -1 is the border-top of #component-usage-container
+            var baseHeight = window.innerHeight - 1;
+            baseHeight -= bannerHeaderHeight;
+            baseHeight -= bannerFooterHeight;
+            baseHeight -= documentationHeader.height();
+
+            // resize component list accordingly
+            if (smallDisplayBoundary > window.innerWidth) {
+                // screen is not wide enough to display content usage
+                // within the same row.
+                componentListingContainer.css(cssComponentListingSmall);
+                componentListingContainer.css({
+                    borderBottom: "1px solid #ddddd8"
+                });
+                componentListing.css({
+                    height: "200px"
+                });
+                // resize the iframe accordingly
+                var componentUsageHeight = baseHeight;
+                if (componentListingContainer.is(":visible")) {
+                    componentUsageHeight -= componentListingContainer.height();
+                    componentUsageHeight -= 1; // border-bottom
+                }
+                componentUsageHeight -= componentListingContainerPaddingY;
+                componentUsageHeight -= componentUsageContainerPaddingY;
+                componentUsage.css({
+                    width: componentUsageContainer.width(),
+                    height: componentUsageHeight
+                });
+                componentUsageContainer.css({
+                    height: componentUsage.height()
+                });
+            } else {
+                componentListingContainer.css(cssComponentListingNormal);
+
+                var componentListingHeight = baseHeight;
+                componentListingHeight -= componentFilterControls.height();
+                componentListingHeight -= componentListingContainerPaddingY;
+                componentListing.css({
+                    height: componentListingHeight
+                });
+
+                // resize the iframe accordingly
+                componentUsage.css({
+                    width: componentUsageContainer.width(),
+                    height: baseHeight - componentUsageContainerPaddingY
+                });
+                componentUsageContainer.css({
+                    height: componentUsage.height()
+                });
+                componentListingContainer.css({
+                    borderBottom: "0px"
+                });
+            }
+        });
+
+
+        var toggleComponentListing = $('#component-list-toggle-link');
+        toggleComponentListing.click(function(){
+            componentListingContainer.toggle(0, function(){
+                toggleComponentListing.text($(this).is(":visible") ? "-" : "+");
+                $(window).resize();
+            });
+        });
+
+        // listen for loading of the iframe to update the title
+        $('#component-usage').on('load', function () {
+
+            // resize window accordingly.
+            $(window).resize();
+
+            var bundleAndComponent = '';
+            var href = $(this).contents().get(0).location.href;
+
+            // see if the href ends in index.htm[l]
+            var indexOfIndexHtml = href.indexOf('index.htm');
+            if (indexOfIndexHtml >= 0) {
+                href = href.substring(0, indexOfIndexHtml);
+            }
+
+            // remove the trailing separator
+            if (href.length > 0) {
+                var indexOfSeparator = href.lastIndexOf('/');
+                if (indexOfSeparator === href.length - 1) {
+                    href = href.substring(0, indexOfSeparator);
+                }
+            }
+
+            // remove the beginning bits
+            if (href.length > 0) {
+                var path = 'nifi-docs/components';
+                var indexOfPath = href.indexOf(path);
+                if (indexOfPath >= 0) {
+                    var indexOfBundle = indexOfPath + path.length + 1;
+                    if (indexOfBundle < href.length) {
+                        bundleAndComponent = href.substr(indexOfBundle);
+                    }
+                }
+            }
+
+            // if we could extract the bundle coordinates
+            if (bundleAndComponent !== '') {
+                var bundleTokens = bundleAndComponent.split('/');
+                if (bundleTokens.length === 4) {
+                    selectComponent(bundleTokens[3], bundleTokens[0], bundleTokens[1], bundleTokens[2]);
+                }
+            }
+        });
+        
+        // listen for on the rest api and user guide and developer guide and admin guide and overview
+        $('a.document-link').on('click', function() {
+            selectDocument($(this).text());
+        });
+
+        // get the initial selection
+        var initialLink = $('a.document-link:first');
+        var initialSelectionType = $.trim($('#initial-selection-type').text());
+
+        if (initialSelectionType !== '') {
+            var initialSelectionBundleGroup = $.trim($('#initial-selection-bundle-group').text());
+            var initialSelectionBundleArtifact = $.trim($('#initial-selection-bundle-artifact').text());
+            var initialSelectionBundleVersion = $.trim($('#initial-selection-bundle-version').text());
+
+            $('a.component-link').each(function () {
+                var componentLink = $(this);
+                var item = componentLink.closest('li.component-item');
+                var extension = item.find('span.extension-class').text();
+                var group = item.find('span.bundle-group').text();
+                var artifact = item.find('span.bundle-artifact').text();
+                var version = item.find('span.bundle-version').text();
+
+                if (extension === initialSelectionType && group === initialSelectionBundleGroup
+                        && artifact === initialSelectionBundleArtifact && version === initialSelectionBundleVersion) {
+                    initialLink = componentLink;
+                    return false;
+                }
+            });
+        }
+
+        // click the first link
+        initialLink[0].click();
+    });
+});


[39/51] [partial] nifi-registry git commit: NIFIREG-201 Refactoring project structure to better isolate extensions

Posted by kd...@apache.org.
http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/6f26290d/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/db/migration/FlowSnapshotEntityV1.java
----------------------------------------------------------------------
diff --git a/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/db/migration/FlowSnapshotEntityV1.java b/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/db/migration/FlowSnapshotEntityV1.java
new file mode 100644
index 0000000..ec6b9a5
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/db/migration/FlowSnapshotEntityV1.java
@@ -0,0 +1,96 @@
+/*
+ * 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.db.migration;
+
+import java.util.Date;
+import java.util.Objects;
+
+/**
+ * FlowSnapshot DB entity from the original database schema in 0.1.0, used for migration purposes.
+ */
+public class FlowSnapshotEntityV1 {
+
+    private String flowId;
+
+    private Integer version;
+
+    private Date created;
+
+    private String createdBy;
+
+    private String comments;
+
+    public String getFlowId() {
+        return flowId;
+    }
+
+    public void setFlowId(String flowId) {
+        this.flowId = flowId;
+    }
+
+    public Integer getVersion() {
+        return version;
+    }
+
+    public void setVersion(Integer version) {
+        this.version = version;
+    }
+
+    public Date getCreated() {
+        return created;
+    }
+
+    public void setCreated(Date created) {
+        this.created = created;
+    }
+
+    public String getCreatedBy() {
+        return createdBy;
+    }
+
+    public void setCreatedBy(String createdBy) {
+        this.createdBy = createdBy;
+    }
+
+    public String getComments() {
+        return comments;
+    }
+
+    public void setComments(String comments) {
+        this.comments = comments;
+    }
+
+    @Override
+    public int hashCode() {
+        return Objects.hash(this.flowId, this.version);
+    }
+
+    @Override
+    public boolean equals(Object obj) {
+        if (obj == null) {
+            return false;
+        }
+
+        if (!(obj instanceof FlowSnapshotEntityV1)) {
+            return false;
+        }
+
+        final FlowSnapshotEntityV1 other = (FlowSnapshotEntityV1) obj;
+        return Objects.equals(this.flowId, other.flowId) && Objects.equals(this.version, other.version);
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/6f26290d/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/db/migration/LegacyDataSourceFactory.java
----------------------------------------------------------------------
diff --git a/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/db/migration/LegacyDataSourceFactory.java b/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/db/migration/LegacyDataSourceFactory.java
new file mode 100644
index 0000000..72d3acf
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/db/migration/LegacyDataSourceFactory.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.db.migration;
+
+import org.apache.commons.lang3.StringUtils;
+import org.apache.nifi.registry.properties.NiFiRegistryProperties;
+import org.h2.jdbcx.JdbcConnectionPool;
+
+import javax.sql.DataSource;
+import java.io.File;
+
+/**
+ * NOTE: This DataSource factory was used in the original 0.1.0 release and remains to migrate data from the old database.
+ * This class is intentionally not a Spring bean, and will be used manually in the custom Flyway migration.
+ */
+public class LegacyDataSourceFactory {
+
+    private static final String DB_USERNAME_PASSWORD = "nifireg";
+    private static final int MAX_CONNECTIONS = 5;
+
+    // database file name
+    private static final String DATABASE_FILE_NAME = "nifi-registry";
+
+    private final NiFiRegistryProperties properties;
+
+    private JdbcConnectionPool connectionPool;
+
+    public LegacyDataSourceFactory(final NiFiRegistryProperties properties) {
+        this.properties = properties;
+    }
+
+    public DataSource getDataSource() {
+        if (connectionPool == null) {
+            final String databaseUrl = getDatabaseUrl(properties);
+            connectionPool = JdbcConnectionPool.create(databaseUrl, DB_USERNAME_PASSWORD, DB_USERNAME_PASSWORD);
+            connectionPool.setMaxConnections(MAX_CONNECTIONS);
+        }
+
+        return connectionPool;
+    }
+
+    public static String getDatabaseUrl(final NiFiRegistryProperties properties) {
+        // locate the repository directory
+        final String repositoryDirectoryPath = properties.getLegacyDatabaseDirectory();
+
+        // ensure the repository directory is specified
+        if (repositoryDirectoryPath == null) {
+            throw new NullPointerException("Database directory must be specified.");
+        }
+
+        // create a handle to the repository directory
+        final File repositoryDirectory = new File(repositoryDirectoryPath);
+
+        // get a handle to the database file
+        final File databaseFile = new File(repositoryDirectory, DATABASE_FILE_NAME);
+
+        // format the database url
+        String databaseUrl = "jdbc:h2:" + databaseFile + ";AUTOCOMMIT=OFF;DB_CLOSE_ON_EXIT=FALSE;LOCK_MODE=3";
+        String databaseUrlAppend = properties.getLegacyDatabaseUrlAppend();
+        if (StringUtils.isNotBlank(databaseUrlAppend)) {
+            databaseUrl += databaseUrlAppend;
+        }
+
+        return databaseUrl;
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/6f26290d/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/db/migration/LegacyDatabaseService.java
----------------------------------------------------------------------
diff --git a/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/db/migration/LegacyDatabaseService.java b/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/db/migration/LegacyDatabaseService.java
new file mode 100644
index 0000000..533fadd
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/db/migration/LegacyDatabaseService.java
@@ -0,0 +1,77 @@
+/*
+ * 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.db.migration;
+
+import org.springframework.jdbc.core.JdbcTemplate;
+
+import javax.sql.DataSource;
+import java.util.List;
+
+/**
+ * Service used to load data from original database used in the 0.1.0 release.
+ */
+public class LegacyDatabaseService {
+
+    private final JdbcTemplate jdbcTemplate;
+
+    public LegacyDatabaseService(final DataSource dataSource) {
+        this.jdbcTemplate = new JdbcTemplate(dataSource);
+    }
+
+    public List<BucketEntityV1> getAllBuckets() {
+        final String sql = "SELECT * FROM bucket ORDER BY name ASC";
+
+        return jdbcTemplate.query(sql, (rs, i) -> {
+            final BucketEntityV1 b = new BucketEntityV1();
+            b.setId(rs.getString("ID"));
+            b.setName(rs.getString("NAME"));
+            b.setDescription(rs.getString("DESCRIPTION"));
+            b.setCreated(rs.getTimestamp("CREATED"));
+            return b;
+        });
+    }
+
+    public List<FlowEntityV1> getAllFlows() {
+        final String sql = "SELECT * FROM flow f, bucket_item item WHERE item.id = f.id";
+
+        return jdbcTemplate.query(sql, (rs, i) -> {
+            final FlowEntityV1 flowEntity = new FlowEntityV1();
+            flowEntity.setId(rs.getString("ID"));
+            flowEntity.setName(rs.getString("NAME"));
+            flowEntity.setDescription(rs.getString("DESCRIPTION"));
+            flowEntity.setCreated(rs.getTimestamp("CREATED"));
+            flowEntity.setModified(rs.getTimestamp("MODIFIED"));
+            flowEntity.setBucketId(rs.getString("BUCKET_ID"));
+            return flowEntity;
+        });
+    }
+
+    public List<FlowSnapshotEntityV1> getAllFlowSnapshots() {
+        final String sql = "SELECT * FROM flow_snapshot fs";
+
+        return jdbcTemplate.query(sql, (rs, i) -> {
+            final FlowSnapshotEntityV1 fs = new FlowSnapshotEntityV1();
+            fs.setFlowId(rs.getString("FLOW_ID"));
+            fs.setVersion(rs.getInt("VERSION"));
+            fs.setCreated(rs.getTimestamp("CREATED"));
+            fs.setCreatedBy(rs.getString("CREATED_BY"));
+            fs.setComments(rs.getString("COMMENTS"));
+            return fs;
+        });
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/6f26290d/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/db/migration/LegacyEntityMapper.java
----------------------------------------------------------------------
diff --git a/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/db/migration/LegacyEntityMapper.java b/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/db/migration/LegacyEntityMapper.java
new file mode 100644
index 0000000..bf82aae
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/db/migration/LegacyEntityMapper.java
@@ -0,0 +1,63 @@
+/*
+ * 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.db.migration;
+
+import org.apache.nifi.registry.db.entity.BucketEntity;
+import org.apache.nifi.registry.db.entity.BucketItemEntityType;
+import org.apache.nifi.registry.db.entity.FlowEntity;
+import org.apache.nifi.registry.db.entity.FlowSnapshotEntity;
+
+/**
+ * Utility methods to map legacy DB entities to current DB entities.
+ *
+ * The initial implementations of these mappings will be almost a direct translation, but if future changes are made
+ * to the original tables these methods will handle the translation from old entity to new entity.
+ */
+public class LegacyEntityMapper {
+
+    public static BucketEntity createBucketEntity(final BucketEntityV1 bucketEntityV1) {
+        final BucketEntity bucketEntity = new BucketEntity();
+        bucketEntity.setId(bucketEntityV1.getId());
+        bucketEntity.setName(bucketEntityV1.getName());
+        bucketEntity.setDescription(bucketEntityV1.getDescription());
+        bucketEntity.setCreated(bucketEntityV1.getCreated());
+        return bucketEntity;
+    }
+
+    public static FlowEntity createFlowEntity(final FlowEntityV1 flowEntityV1) {
+        final FlowEntity flowEntity = new FlowEntity();
+        flowEntity.setId(flowEntityV1.getId());
+        flowEntity.setName(flowEntityV1.getName());
+        flowEntity.setDescription(flowEntityV1.getDescription());
+        flowEntity.setCreated(flowEntityV1.getCreated());
+        flowEntity.setModified(flowEntityV1.getModified());
+        flowEntity.setBucketId(flowEntityV1.getBucketId());
+        flowEntity.setType(BucketItemEntityType.FLOW);
+        return flowEntity;
+    }
+
+    public static FlowSnapshotEntity createFlowSnapshotEntity(final FlowSnapshotEntityV1 flowSnapshotEntityV1) {
+        final FlowSnapshotEntity flowSnapshotEntity = new FlowSnapshotEntity();
+        flowSnapshotEntity.setFlowId(flowSnapshotEntityV1.getFlowId());
+        flowSnapshotEntity.setVersion(flowSnapshotEntityV1.getVersion());
+        flowSnapshotEntity.setComments(flowSnapshotEntityV1.getComments());
+        flowSnapshotEntity.setCreated(flowSnapshotEntityV1.getCreated());
+        flowSnapshotEntity.setCreatedBy(flowSnapshotEntityV1.getCreatedBy());
+        return flowSnapshotEntity;
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/6f26290d/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/event/EventFactory.java
----------------------------------------------------------------------
diff --git a/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/event/EventFactory.java b/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/event/EventFactory.java
new file mode 100644
index 0000000..b837d6d
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/event/EventFactory.java
@@ -0,0 +1,97 @@
+/*
+ * 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.event;
+
+import org.apache.nifi.registry.bucket.Bucket;
+import org.apache.nifi.registry.flow.VersionedFlow;
+import org.apache.nifi.registry.flow.VersionedFlowSnapshot;
+import org.apache.nifi.registry.hook.Event;
+import org.apache.nifi.registry.hook.EventFieldName;
+import org.apache.nifi.registry.hook.EventType;
+import org.apache.nifi.registry.security.authorization.user.NiFiUserUtils;
+
+/**
+ * Factory to create Events from domain objects.
+ */
+public class EventFactory {
+
+    public static Event bucketCreated(final Bucket bucket) {
+        return new StandardEvent.Builder()
+                .eventType(EventType.CREATE_BUCKET)
+                .addField(EventFieldName.BUCKET_ID, bucket.getIdentifier())
+                .addField(EventFieldName.USER, NiFiUserUtils.getNiFiUserIdentity())
+                .build();
+    }
+
+    public static Event bucketUpdated(final Bucket bucket) {
+        return new StandardEvent.Builder()
+                .eventType(EventType.UPDATE_BUCKET)
+                .addField(EventFieldName.BUCKET_ID, bucket.getIdentifier())
+                .addField(EventFieldName.USER, NiFiUserUtils.getNiFiUserIdentity())
+                .build();
+    }
+
+    public static Event bucketDeleted(final Bucket bucket) {
+        return new StandardEvent.Builder()
+                .eventType(EventType.DELETE_BUCKET)
+                .addField(EventFieldName.BUCKET_ID, bucket.getIdentifier())
+                .addField(EventFieldName.USER, NiFiUserUtils.getNiFiUserIdentity())
+                .build();
+    }
+
+    public static Event flowCreated(final VersionedFlow versionedFlow) {
+        return new StandardEvent.Builder()
+                .eventType(EventType.CREATE_FLOW)
+                .addField(EventFieldName.BUCKET_ID, versionedFlow.getBucketIdentifier())
+                .addField(EventFieldName.FLOW_ID, versionedFlow.getIdentifier())
+                .addField(EventFieldName.USER, NiFiUserUtils.getNiFiUserIdentity())
+                .build();
+    }
+
+    public static Event flowUpdated(final VersionedFlow versionedFlow) {
+        return new StandardEvent.Builder()
+                .eventType(EventType.UPDATE_FLOW)
+                .addField(EventFieldName.BUCKET_ID, versionedFlow.getBucketIdentifier())
+                .addField(EventFieldName.FLOW_ID, versionedFlow.getIdentifier())
+                .addField(EventFieldName.USER, NiFiUserUtils.getNiFiUserIdentity())
+                .build();
+    }
+
+    public static Event flowDeleted(final VersionedFlow versionedFlow) {
+        return new StandardEvent.Builder()
+                .eventType(EventType.DELETE_FLOW)
+                .addField(EventFieldName.BUCKET_ID, versionedFlow.getBucketIdentifier())
+                .addField(EventFieldName.FLOW_ID, versionedFlow.getIdentifier())
+                .addField(EventFieldName.USER, NiFiUserUtils.getNiFiUserIdentity())
+                .build();
+    }
+
+    public static Event flowVersionCreated(final VersionedFlowSnapshot versionedFlowSnapshot) {
+        final String versionComments = versionedFlowSnapshot.getSnapshotMetadata().getComments() == null
+                ? "" : versionedFlowSnapshot.getSnapshotMetadata().getComments();
+
+        return new StandardEvent.Builder()
+                .eventType(EventType.CREATE_FLOW_VERSION)
+                .addField(EventFieldName.BUCKET_ID, versionedFlowSnapshot.getSnapshotMetadata().getBucketIdentifier())
+                .addField(EventFieldName.FLOW_ID, versionedFlowSnapshot.getSnapshotMetadata().getFlowIdentifier())
+                .addField(EventFieldName.VERSION, String.valueOf(versionedFlowSnapshot.getSnapshotMetadata().getVersion()))
+                .addField(EventFieldName.USER, versionedFlowSnapshot.getSnapshotMetadata().getAuthor())
+                .addField(EventFieldName.COMMENT, versionComments)
+                .build();
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/6f26290d/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/event/EventService.java
----------------------------------------------------------------------
diff --git a/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/event/EventService.java b/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/event/EventService.java
new file mode 100644
index 0000000..8a11493
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/event/EventService.java
@@ -0,0 +1,115 @@
+/*
+ * 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.event;
+
+import org.apache.nifi.registry.hook.Event;
+import org.apache.nifi.registry.hook.EventHookProvider;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.beans.factory.DisposableBean;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+
+import javax.annotation.PostConstruct;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.BlockingQueue;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.concurrent.LinkedBlockingQueue;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * Service used for publishing events and passing events to the hook providers.
+ */
+@Service
+public class EventService implements DisposableBean {
+
+    private static final Logger LOGGER = LoggerFactory.getLogger(EventService.class);
+
+    // Should only be a few events in the queue at a time, but setting a capacity just so it isn't unbounded
+    static final int EVENT_QUEUE_SIZE = 10_000;
+
+    private final BlockingQueue<Event> eventQueue;
+    private final ExecutorService scheduledExecutorService;
+    private final List<EventHookProvider> eventHookProviders;
+
+    @Autowired
+    public EventService(final List<EventHookProvider> eventHookProviders) {
+        this.eventQueue = new LinkedBlockingQueue<>(EVENT_QUEUE_SIZE);
+        this.scheduledExecutorService = Executors.newSingleThreadExecutor();
+        this.eventHookProviders = new ArrayList<>(eventHookProviders);
+    }
+
+    @PostConstruct
+    public void postConstruct() {
+        LOGGER.info("Starting event consumer...");
+
+        this.scheduledExecutorService.execute(() -> {
+            while (!Thread.interrupted()) {
+                try {
+                    final Event event = eventQueue.poll(1000, TimeUnit.MILLISECONDS);
+                    if (event == null) {
+                        continue;
+                    }
+
+                    // event was available so notify each provider, contain errors per-provider
+                    for(final EventHookProvider provider : eventHookProviders) {
+                        try {
+                            if (event.getEventType() == null
+                                    || (event.getEventType() != null && provider.shouldHandle(event.getEventType()))) {
+                                provider.handle(event);
+                            }
+                        } catch (Exception e) {
+                            LOGGER.error("Error handling event hook", e);
+                        }
+                    }
+                } catch (InterruptedException e) {
+                    LOGGER.warn("Interrupted while polling event queue");
+                    return;
+                }
+            }
+        });
+
+        LOGGER.info("Event consumer started!");
+    }
+
+    @Override
+    public void destroy() throws Exception {
+        LOGGER.info("Shutting down event consumer...");
+        this.scheduledExecutorService.shutdownNow();
+        LOGGER.info("Event consumer shutdown!");
+    }
+
+    public void publish(final Event event) {
+        if (event == null) {
+            return;
+        }
+
+        try {
+            event.validate();
+
+            final boolean queued = eventQueue.offer(event);
+            if (!queued) {
+                LOGGER.error("Unable to queue event because queue is full");
+            }
+        } catch (IllegalStateException e) {
+            LOGGER.error("Invalid event due to: " + e.getMessage(), e);
+        }
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/6f26290d/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/event/StandardEvent.java
----------------------------------------------------------------------
diff --git a/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/event/StandardEvent.java b/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/event/StandardEvent.java
new file mode 100644
index 0000000..4ad459d
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/event/StandardEvent.java
@@ -0,0 +1,124 @@
+/*
+ * 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.event;
+
+import org.apache.commons.lang3.Validate;
+import org.apache.nifi.registry.hook.Event;
+import org.apache.nifi.registry.hook.EventField;
+import org.apache.nifi.registry.hook.EventFieldName;
+import org.apache.nifi.registry.hook.EventType;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.List;
+
+/**
+ * Standard implementation of Event.
+ */
+public class StandardEvent implements Event {
+
+    private final EventType eventType;
+
+    private final List<EventField> eventFields;
+
+    private StandardEvent(final Builder builder) {
+        this.eventType = builder.eventType;
+        this.eventFields = Collections.unmodifiableList(builder.eventFields == null
+                ? Collections.emptyList() : new ArrayList<>(builder.eventFields));
+        Validate.notNull(this.eventType);
+    }
+
+    @Override
+    public EventType getEventType() {
+        return eventType;
+    }
+
+    @Override
+    public List<EventField> getFields() {
+        return eventFields;
+    }
+
+    @Override
+    public EventField getField(final EventFieldName fieldName) {
+        if (fieldName == null) {
+            return null;
+        }
+
+        return eventFields.stream().filter(e -> fieldName.equals(e.getName())).findFirst().orElse(null);
+    }
+
+    @Override
+    public void validate() throws IllegalStateException {
+        final int numProvidedFields = eventFields.size();
+        final int numRequiredFields = eventType.getFieldNames().size();
+
+        if (numProvidedFields != numRequiredFields) {
+            throw new IllegalStateException(numRequiredFields + " fields were required, but only " + numProvidedFields + " were provided");
+        }
+
+        for (int i=0; i < numRequiredFields; i++) {
+            final EventFieldName required = eventType.getFieldNames().get(i);
+            final EventFieldName provided = eventFields.get(i).getName();
+            if (!required.equals(provided)) {
+                throw new IllegalStateException("Expected " + required.name() + ", but found " + provided.name());
+            }
+        }
+    }
+
+    /**
+     * Builder for Events.
+     */
+    public static class Builder {
+
+        private EventType eventType;
+        private List<EventField> eventFields = new ArrayList<>();
+
+        public Builder eventType(final EventType eventType) {
+            this.eventType = eventType;
+            return this;
+        }
+
+        public Builder addField(final EventFieldName name, final String value) {
+            this.eventFields.add(new StandardEventField(name, value));
+            return this;
+        }
+
+        public Builder addField(final EventField arg) {
+            if (arg != null) {
+                this.eventFields.add(arg);
+            }
+            return this;
+        }
+
+        public Builder addFields(final Collection<EventField> fields) {
+            if (fields != null) {
+                this.eventFields.addAll(fields);
+            }
+            return this;
+        }
+
+        public Builder clearFields() {
+            this.eventFields.clear();
+            return this;
+        }
+
+        public Event build() {
+            return new StandardEvent(this);
+        }
+    }
+}

http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/6f26290d/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/event/StandardEventField.java
----------------------------------------------------------------------
diff --git a/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/event/StandardEventField.java b/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/event/StandardEventField.java
new file mode 100644
index 0000000..21266bb
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/event/StandardEventField.java
@@ -0,0 +1,49 @@
+/*
+ * 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.event;
+
+import org.apache.commons.lang3.Validate;
+import org.apache.nifi.registry.hook.EventField;
+import org.apache.nifi.registry.hook.EventFieldName;
+
+/**
+ * Standard implementation of EventField.
+ */
+public class StandardEventField implements EventField {
+
+    private final EventFieldName name;
+
+    private final String value;
+
+    public StandardEventField(final EventFieldName name, final String value) {
+        this.name = name;
+        this.value = value;
+        Validate.notNull(this.name);
+        Validate.notNull(this.value);
+    }
+
+    @Override
+    public EventFieldName getName() {
+        return name;
+    }
+
+    @Override
+    public String getValue() {
+        return value;
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/6f26290d/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/exception/AdministrationException.java
----------------------------------------------------------------------
diff --git a/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/exception/AdministrationException.java b/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/exception/AdministrationException.java
new file mode 100644
index 0000000..8f9180c
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/exception/AdministrationException.java
@@ -0,0 +1,39 @@
+/*
+ * 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.exception;
+
+/**
+ *
+ */
+public class AdministrationException extends RuntimeException {
+
+    public AdministrationException(Throwable cause) {
+        super(cause);
+    }
+
+    public AdministrationException(String message, Throwable cause) {
+        super(message, cause);
+    }
+
+    public AdministrationException(String message) {
+        super(message);
+    }
+
+    public AdministrationException() {
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/6f26290d/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/exception/ResourceNotFoundException.java
----------------------------------------------------------------------
diff --git a/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/exception/ResourceNotFoundException.java b/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/exception/ResourceNotFoundException.java
new file mode 100644
index 0000000..a83e9e2
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/exception/ResourceNotFoundException.java
@@ -0,0 +1,32 @@
+/*
+ * 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.exception;
+
+/**
+ * An exception that is thrown when an entity is not found.
+ */
+public class ResourceNotFoundException extends RuntimeException {
+
+    public ResourceNotFoundException(String message) {
+        super(message);
+    }
+
+    public ResourceNotFoundException(String message, Throwable cause) {
+        super(message, cause);
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/6f26290d/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/extension/ExtensionCloseable.java
----------------------------------------------------------------------
diff --git a/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/extension/ExtensionCloseable.java b/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/extension/ExtensionCloseable.java
new file mode 100644
index 0000000..b24f950
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/extension/ExtensionCloseable.java
@@ -0,0 +1,49 @@
+/*
+ * 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.extension;
+
+import java.io.Closeable;
+import java.io.IOException;
+
+public class ExtensionCloseable implements Closeable {
+    private final ClassLoader toSet;
+
+    private ExtensionCloseable(ClassLoader toSet) {
+        this.toSet = toSet;
+    }
+
+    public static ExtensionCloseable withComponentClassLoader(final ExtensionManager manager, final Class componentClass) {
+
+        final ClassLoader current = Thread.currentThread().getContextClassLoader();
+        final ExtensionCloseable closeable = new ExtensionCloseable(current);
+
+        ClassLoader componentClassLoader = manager.getExtensionClassLoader(componentClass.getName());
+        if (componentClassLoader == null) {
+            componentClassLoader = componentClass.getClassLoader();
+        }
+
+        Thread.currentThread().setContextClassLoader(componentClassLoader);
+        return closeable;
+    }
+
+    @Override
+    public void close() throws IOException {
+        if (toSet != null) {
+            Thread.currentThread().setContextClassLoader(toSet);
+        }
+    }
+}

http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/6f26290d/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/extension/ExtensionManager.java
----------------------------------------------------------------------
diff --git a/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/extension/ExtensionManager.java b/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/extension/ExtensionManager.java
new file mode 100644
index 0000000..ca3259d
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/extension/ExtensionManager.java
@@ -0,0 +1,217 @@
+/*
+ * 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.extension;
+
+import org.apache.commons.lang3.StringUtils;
+import org.apache.nifi.registry.hook.EventHookProvider;
+import org.apache.nifi.registry.security.authentication.IdentityProvider;
+import org.apache.nifi.registry.security.authorization.AccessPolicyProvider;
+import org.apache.nifi.registry.security.authorization.Authorizer;
+import org.apache.nifi.registry.security.authorization.UserGroupProvider;
+import org.apache.nifi.registry.flow.FlowPersistenceProvider;
+import org.apache.nifi.registry.properties.NiFiRegistryProperties;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Component;
+
+import javax.annotation.PostConstruct;
+import java.io.File;
+import java.net.MalformedURLException;
+import java.net.URL;
+import java.net.URLClassLoader;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Map;
+import java.util.ServiceLoader;
+import java.util.Set;
+import java.util.concurrent.atomic.AtomicBoolean;
+
+@Component
+public class ExtensionManager {
+
+    static final Logger LOGGER = LoggerFactory.getLogger(ExtensionManager.class);
+
+    private static final List<Class> EXTENSION_CLASSES;
+    static {
+        final List<Class> classes = new ArrayList<>();
+        classes.add(FlowPersistenceProvider.class);
+        classes.add(UserGroupProvider.class);
+        classes.add(AccessPolicyProvider.class);
+        classes.add(Authorizer.class);
+        classes.add(IdentityProvider.class);
+        classes.add(EventHookProvider.class);
+        EXTENSION_CLASSES = Collections.unmodifiableList(classes);
+    }
+
+    private final NiFiRegistryProperties properties;
+    private final Map<String,ExtensionClassLoader> classLoaderMap = new HashMap<>();
+    private final AtomicBoolean loaded = new AtomicBoolean(false);
+
+    @Autowired
+    public ExtensionManager(final NiFiRegistryProperties properties) {
+        this.properties = properties;
+    }
+
+    @PostConstruct
+    public synchronized void discoverExtensions() {
+        if (!loaded.get()) {
+            // get the list of class loaders to consider
+            final List<ExtensionClassLoader> classLoaders = getClassLoaders();
+
+            // for each class loader, attempt to load each extension class using the ServiceLoader
+            for (final ExtensionClassLoader extensionClassLoader : classLoaders) {
+                for (final Class extensionClass : EXTENSION_CLASSES) {
+                    loadExtensions(extensionClass, extensionClassLoader);
+                }
+            }
+
+            loaded.set(true);
+        }
+    }
+
+    public ClassLoader getExtensionClassLoader(final String canonicalClassName) {
+        if (StringUtils.isBlank(canonicalClassName)) {
+            throw new IllegalArgumentException("Class name can not be null");
+        }
+
+        return classLoaderMap.get(canonicalClassName);
+    }
+
+    /**
+     * Loads implementations of the given extension class from the given class loader.
+     *
+     * @param extensionClass the extension/service class
+     * @param extensionClassLoader the class loader to search
+     */
+    private void loadExtensions(final Class extensionClass, final ExtensionClassLoader extensionClassLoader) {
+        final ServiceLoader<?> serviceLoader = ServiceLoader.load(extensionClass, extensionClassLoader);
+        for (final Object o : serviceLoader) {
+            final String extensionClassName = o.getClass().getCanonicalName();
+            if (classLoaderMap.containsKey(extensionClassName)) {
+                final String currDir = extensionClassLoader.getRootDir();
+                final String existingDir = classLoaderMap.get(extensionClassName).getRootDir();
+                LOGGER.warn("Skipping {} from {} which was already found in {}", new Object[]{extensionClassName, currDir, existingDir});
+            } else {
+                classLoaderMap.put(o.getClass().getCanonicalName(), extensionClassLoader);
+            }
+        }
+    }
+
+    /**
+     * Gets all of the class loaders to consider for loading extensions.
+     *
+     * Includes the class loader of the web-app running the framework, plus a class loader for each additional
+     * directory specified in nifi-registry.properties.
+     *
+     * @return a list of extension class loaders
+     */
+    private List<ExtensionClassLoader> getClassLoaders() {
+        final List<ExtensionClassLoader> classLoaders = new ArrayList<>();
+
+        // start with the class loader that loaded ExtensionManager, should be WebAppClassLoader for API WAR
+        final ExtensionClassLoader frameworkClassLoader = new ExtensionClassLoader("web-api", new URL[0], this.getClass().getClassLoader());
+        classLoaders.add(frameworkClassLoader);
+
+        // we want to use the system class loader as the parent of the extension class loaders
+        ClassLoader systemClassLoader = FlowPersistenceProvider.class.getClassLoader();
+
+        // add a class loader for each extension dir
+        final Set<String> extensionDirs = properties.getExtensionsDirs();
+        for (final String dir : extensionDirs) {
+            if (!StringUtils.isBlank(dir)) {
+                final ExtensionClassLoader classLoader = createClassLoader(dir, systemClassLoader);
+                if (classLoader != null) {
+                    classLoaders.add(classLoader);
+                }
+            }
+        }
+
+        return classLoaders;
+    }
+
+    /**
+     * Creates a class loader for the given directory of resources.
+     *
+     * @param dir the dir of resources to add to the class loader
+     * @param parentClassLoader the parent class loader
+     * @return a class loader including all of the resources in the given dir, with the specified parent class loader
+     */
+    private ExtensionClassLoader createClassLoader(final String dir, final ClassLoader parentClassLoader) {
+        final File dirFile = new File(dir);
+
+        if (!dirFile.exists()) {
+            LOGGER.warn("Skipping extension directory that does not exist: " + dir);
+            return null;
+        }
+
+        if (!dirFile.canRead()) {
+            LOGGER.warn("Skipping extension directory that can not be read: " + dir);
+            return null;
+        }
+
+        final List<URL> resources = new LinkedList<>();
+
+        try {
+            resources.add(dirFile.toURI().toURL());
+        } catch (final MalformedURLException mfe) {
+            LOGGER.warn("Unable to add {} to classpath due to {}",
+                    new Object[]{ dirFile.getAbsolutePath(), mfe.getMessage()}, mfe);
+        }
+
+        if (dirFile.isDirectory()) {
+            final File[] files = dirFile.listFiles();
+            if (files != null) {
+                for (final File resource : files) {
+                    if (resource.isDirectory()) {
+                        LOGGER.warn("Recursive directories are not supported, skipping " + resource.getAbsolutePath());
+                    } else {
+                        try {
+                            resources.add(resource.toURI().toURL());
+                        } catch (final MalformedURLException mfe) {
+                            LOGGER.warn("Unable to add {} to classpath due to {}",
+                                    new Object[]{ resource.getAbsolutePath(), mfe.getMessage()}, mfe);
+                        }
+                    }
+                }
+            }
+        }
+
+        final URL[] urls = resources.toArray(new URL[resources.size()]);
+        return new ExtensionClassLoader(dir, urls, parentClassLoader);
+    }
+
+    /**
+     * Extend URLClassLoader to keep track of the root directory.
+     */
+    private static class ExtensionClassLoader extends URLClassLoader {
+
+        private final String rootDir;
+
+        public ExtensionClassLoader(final String rootDir, final URL[] urls, final ClassLoader parent) {
+            super(urls, parent);
+            this.rootDir = rootDir;
+        }
+
+        public String getRootDir() {
+            return rootDir;
+        }
+    }
+}

http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/6f26290d/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/provider/ProviderFactory.java
----------------------------------------------------------------------
diff --git a/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/provider/ProviderFactory.java b/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/provider/ProviderFactory.java
new file mode 100644
index 0000000..a3f3276
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/provider/ProviderFactory.java
@@ -0,0 +1,46 @@
+/*
+ * 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.provider;
+
+import java.util.List;
+
+import org.apache.nifi.registry.flow.FlowPersistenceProvider;
+import org.apache.nifi.registry.hook.EventHookProvider;
+
+/**
+ * A factory for obtaining the configured providers.
+ */
+public interface ProviderFactory {
+
+    /**
+     * Initialize the factory.
+     *
+     * @throws ProviderFactoryException if an error occurs during initialization
+     */
+    void initialize() throws ProviderFactoryException;
+
+    /**
+     * @return the configured FlowPersistenceProvider
+     */
+    FlowPersistenceProvider getFlowPersistenceProvider();
+
+    /**
+     * @return the configured FlowHookProviders
+     */
+    List<EventHookProvider> getEventHookProviders();
+
+}

http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/6f26290d/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/provider/ProviderFactoryException.java
----------------------------------------------------------------------
diff --git a/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/provider/ProviderFactoryException.java b/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/provider/ProviderFactoryException.java
new file mode 100644
index 0000000..3842b9e
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/provider/ProviderFactoryException.java
@@ -0,0 +1,38 @@
+/*
+ * 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.provider;
+
+/**
+ * An error that occurs while initializing a ProviderFactory.
+ */
+public class ProviderFactoryException extends RuntimeException {
+
+    public ProviderFactoryException() {
+    }
+
+    public ProviderFactoryException(String message) {
+        super(message);
+    }
+
+    public ProviderFactoryException(String message, Throwable cause) {
+        super(message, cause);
+    }
+
+    public ProviderFactoryException(Throwable cause) {
+        super(cause);
+    }
+}

http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/6f26290d/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/provider/StandardProviderConfigurationContext.java
----------------------------------------------------------------------
diff --git a/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/provider/StandardProviderConfigurationContext.java b/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/provider/StandardProviderConfigurationContext.java
new file mode 100644
index 0000000..8f186fd
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/provider/StandardProviderConfigurationContext.java
@@ -0,0 +1,39 @@
+/*
+ * 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.provider;
+
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * Standard configuration context to be passed to onConfigured method of Providers.
+ */
+public class StandardProviderConfigurationContext implements ProviderConfigurationContext {
+
+    private final Map<String,String> properties;
+
+    public StandardProviderConfigurationContext(final Map<String, String> properties) {
+        this.properties = Collections.unmodifiableMap(new HashMap<>(properties));
+    }
+
+    @Override
+    public Map<String, String> getProperties() {
+        return properties;
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/6f26290d/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/provider/StandardProviderFactory.java
----------------------------------------------------------------------
diff --git a/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/provider/StandardProviderFactory.java b/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/provider/StandardProviderFactory.java
new file mode 100644
index 0000000..65ba914
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/provider/StandardProviderFactory.java
@@ -0,0 +1,217 @@
+/*
+ * 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.provider;
+
+import org.apache.nifi.registry.extension.ExtensionManager;
+import org.apache.nifi.registry.flow.FlowPersistenceProvider;
+import org.apache.nifi.registry.hook.EventHookProvider;
+import org.apache.nifi.registry.properties.NiFiRegistryProperties;
+import org.apache.nifi.registry.provider.generated.Property;
+import org.apache.nifi.registry.provider.generated.Providers;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.xml.sax.SAXException;
+
+import javax.annotation.PostConstruct;
+import javax.xml.XMLConstants;
+import javax.xml.bind.JAXBContext;
+import javax.xml.bind.JAXBElement;
+import javax.xml.bind.JAXBException;
+import javax.xml.bind.Unmarshaller;
+import javax.xml.transform.stream.StreamSource;
+import javax.xml.validation.Schema;
+import javax.xml.validation.SchemaFactory;
+import java.io.File;
+import java.lang.reflect.Constructor;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.atomic.AtomicReference;
+
+/**
+ * Standard implementation of ProviderFactory.
+ */
+@Configuration
+public class StandardProviderFactory implements ProviderFactory {
+
+    private static final Logger LOGGER = LoggerFactory.getLogger(StandardProviderFactory.class);
+
+    private static final String PROVIDERS_XSD = "/providers.xsd";
+    private static final String JAXB_GENERATED_PATH = "org.apache.nifi.registry.provider.generated";
+    private static final JAXBContext JAXB_CONTEXT = initializeJaxbContext();
+
+    /**
+     * Load the JAXBContext.
+     */
+    private static JAXBContext initializeJaxbContext() {
+        try {
+            return JAXBContext.newInstance(JAXB_GENERATED_PATH, StandardProviderFactory.class.getClassLoader());
+        } catch (JAXBException e) {
+            throw new RuntimeException("Unable to create JAXBContext.", e);
+        }
+    }
+
+    private final NiFiRegistryProperties properties;
+    private final ExtensionManager extensionManager;
+    private final AtomicReference<Providers> providersHolder = new AtomicReference<>(null);
+
+    private FlowPersistenceProvider flowPersistenceProvider;
+    private List<EventHookProvider> eventHookProviders;
+
+    @Autowired
+    public StandardProviderFactory(final NiFiRegistryProperties properties, final ExtensionManager extensionManager) {
+        this.properties = properties;
+        this.extensionManager = extensionManager;
+
+        if (this.properties == null) {
+            throw new IllegalStateException("NiFiRegistryProperties cannot be null");
+        }
+
+        if (this.extensionManager == null) {
+            throw new IllegalStateException("ExtensionManager cannot be null");
+        }
+    }
+
+    @PostConstruct
+    @Override
+    public synchronized void initialize() throws ProviderFactoryException {
+        if (providersHolder.get() == null) {
+            final File providersConfigFile = properties.getProvidersConfigurationFile();
+            if (providersConfigFile.exists()) {
+                try {
+                    // find the schema
+                    final SchemaFactory schemaFactory = SchemaFactory.newInstance(XMLConstants.W3C_XML_SCHEMA_NS_URI);
+                    final Schema schema = schemaFactory.newSchema(StandardProviderFactory.class.getResource(PROVIDERS_XSD));
+
+                    // attempt to unmarshal
+                    final Unmarshaller unmarshaller = JAXB_CONTEXT.createUnmarshaller();
+                    unmarshaller.setSchema(schema);
+
+                    // set the holder for later use
+                    final JAXBElement<Providers> element = unmarshaller.unmarshal(new StreamSource(providersConfigFile), Providers.class);
+                    providersHolder.set(element.getValue());
+                } catch (SAXException | JAXBException e) {
+                    throw new ProviderFactoryException("Unable to load the providers configuration file at: " + providersConfigFile.getAbsolutePath(), e);
+                }
+            } else {
+                throw new ProviderFactoryException("Unable to find the providers configuration file at " + providersConfigFile.getAbsolutePath());
+            }
+        }
+    }
+
+    @Bean
+    @Override
+    public synchronized FlowPersistenceProvider getFlowPersistenceProvider() {
+        if (flowPersistenceProvider == null) {
+            if (providersHolder.get() == null) {
+                throw new ProviderFactoryException("ProviderFactory must be initialized before obtaining a Provider");
+            }
+
+            final Providers providers = providersHolder.get();
+            final org.apache.nifi.registry.provider.generated.Provider jaxbFlowProvider = providers.getFlowPersistenceProvider();
+            final String flowProviderClassName = jaxbFlowProvider.getClazz();
+
+            try {
+                final ClassLoader classLoader = extensionManager.getExtensionClassLoader(flowProviderClassName);
+                if (classLoader == null) {
+                    throw new IllegalStateException("Extension not found in any of the configured class loaders: " + flowProviderClassName);
+                }
+
+                final Class<?> rawFlowProviderClass = Class.forName(flowProviderClassName, true, classLoader);
+                final Class<? extends FlowPersistenceProvider> flowProviderClass = rawFlowProviderClass.asSubclass(FlowPersistenceProvider.class);
+
+                final Constructor constructor = flowProviderClass.getConstructor();
+                flowPersistenceProvider = (FlowPersistenceProvider) constructor.newInstance();
+
+                LOGGER.info("Instantiated FlowPersistenceProvider with class name {}", new Object[] {flowProviderClassName});
+            } catch (Exception e) {
+                throw new ProviderFactoryException("Error creating FlowPersistenceProvider with class name: " + flowProviderClassName, e);
+            }
+
+            final ProviderConfigurationContext configurationContext = createConfigurationContext(jaxbFlowProvider.getProperty());
+            flowPersistenceProvider.onConfigured(configurationContext);
+            LOGGER.info("Configured FlowPersistenceProvider with class name {}", new Object[] {flowProviderClassName});
+        }
+
+        return flowPersistenceProvider;
+    }
+
+    @Bean
+    @Override
+    public List<EventHookProvider> getEventHookProviders() {
+        if (eventHookProviders == null) {
+            eventHookProviders = new ArrayList<>();
+
+            if (providersHolder.get() == null) {
+                throw new ProviderFactoryException("ProviderFactory must be initialized before obtaining a Provider");
+            }
+
+            final Providers providers = providersHolder.get();
+            final List<org.apache.nifi.registry.provider.generated.Provider> jaxbHookProvider = providers.getEventHookProvider();
+
+            if(jaxbHookProvider == null || jaxbHookProvider.isEmpty()) {
+                // no hook provided
+                return eventHookProviders;
+            }
+
+            for (org.apache.nifi.registry.provider.generated.Provider hookProvider : jaxbHookProvider) {
+
+                final String hookProviderClassName = hookProvider.getClazz();
+                EventHookProvider hook;
+
+                try {
+                    final ClassLoader classLoader = extensionManager.getExtensionClassLoader(hookProviderClassName);
+                    if (classLoader == null) {
+                        throw new IllegalStateException("Extension not found in any of the configured class loaders: " + hookProviderClassName);
+                    }
+
+                    final Class<?> rawHookProviderClass = Class.forName(hookProviderClassName, true, classLoader);
+                    final Class<? extends EventHookProvider> hookProviderClass = rawHookProviderClass.asSubclass(EventHookProvider.class);
+
+                    final Constructor constructor = hookProviderClass.getConstructor();
+                    hook = (EventHookProvider) constructor.newInstance();
+
+                    LOGGER.info("Instantiated EventHookProvider with class name {}", new Object[] {hookProviderClassName});
+                } catch (Exception e) {
+                    throw new ProviderFactoryException("Error creating EventHookProvider with class name: " + hookProviderClassName, e);
+                }
+
+                final ProviderConfigurationContext configurationContext = createConfigurationContext(hookProvider.getProperty());
+                hook.onConfigured(configurationContext);
+                eventHookProviders.add(hook);
+                LOGGER.info("Configured EventHookProvider with class name {}", new Object[] {hookProviderClassName});
+            }
+        }
+
+        return eventHookProviders;
+    }
+
+    private ProviderConfigurationContext createConfigurationContext(final List<Property> configProperties) {
+        final Map<String,String> properties = new HashMap<>();
+
+        if (configProperties != null) {
+            configProperties.stream().forEach(p -> properties.put(p.getName(), p.getValue()));
+        }
+
+        return new StandardProviderConfigurationContext(properties);
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/6f26290d/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/provider/flow/FileSystemFlowPersistenceProvider.java
----------------------------------------------------------------------
diff --git a/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/provider/flow/FileSystemFlowPersistenceProvider.java b/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/provider/flow/FileSystemFlowPersistenceProvider.java
new file mode 100644
index 0000000..071656d
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/provider/flow/FileSystemFlowPersistenceProvider.java
@@ -0,0 +1,186 @@
+/*
+ * 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.provider.flow;
+
+import org.apache.commons.io.IOUtils;
+import org.apache.commons.lang3.StringUtils;
+import org.apache.nifi.registry.flow.FlowPersistenceException;
+import org.apache.nifi.registry.flow.FlowPersistenceProvider;
+import org.apache.nifi.registry.flow.FlowSnapshotContext;
+import org.apache.nifi.registry.provider.ProviderConfigurationContext;
+import org.apache.nifi.registry.provider.ProviderCreationException;
+import org.apache.nifi.registry.util.FileUtils;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.util.Map;
+
+/**
+ * A FlowPersistenceProvider that uses the local filesystem for storage.
+ */
+public class FileSystemFlowPersistenceProvider implements FlowPersistenceProvider {
+
+    static final Logger LOGGER = LoggerFactory.getLogger(FileSystemFlowPersistenceProvider.class);
+
+    static final String FLOW_STORAGE_DIR_PROP = "Flow Storage Directory";
+
+    static final String SNAPSHOT_EXTENSION = ".snapshot";
+
+    private File flowStorageDir;
+
+    @Override
+    public void onConfigured(final ProviderConfigurationContext configurationContext) throws ProviderCreationException {
+        final Map<String,String> props = configurationContext.getProperties();
+        if (!props.containsKey(FLOW_STORAGE_DIR_PROP)) {
+            throw new ProviderCreationException("The property " + FLOW_STORAGE_DIR_PROP + " must be provided");
+        }
+
+        final String flowStorageDirValue = props.get(FLOW_STORAGE_DIR_PROP);
+        if (StringUtils.isBlank(flowStorageDirValue)) {
+            throw new ProviderCreationException("The property " + FLOW_STORAGE_DIR_PROP + " cannot be null or blank");
+        }
+
+        try {
+            flowStorageDir = new File(flowStorageDirValue);
+            FileUtils.ensureDirectoryExistAndCanReadAndWrite(flowStorageDir);
+            LOGGER.info("Configured FileSystemFlowPersistenceProvider with Flow Storage Directory {}", new Object[] {flowStorageDir.getAbsolutePath()});
+        } catch (IOException e) {
+            throw new ProviderCreationException(e);
+        }
+    }
+
+    @Override
+    public synchronized void saveFlowContent(final FlowSnapshotContext context, final byte[] content) throws FlowPersistenceException {
+        final File bucketDir = new File(flowStorageDir, context.getBucketId());
+        try {
+            FileUtils.ensureDirectoryExistAndCanReadAndWrite(bucketDir);
+        } catch (IOException e) {
+            throw new FlowPersistenceException("Error accessing bucket directory at " + bucketDir.getAbsolutePath(), e);
+        }
+
+        final File flowDir = new File(bucketDir, context.getFlowId());
+        try {
+            FileUtils.ensureDirectoryExistAndCanReadAndWrite(flowDir);
+        } catch (IOException e) {
+            throw new FlowPersistenceException("Error accessing flow directory at " + flowDir.getAbsolutePath(), e);
+        }
+
+        final String versionString = String.valueOf(context.getVersion());
+        final File versionDir = new File(flowDir, versionString);
+        try {
+            FileUtils.ensureDirectoryExistAndCanReadAndWrite(versionDir);
+        } catch (IOException e) {
+            throw new FlowPersistenceException("Error accessing version directory at " + versionDir.getAbsolutePath(), e);
+        }
+
+        final File versionFile = new File(versionDir, versionString + SNAPSHOT_EXTENSION);
+        if (versionFile.exists()) {
+            throw new FlowPersistenceException("Unable to save, a snapshot already exists with version " + versionString);
+        }
+
+        if (LOGGER.isDebugEnabled()) {
+            LOGGER.debug("Saving snapshot with filename {}", new Object[] {versionFile.getAbsolutePath()});
+        }
+
+        try (final OutputStream out = new FileOutputStream(versionFile)) {
+            out.write(content);
+            out.flush();
+        } catch (Exception e) {
+            throw new FlowPersistenceException("Unable to write snapshot to disk due to " + e.getMessage(), e);
+        }
+    }
+
+    @Override
+    public synchronized byte[] getFlowContent(final String bucketId, final String flowId, final int version) throws FlowPersistenceException {
+        final File snapshotFile = getSnapshotFile(bucketId, flowId, version);
+        if (LOGGER.isDebugEnabled()) {
+            LOGGER.debug("Retrieving snapshot with filename {}", new Object[] {snapshotFile.getAbsolutePath()});
+        }
+
+        if (!snapshotFile.exists()) {
+            return null;
+        }
+
+        try (final InputStream in = new FileInputStream(snapshotFile)){
+            return IOUtils.toByteArray(in);
+        } catch (IOException e) {
+            throw new FlowPersistenceException("Error reading snapshot file: " + snapshotFile.getAbsolutePath(), e);
+        }
+    }
+
+    @Override
+    public synchronized void deleteAllFlowContent(final String bucketId, final String flowId) throws FlowPersistenceException {
+        final File flowDir = new File(flowStorageDir, bucketId + "/" + flowId);
+        if (!flowDir.exists()) {
+            LOGGER.debug("Snapshot directory does not exist at {}", new Object[] {flowDir.getAbsolutePath()});
+            return;
+        }
+
+        // delete everything under the flow directory
+        try {
+            org.apache.commons.io.FileUtils.cleanDirectory(flowDir);
+        } catch (IOException e) {
+            throw new FlowPersistenceException("Error deleting snapshots at " + flowDir.getAbsolutePath(), e);
+        }
+
+        // delete the directory for the flow
+        final boolean flowDirDeleted = flowDir.delete();
+        if (!flowDirDeleted) {
+            LOGGER.error("Unable to delete flow directory: " + flowDir.getAbsolutePath());
+        }
+
+        // delete the directory for the bucket if there is nothing left
+        final File bucketDir = new File(flowStorageDir, bucketId);
+        final File[] bucketFiles = bucketDir.listFiles();
+        if (bucketFiles.length == 0) {
+            final boolean deletedBucket = bucketDir.delete();
+            if (!deletedBucket) {
+                LOGGER.error("Unable to delete bucket directory: " + flowDir.getAbsolutePath());
+            }
+        }
+    }
+
+    @Override
+    public synchronized void deleteFlowContent(final String bucketId, final String flowId, final int version) throws FlowPersistenceException {
+        final File snapshotFile = getSnapshotFile(bucketId, flowId, version);
+        if (!snapshotFile.exists()) {
+            LOGGER.debug("Snapshot file does not exist at {}", new Object[] {snapshotFile.getAbsolutePath()});
+            return;
+        }
+
+        final boolean deleted = snapshotFile.delete();
+        if (!deleted) {
+            throw new FlowPersistenceException("Unable to delete snapshot at " + snapshotFile.getAbsolutePath());
+        }
+
+        if (LOGGER.isDebugEnabled()) {
+            LOGGER.debug("Deleted snapshot at {}", new Object[] {snapshotFile.getAbsolutePath()});
+        }
+    }
+
+    protected File getSnapshotFile(final String bucketId, final String flowId, final int version) {
+        final String snapshotFilename = bucketId + "/" + flowId + "/" + version + "/" + version + SNAPSHOT_EXTENSION;
+        return new File(flowStorageDir, snapshotFilename);
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/6f26290d/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/provider/flow/StandardFlowSnapshotContext.java
----------------------------------------------------------------------
diff --git a/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/provider/flow/StandardFlowSnapshotContext.java b/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/provider/flow/StandardFlowSnapshotContext.java
new file mode 100644
index 0000000..1728513
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/provider/flow/StandardFlowSnapshotContext.java
@@ -0,0 +1,172 @@
+/*
+ * 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.provider.flow;
+
+import org.apache.commons.lang3.Validate;
+import org.apache.nifi.registry.bucket.Bucket;
+import org.apache.nifi.registry.flow.FlowSnapshotContext;
+import org.apache.nifi.registry.flow.VersionedFlow;
+import org.apache.nifi.registry.flow.VersionedFlowSnapshotMetadata;
+
+/**
+ * Standard implementation of FlowSnapshotContext.
+ */
+public class StandardFlowSnapshotContext implements FlowSnapshotContext {
+
+    private final String bucketId;
+    private final String bucketName;
+    private final String flowId;
+    private final String flowName;
+    private final int version;
+    private final String comments;
+    private final String author;
+    private final long snapshotTimestamp;
+
+    private StandardFlowSnapshotContext(final Builder builder) {
+        this.bucketId = builder.bucketId;
+        this.bucketName = builder.bucketName;
+        this.flowId = builder.flowId;
+        this.flowName = builder.flowName;
+        this.version = builder.version;
+        this.comments = builder.comments;
+        this.author = builder.author;
+        this.snapshotTimestamp = builder.snapshotTimestamp;
+
+        Validate.notBlank(bucketId);
+        Validate.notBlank(bucketName);
+        Validate.notBlank(flowId);
+        Validate.notBlank(flowName);
+        Validate.isTrue(version > 0);
+        Validate.isTrue(snapshotTimestamp > 0);
+    }
+
+    @Override
+    public String getBucketId() {
+        return bucketId;
+    }
+
+    @Override
+    public String getBucketName() {
+        return bucketName;
+    }
+
+    @Override
+    public String getFlowId() {
+        return flowId;
+    }
+
+    @Override
+    public String getFlowName() {
+        return flowName;
+    }
+
+    @Override
+    public int getVersion() {
+        return version;
+    }
+
+    @Override
+    public String getComments() {
+        return comments;
+    }
+
+    @Override
+    public long getSnapshotTimestamp() {
+        return snapshotTimestamp;
+    }
+
+    @Override
+    public String getAuthor() {
+        return author;
+    }
+
+    /**
+     * Builder for creating instances of StandardFlowSnapshotContext.
+     */
+    public static class Builder {
+
+        private String bucketId;
+        private String bucketName;
+        private String flowId;
+        private String flowName;
+        private int version;
+        private String comments;
+        private String author;
+        private long snapshotTimestamp;
+
+        public Builder() {
+
+        }
+
+        public Builder(final Bucket bucket, final VersionedFlow versionedFlow, final VersionedFlowSnapshotMetadata snapshotMetadata) {
+            bucketId(bucket.getIdentifier());
+            bucketName(bucket.getName());
+            flowId(snapshotMetadata.getFlowIdentifier());
+            flowName(versionedFlow.getName());
+            version(snapshotMetadata.getVersion());
+            comments(snapshotMetadata.getComments());
+            author(snapshotMetadata.getAuthor());
+            snapshotTimestamp(snapshotMetadata.getTimestamp());
+        }
+
+        public Builder bucketId(final String bucketId) {
+            this.bucketId = bucketId;
+            return this;
+        }
+
+        public Builder bucketName(final String bucketName) {
+            this.bucketName = bucketName;
+            return this;
+        }
+
+        public Builder flowId(final String flowId) {
+            this.flowId = flowId;
+            return this;
+        }
+
+        public Builder flowName(final String flowName) {
+            this.flowName = flowName;
+            return this;
+        }
+
+        public Builder version(final int version) {
+            this.version = version;
+            return this;
+        }
+
+        public Builder comments(final String comments) {
+            this.comments = comments;
+            return this;
+        }
+
+        public Builder author(final String author) {
+            this.author = author;
+            return this;
+        }
+
+        public Builder snapshotTimestamp(final long snapshotTimestamp) {
+            this.snapshotTimestamp = snapshotTimestamp;
+            return this;
+        }
+
+        public StandardFlowSnapshotContext build() {
+            return new StandardFlowSnapshotContext(this);
+        }
+
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/6f26290d/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/provider/flow/git/Bucket.java
----------------------------------------------------------------------
diff --git a/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/provider/flow/git/Bucket.java b/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/provider/flow/git/Bucket.java
new file mode 100644
index 0000000..3595d84
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/provider/flow/git/Bucket.java
@@ -0,0 +1,87 @@
+/*
+ * 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.provider.flow.git;
+
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Optional;
+import java.util.stream.Collectors;
+
+class Bucket {
+    private final String bucketId;
+    private String bucketDirName;
+
+    /**
+     * Flow ID to Flow.
+     */
+    private Map<String, Flow> flows = new HashMap<>();
+
+    public Bucket(String bucketId) {
+        this.bucketId = bucketId;
+    }
+
+    public String getBucketId() {
+        return bucketId;
+    }
+
+    /**
+     * Returns the directory name of this bucket.
+     * @return can be different from original bucket name if it contained sanitized character.
+     */
+    public String getBucketDirName() {
+        return bucketDirName;
+    }
+
+    /**
+     * Set the name of bucket directory.
+     * @param bucketDirName The directory name must be sanitized, use {@link org.apache.nifi.registry.util.FileUtils#sanitizeFilename(String)} to do so.
+     */
+    public void setBucketDirName(String bucketDirName) {
+        this.bucketDirName = bucketDirName;
+    }
+
+    public Flow getFlowOrCreate(String flowId) {
+        return this.flows.computeIfAbsent(flowId, k -> new Flow(flowId));
+    }
+
+    public Optional<Flow> getFlow(String flowId) {
+        return Optional.ofNullable(flows.get(flowId));
+    }
+
+    public void removeFlow(String flowId) {
+        flows.remove(flowId);
+    }
+
+    public boolean isEmpty() {
+        return flows.isEmpty();
+    }
+
+    /**
+     * Serialize the latest version of this Bucket meta data.
+     * @return serialized bucket
+     */
+    Map<String, Object> serialize() {
+        final Map<String, Object> map = new HashMap<>();
+
+        map.put(GitFlowMetaData.LAYOUT_VERSION, GitFlowMetaData.CURRENT_LAYOUT_VERSION);
+        map.put(GitFlowMetaData.BUCKET_ID, bucketId);
+        map.put(GitFlowMetaData.FLOWS,
+                flows.keySet().stream().collect(Collectors.toMap(k -> k, k -> flows.get(k).serialize())));
+
+        return map;
+    }
+}

http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/6f26290d/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/provider/flow/git/Flow.java
----------------------------------------------------------------------
diff --git a/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/provider/flow/git/Flow.java b/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/provider/flow/git/Flow.java
new file mode 100644
index 0000000..1bc7f3f
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/provider/flow/git/Flow.java
@@ -0,0 +1,105 @@
+/*
+ * 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.provider.flow.git;
+
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Optional;
+
+class Flow {
+    /**
+     * The ID of a Flow. It never changes.
+     */
+    private final String flowId;
+
+    /**
+     * A version to a Flow pointer.
+     */
+    private final Map<Integer, FlowPointer> versions = new HashMap<>();
+
+    public Flow(String flowId) {
+        this.flowId = flowId;
+    }
+
+    public boolean hasVersion(int version) {
+        return versions.containsKey(version);
+    }
+
+    public FlowPointer getFlowVersion(int version) {
+        return versions.get(version);
+    }
+
+    public void putVersion(int version, FlowPointer pointer) {
+        versions.put(version, pointer);
+    }
+
+    public static class FlowPointer {
+        private String gitRev;
+        private String objectId;
+        private final String fileName;
+
+        /**
+         * Create new FlowPointer instance.
+         * @param fileName The filename must be sanitized, use {@link org.apache.nifi.registry.util.FileUtils#sanitizeFilename(String)} to do so.
+         */
+        public FlowPointer(String fileName) {
+            this.fileName = fileName;
+        }
+
+        public void setGitRev(String gitRev) {
+            this.gitRev = gitRev;
+        }
+
+        public String getGitRev() {
+            return gitRev;
+        }
+
+        public String getFileName() {
+            return fileName;
+        }
+
+        public String getObjectId() {
+            return objectId;
+        }
+
+        public void setObjectId(String objectId) {
+            this.objectId = objectId;
+        }
+    }
+
+    /**
+     * Serialize the latest version of this Flow meta data.
+     * @return serialized flow
+     */
+    Map<String, Object> serialize() {
+        final Map<String, Object> map = new HashMap<>();
+        final Optional<Integer> latestVerOpt = getLatestVersion();
+        if (!latestVerOpt.isPresent()) {
+            throw new IllegalStateException("Flow version is not added yet, can not be serialized.");
+        }
+        final Integer latestVer = latestVerOpt.get();
+        map.put(GitFlowMetaData.VER, latestVer);
+        map.put(GitFlowMetaData.FILE, versions.get(latestVer).fileName);
+
+        return map;
+    }
+
+    Optional<Integer> getLatestVersion() {
+        return versions.keySet().stream().reduce(Integer::max);
+    }
+
+}


[04/51] [partial] nifi-registry git commit: NIFIREG-201 Refactoring project structure to better isolate extensions

Posted by kd...@apache.org.
http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/6f26290d/nifi-registry-core/nifi-registry-web-ui/src/main/webapp/components/administration/nf-registry-administration.html
----------------------------------------------------------------------
diff --git a/nifi-registry-core/nifi-registry-web-ui/src/main/webapp/components/administration/nf-registry-administration.html b/nifi-registry-core/nifi-registry-web-ui/src/main/webapp/components/administration/nf-registry-administration.html
new file mode 100644
index 0000000..23f40a0
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-web-ui/src/main/webapp/components/administration/nf-registry-administration.html
@@ -0,0 +1,40 @@
+<!--
+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.
+-->
+
+<div id="nifi-registry-administration-perspective">
+    <mat-button-toggle-group name="nifi-registry-administration-perspective" fxLayout="row"
+                             fxLayoutAlign="space-between center" class="tab-toggle-group">
+        <mat-button-toggle
+                [disabled]="!nfRegistryService.currentUser.resourcePermissions.buckets.canRead"
+                [matTooltip]="'Manage NiFi Registry buckets.'"
+                [checked]="nfRegistryService.adminPerspective === 'workflow'" value="workflow"
+                class="uppercase"
+                (change)="navigateToAdminPerspective($event)"
+                i18n="Workflow administration tab|A description of the type of administration options available.@@nf-admin-workflow-tab-title">
+            Buckets
+        </mat-button-toggle>
+        <mat-button-toggle
+                [disabled]="nfRegistryService.currentUser.anonymous || !nfRegistryService.currentUser.resourcePermissions.tenants.canRead"
+                [matTooltip]="getUserTooltip()"
+                [checked]="nfRegistryService.adminPerspective === 'users'" value="users" class="uppercase"
+                (change)="navigateToAdminPerspective($event)"
+                i18n="Users administration tab|A description of the type of administration options available.@@nf-admin-users-tab-title">
+            Users
+        </mat-button-toggle>
+    </mat-button-toggle-group>
+</div>
+<router-outlet></router-outlet>

http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/6f26290d/nifi-registry-core/nifi-registry-web-ui/src/main/webapp/components/administration/nf-registry-administration.js
----------------------------------------------------------------------
diff --git a/nifi-registry-core/nifi-registry-web-ui/src/main/webapp/components/administration/nf-registry-administration.js b/nifi-registry-core/nifi-registry-web-ui/src/main/webapp/components/administration/nf-registry-administration.js
new file mode 100644
index 0000000..fa37658
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-web-ui/src/main/webapp/components/administration/nf-registry-administration.js
@@ -0,0 +1,98 @@
+/*
+ * 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.
+ */
+var ngCore = require('@angular/core');
+var NfRegistryService = require('nifi-registry/services/nf-registry.service.js');
+var nfRegistryAnimations = require('nifi-registry/nf-registry.animations.js');
+var ngRouter = require('@angular/router');
+
+/**
+ * NfRegistryAdministration constructor.
+ *
+ * @param nfRegistryService     The nf-registry.service module.
+ * @param router                The angular router module.
+ * @constructor
+ */
+function NfRegistryAdministration(nfRegistryService, router) {
+    //Services
+    this.router = router;
+    this.nfRegistryService = nfRegistryService;
+};
+
+NfRegistryAdministration.prototype = {
+    constructor: NfRegistryAdministration,
+
+    /**
+     * Initialize the component.
+     */
+    ngOnInit: function () {
+        var self = this;
+        this.nfRegistryService.perspective = 'administration';
+        this.nfRegistryService.setBreadcrumbState('in');
+    },
+
+    /**
+     * Destroy the component.
+     */
+    ngOnDestroy: function () {
+        this.nfRegistryService.perspective = '';
+        this.nfRegistryService.setBreadcrumbState('out');
+    },
+
+    /**
+     * Navigates to admin perspective.
+     *
+     * @param $event
+     */
+    navigateToAdminPerspective: function($event) {
+        this.router.navigateByUrl('nifi-registry/administration/' + $event.value);
+    },
+
+    /**
+     * Generate the user tab tooltip.
+     *
+     * @returns {*}
+     */
+    getUserTooltip: function() {
+        if(this.nfRegistryService.currentUser.anonymous) {
+            return 'Please configure NiFi Registry security to enable.';
+        }
+        else {
+            if(!this.nfRegistryService.currentUser.resourcePermissions.tenants.canRead) {
+                return 'You do not have permission. Please contact your System Administrator.'
+            } else {
+                return 'Manage NiFi Registry users and groups.'
+            }
+        }
+    }
+};
+
+NfRegistryAdministration.annotations = [
+    new ngCore.Component({
+        template: require('./nf-registry-administration.html!text'),
+        animations: [nfRegistryAnimations.slideInLeftAnimation],
+        host: {
+            '[@routeAnimation]': 'routeAnimation'
+        }
+    })
+];
+
+NfRegistryAdministration.parameters = [
+    NfRegistryService,
+    ngRouter.Router
+];
+
+module.exports = NfRegistryAdministration;

http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/6f26290d/nifi-registry-core/nifi-registry-web-ui/src/main/webapp/components/administration/nf-registry-administration.spec.js
----------------------------------------------------------------------
diff --git a/nifi-registry-core/nifi-registry-web-ui/src/main/webapp/components/administration/nf-registry-administration.spec.js b/nifi-registry-core/nifi-registry-web-ui/src/main/webapp/components/administration/nf-registry-administration.spec.js
new file mode 100644
index 0000000..fbf228a
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-web-ui/src/main/webapp/components/administration/nf-registry-administration.spec.js
@@ -0,0 +1,146 @@
+/*
+ * 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.
+ */
+
+var NfRegistryRoutes = require('nifi-registry/nf-registry.routes.js');
+var ngCoreTesting = require('@angular/core/testing');
+var ngCommon = require('@angular/common');
+var ngCommonHttp = require('@angular/common/http');
+var NfRegistryTokenInterceptor = require('nifi-registry/services/nf-registry.token.interceptor.js');
+var NfStorage = require('nifi-registry/services/nf-storage.service.js');
+var ngPlatformBrowser = require('@angular/platform-browser');
+var NfRegistry = require('nifi-registry/nf-registry.js');
+var NfRegistryApi = require('nifi-registry/services/nf-registry.api.js');
+var NfRegistryService = require('nifi-registry/services/nf-registry.service.js');
+var NfPageNotFoundComponent = require('nifi-registry/components/page-not-found/nf-registry-page-not-found.js');
+var NfRegistryExplorer = require('nifi-registry/components/explorer/nf-registry-explorer.js');
+var NfRegistryAdministration = require('nifi-registry/components/administration/nf-registry-administration.js');
+var NfRegistryUsersAdministration = require('nifi-registry/components/administration/users/nf-registry-users-administration.js');
+var NfRegistryAddUser = require('nifi-registry/components/administration/users/dialogs/add-user/nf-registry-add-user.js');
+var NfRegistryManageUser = require('nifi-registry/components/administration/users/sidenav/manage-user/nf-registry-manage-user.js');
+var NfRegistryManageGroup = require('nifi-registry/components/administration/users/sidenav/manage-group/nf-registry-manage-group.js');
+var NfRegistryManageBucket = require('nifi-registry/components/administration/workflow/sidenav/manage-bucket/nf-registry-manage-bucket.js');
+var NfRegistryWorkflowAdministration = require('nifi-registry/components/administration/workflow/nf-registry-workflow-administration.js');
+var NfRegistryGridListViewer = require('nifi-registry/components/explorer/grid-list/registry/nf-registry-grid-list-viewer.js');
+var NfRegistryBucketGridListViewer = require('nifi-registry/components/explorer/grid-list/registry/nf-registry-bucket-grid-list-viewer.js');
+var NfRegistryDropletGridListViewer = require('nifi-registry/components/explorer/grid-list/registry/nf-registry-droplet-grid-list-viewer.js');
+var fdsCore = require('@flow-design-system/core');
+var ngMoment = require('angular2-moment');
+var rxjs = require('rxjs/Rx');
+var NfLoginComponent = require('nifi-registry/components/login/nf-registry-login.js');
+var NfUserLoginComponent = require('nifi-registry/components/login/dialogs/nf-registry-user-login.js');
+
+describe('NfRegistryAdministration Component', function () {
+    var comp;
+    var fixture;
+    var de;
+    var el;
+    var nfRegistryService;
+    var nfRegistryApi;
+
+    beforeEach(function () {
+        ngCoreTesting.TestBed.configureTestingModule({
+            imports: [
+                ngMoment.MomentModule,
+                ngCommonHttp.HttpClientModule,
+                fdsCore,
+                NfRegistryRoutes
+            ],
+            declarations: [
+                NfRegistry,
+                NfRegistryExplorer,
+                NfRegistryAdministration,
+                NfRegistryUsersAdministration,
+                NfRegistryManageUser,
+                NfRegistryManageGroup,
+                NfRegistryManageBucket,
+                NfRegistryAddUser,
+                NfRegistryWorkflowAdministration,
+                NfRegistryGridListViewer,
+                NfRegistryBucketGridListViewer,
+                NfRegistryDropletGridListViewer,
+                NfPageNotFoundComponent,
+                NfLoginComponent,
+                NfUserLoginComponent
+            ],
+            providers: [
+                NfRegistryService,
+                NfRegistryApi,
+                NfStorage,
+                {
+                    provide: ngCommonHttp.HTTP_INTERCEPTORS,
+                    useClass: NfRegistryTokenInterceptor,
+                    multi: true
+                },
+                {
+                    provide: ngCommon.APP_BASE_HREF,
+                    useValue: '/'
+                }
+            ]
+        });
+
+        fixture = ngCoreTesting.TestBed.createComponent(NfRegistryAdministration);
+
+        // test instance
+        comp = fixture.componentInstance;
+
+        // from the root injector
+        nfRegistryService = ngCoreTesting.TestBed.get(NfRegistryService);
+        nfRegistryApi = ngCoreTesting.TestBed.get(NfRegistryApi);
+        de = fixture.debugElement.query(ngPlatformBrowser.By.css('#nifi-registry-administration-perspective'));
+        el = de.nativeElement;
+
+        // Spy
+        spyOn(nfRegistryApi, 'getDroplets').and.callFake(function () {
+        }).and.returnValue(rxjs.Observable.of([{
+            "identifier": "2e04b4fb-9513-47bb-aa74-1ae34616bfdc",
+            "name": "Flow #1",
+            "description": "This is flow #1",
+            "bucketIdentifier": "2f7f9e54-dc09-4ceb-aa58-9fe581319cdc",
+            "createdTimestamp": 1505931890999,
+            "modifiedTimestamp": 1505931890999,
+            "type": "FLOW",
+            "snapshotMetadata": null,
+            "link": {
+                "params": {
+                    "rel": "self"
+                },
+                "href": "flows/2e04b4fb-9513-47bb-aa74-1ae34616bfdc"
+            }
+        }]));
+    });
+
+    it('should have a defined component', function () {
+        fixture.detectChanges();
+
+        //assertions
+        expect(comp).toBeDefined();
+        expect(nfRegistryService.perspective).toBe('administration');
+        expect(nfRegistryService.breadCrumbState).toBe('in');
+        expect(de).toBeDefined();
+    });
+
+    it('should destroy the component', function () {
+        fixture.detectChanges();
+
+        // The function to test
+        comp.ngOnDestroy();
+
+        //assertions
+        expect(nfRegistryService.perspective).toBe('');
+        expect(nfRegistryService.breadCrumbState).toBe('out');
+    });
+});
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/6f26290d/nifi-registry-core/nifi-registry-web-ui/src/main/webapp/components/administration/users/dialogs/add-user-to-groups/nf-registry-add-user-to-groups.html
----------------------------------------------------------------------
diff --git a/nifi-registry-core/nifi-registry-web-ui/src/main/webapp/components/administration/users/dialogs/add-user-to-groups/nf-registry-add-user-to-groups.html b/nifi-registry-core/nifi-registry-web-ui/src/main/webapp/components/administration/users/dialogs/add-user-to-groups/nf-registry-add-user-to-groups.html
new file mode 100644
index 0000000..70e59ed
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-web-ui/src/main/webapp/components/administration/users/dialogs/add-user-to-groups/nf-registry-add-user-to-groups.html
@@ -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.
+-->
+
+<div id="nifi-registry-admin-add-selected-users-to-group-dialog">
+    <div class="pad-bottom-md" fxLayout="row" fxLayoutAlign="space-between center">
+        <span class="md-card-title">Add user to groups</span>
+        <button mat-icon-button (click)="cancel()">
+            <mat-icon color="primary">close</mat-icon>
+        </button>
+    </div>
+    <div *ngIf="filteredUserGroups.length > 0" class="pad-bottom-md">
+        <div id="nifi-registry-users-administration-list-container-column-header" class="td-data-table">
+            <div class="td-data-table-column" (click)="sortUserGroups(column)"
+                 *ngFor="let column of userGroupsColumns"
+                 fxFlex="{{column.width}}">
+                {{column.label}}
+                <i *ngIf="column.active && column.sortable && column.sortOrder === 'ASC'" class="fa fa-caret-up"
+                   aria-hidden="true"></i>
+                <i *ngIf="column.active && column.sortable && column.sortOrder === 'DESC'" class="fa fa-caret-down"
+                   aria-hidden="true"></i>
+            </div>
+            <div class="td-data-table-column">
+                <mat-checkbox [(ngModel)]="allGroupsSelected"
+                              (checked)="allGroupsSelected"
+                              (change)="toggleUserGroupsSelectAll()"></mat-checkbox>
+            </div>
+        </div>
+        <div id="nifi-registry-add-selected-users-to-group-list-container">
+            <div fxLayout="row" fxLayoutAlign="space-between center"
+                 class="td-data-table-row"
+                 [ngClass]="{'selected' : group.checked}"
+                 *ngFor="let group of filteredUserGroups"
+                 (click)="group.checked = !group.checked;determineAllUserGroupsSelectedState();">
+                <div class="td-data-table-cell" *ngFor="let column of userGroupsColumns"
+                     fxFlex="{{column.width}}">
+                    <div class="ellipsis" matTooltip="{{column.format ? column.format(group[column.name]) : group[column.name]}}">
+                        <i class="fa fa-users push-right-sm" aria-hidden="true"></i>{{column.format ?
+                        column.format(group[column.name]) : group[column.name]}}
+                    </div>
+                </div>
+                <div class="td-data-table-cell">
+                    <mat-checkbox [(ngModel)]="group.checked"
+                                  [checked]="group.checked"
+                                  (change)="determineAllUserGroupsSelectedState()"
+                                  (click)="group.checked = !group.checked;determineAllUserGroupsSelectedState()">
+                    </mat-checkbox>
+                </div>
+            </div>
+        </div>
+    </div>
+    <div class="mat-padding push-bottom-md" *ngIf="filteredUserGroups.length === 0" layout="row"
+         layout-align="center center">
+        <h3>User belongs to all groups.</h3>
+    </div>
+    <div fxLayout="row">
+        <span fxFlex></span>
+        <button (click)="cancel()" color="fds-regular" mat-raised-button
+                i18n="Cancel addition of selected users to group|A button for cancelling the addition of selected users to a group in the registry.@@nf-admin-workflow-cancel-add-selected-users-to-group-button">
+            Cancel
+        </button>
+        <button [disabled]="isAddToSelectedGroupsDisabled" class="push-left-sm" (click)="addToSelectedGroups()"
+                color="fds-primary" mat-raised-button
+                i18n="Add selected users to group button|A button for adding users to an existing group in the registry.@@nf-admin-workflow-add-selected-users-to-group-button">
+            Add
+        </button>
+    </div>
+</div>

http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/6f26290d/nifi-registry-core/nifi-registry-web-ui/src/main/webapp/components/administration/users/dialogs/add-user-to-groups/nf-registry-add-user-to-groups.js
----------------------------------------------------------------------
diff --git a/nifi-registry-core/nifi-registry-web-ui/src/main/webapp/components/administration/users/dialogs/add-user-to-groups/nf-registry-add-user-to-groups.js b/nifi-registry-core/nifi-registry-web-ui/src/main/webapp/components/administration/users/dialogs/add-user-to-groups/nf-registry-add-user-to-groups.js
new file mode 100644
index 0000000..e808803
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-web-ui/src/main/webapp/components/administration/users/dialogs/add-user-to-groups/nf-registry-add-user-to-groups.js
@@ -0,0 +1,254 @@
+/*
+ * 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.
+ */
+
+var covalentCore = require('@covalent/core');
+var NfRegistryApi = require('nifi-registry/services/nf-registry.api.js');
+var ngCore = require('@angular/core');
+var fdsSnackBarsModule = require('@flow-design-system/snackbars');
+var NfRegistryService = require('nifi-registry/services/nf-registry.service.js');
+var ngMaterial = require('@angular/material');
+var $ = require('jquery');
+
+/**
+ * NfRegistryAddUserToGroups constructor.
+ *
+ * @param nfRegistryApi         The api service.
+ * @param tdDataTableService    The covalent data table service module.
+ * @param nfRegistryService     The nf-registry.service module.
+ * @param matDialogRef          The angular material dialog ref.
+ * @param fdsSnackBarService    The FDS snack bar service module.
+ * @param data                  The data passed into this component.
+ * @constructor
+ */
+function NfRegistryAddUserToGroups(nfRegistryApi, tdDataTableService, nfRegistryService, matDialogRef, fdsSnackBarService, data) {
+    //Services
+    this.dataTableService = tdDataTableService;
+    this.snackBarService = fdsSnackBarService;
+    this.nfRegistryService = nfRegistryService;
+    this.nfRegistryApi = nfRegistryApi;
+    this.dialogRef = matDialogRef;
+    this.data = data;
+
+    // local state
+    //make an independent copy of the groups for sorting and selecting within the scope of this component
+    this.groups = $.extend(true, [], this.nfRegistryService.groups);
+    this.filteredUserGroups = [];
+    this.isAddToSelectedGroupsDisabled = true;
+    this.userGroupsSearchTerms = [];
+    this.allGroupsSelected = false;
+    this.userGroupsColumns = [
+        {
+            name: 'identity',
+            label: 'Display Name',
+            sortable: true,
+            tooltip: 'Group name.',
+            width: 100
+        }
+    ];
+};
+
+NfRegistryAddUserToGroups.prototype = {
+    constructor: NfRegistryAddUserToGroups,
+
+    /**
+     * Initialize the component.
+     */
+    ngOnInit: function () {
+        var self = this;
+
+        // filter out any groups that
+        // 1) that are not configurable
+        self.groups = self.groups.filter(function (group) {
+            return (group.configurable) ? true : false
+        });
+        // 2) the user already belongs to
+        this.data.user.userGroups.forEach(function (userGroup) {
+            self.groups = self.groups.filter(function (group) {
+                return (group.identifier !== userGroup.identifier) ? true : false
+            });
+        });
+
+        this.filterGroups();
+        this.deselectAllUserGroups();
+        this.determineAllUserGroupsSelectedState();
+    },
+
+    /**
+     * Filter groups.
+     *
+     * @param {string} [sortBy]       The column name to sort `userGroupsColumns` by.
+     * @param {string} [sortOrder]    The order. Either 'ASC' or 'DES'
+     */
+    filterGroups: function (sortBy, sortOrder) {
+        // if `sortOrder` is `undefined` then use 'ASC'
+        if (sortOrder === undefined) {
+            sortOrder = 'ASC'
+        }
+        // if `sortBy` is `undefined` then find the first sortable column in `dropletColumns`
+        if (sortBy === undefined) {
+            var arrayLength = this.userGroupsColumns.length;
+            for (var i = 0; i < arrayLength; i++) {
+                if (this.userGroupsColumns[i].sortable === true) {
+                    sortBy = this.userGroupsColumns[i].name;
+                    //only one column can be actively sorted so we reset all to inactive
+                    this.userGroupsColumns.forEach(function (c) {
+                        c.active = false;
+                    });
+                    //and set this column as the actively sorted column
+                    this.userGroupsColumns[i].active = true;
+                    this.userGroupsColumns[i].sortOrder = sortOrder;
+                    break;
+                }
+            }
+        }
+
+        var newUserGroupsData = this.groups;
+
+        for (var i = 0; i < this.userGroupsSearchTerms.length; i++) {
+            newUserGroupsData = this.dataTableService.filterData(newUserGroupsData, this.userGroupsSearchTerms[i], true);
+        }
+
+        newUserGroupsData = this.dataTableService.sortData(newUserGroupsData, sortBy, sortOrder);
+        this.filteredUserGroups = newUserGroupsData;
+    },
+
+    /**
+     * Sort `filteredUserGroups` by `column`.
+     *
+     * @param column    The column to sort by.
+     */
+    sortUserGroups: function (column) {
+        if (column.sortable) {
+            var sortBy = column.name;
+            var sortOrder = column.sortOrder = (column.sortOrder === 'ASC') ? 'DESC' : 'ASC';
+            this.filterGroups(sortBy, sortOrder);
+        }
+    },
+
+    /**
+     * Checks the `allGroupsSelected` property state and either selects
+     * or deselects each of the `filteredUserGroups`.
+     */
+    toggleUserGroupsSelectAll: function () {
+        if (this.allGroupsSelected) {
+            this.selectAllUserGroups();
+        } else {
+            this.deselectAllUserGroups();
+        }
+    },
+
+    /**
+     * Sets the `checked` property of each of the `filteredUserGroups` to true
+     * and sets the `isAddToSelectedGroupsDisabled` and the `allGroupsSelected`
+     * properties accordingly.
+     */
+    selectAllUserGroups: function () {
+        this.filteredUserGroups.forEach(function (c) {
+            c.checked = true;
+        });
+        this.isAddToSelectedGroupsDisabled = false;
+        this.allGroupsSelected = true;
+    },
+
+    /**
+     * Sets the `checked` property of each group to false
+     * and sets the `isAddToSelectedGroupsDisabled` and the `allGroupsSelected`
+     * properties accordingly.
+     */
+    deselectAllUserGroups: function () {
+        this.filteredUserGroups.forEach(function (c) {
+            c.checked = false;
+        });
+        this.isAddToSelectedGroupsDisabled = true;
+        this.allGroupsSelected = false;
+    },
+
+    /**
+     * Checks of each of the `filteredUserGroups`'s checked property state
+     * and sets the `allBucketsSelected` and `isAddToSelectedGroupsDisabled`
+     * property accordingly.
+     */
+    determineAllUserGroupsSelectedState: function () {
+        var selected = 0;
+        var allSelected = true;
+        this.filteredUserGroups.forEach(function (c) {
+            if (c.checked) {
+                selected++;
+            }
+            if (c.checked === undefined || c.checked === false) {
+                allSelected = false;
+            }
+        });
+
+        if (selected > 0) {
+            this.isAddToSelectedGroupsDisabled = false;
+        } else {
+            this.isAddToSelectedGroupsDisabled = true;
+        }
+
+        this.allGroupsSelected = allSelected;
+    },
+
+    /**
+     * Adds users to each of the selected groups.
+     */
+    addToSelectedGroups: function () {
+        var self = this;
+        var selectedGroups = this.filteredUserGroups.filter(function (filteredUserGroup) {
+            return filteredUserGroup.checked;
+        });
+        selectedGroups.forEach(function (selectedGroup) {
+            selectedGroup.users.push(self.data.user);
+            self.nfRegistryApi.updateUserGroup(selectedGroup.identifier, selectedGroup.identity, selectedGroup.users).subscribe(function (group) {
+                self.dialogRef.close();
+                var snackBarRef = self.snackBarService.openCoaster({
+                    title: 'Success',
+                    message: 'User has been added to the ' + group.identity + ' group.',
+                    verticalPosition: 'bottom',
+                    horizontalPosition: 'right',
+                    icon: 'fa fa-check-circle-o',
+                    color: '#1EB475',
+                    duration: 3000
+                });
+            });
+        });
+    },
+
+    /**
+     * Cancel adding selected users to groups and close the dialog.
+     */
+    cancel: function () {
+        this.dialogRef.close();
+    }
+};
+
+NfRegistryAddUserToGroups.annotations = [
+    new ngCore.Component({
+        template: require('./nf-registry-add-user-to-groups.html!text')
+    })
+];
+
+NfRegistryAddUserToGroups.parameters = [
+    NfRegistryApi,
+    covalentCore.TdDataTableService,
+    NfRegistryService,
+    ngMaterial.MatDialogRef,
+    fdsSnackBarsModule.FdsSnackBarService,
+    ngMaterial.MAT_DIALOG_DATA
+];
+
+module.exports = NfRegistryAddUserToGroups;

http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/6f26290d/nifi-registry-core/nifi-registry-web-ui/src/main/webapp/components/administration/users/dialogs/add-user-to-groups/nf-registry-add-user-to-groups.spec.js
----------------------------------------------------------------------
diff --git a/nifi-registry-core/nifi-registry-web-ui/src/main/webapp/components/administration/users/dialogs/add-user-to-groups/nf-registry-add-user-to-groups.spec.js b/nifi-registry-core/nifi-registry-web-ui/src/main/webapp/components/administration/users/dialogs/add-user-to-groups/nf-registry-add-user-to-groups.spec.js
new file mode 100644
index 0000000..1e0140d
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-web-ui/src/main/webapp/components/administration/users/dialogs/add-user-to-groups/nf-registry-add-user-to-groups.spec.js
@@ -0,0 +1,172 @@
+/*
+ * 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.
+ */
+
+var NfRegistryApi = require('nifi-registry/services/nf-registry.api.js');
+var NfRegistryService = require('nifi-registry/services/nf-registry.service.js');
+var NfRegistryAddUserToGroups = require('nifi-registry/components/administration/users/dialogs/add-user-to-groups/nf-registry-add-user-to-groups.js');
+var rxjs = require('rxjs/Rx');
+var covalentCore = require('@covalent/core');
+var fdsSnackBarsModule = require('@flow-design-system/snackbars');
+
+describe('NfRegistryAddUserToGroups Component isolated unit tests', function () {
+    var comp;
+    var nfRegistryService;
+    var nfRegistryApi;
+    var snackBarService;
+    var dataTableService;
+
+    beforeEach(function () {
+        nfRegistryService = new NfRegistryService();
+        // setup the nfRegistryService
+        nfRegistryService.user = {identifier: 3, identity: 'User 3', userGroups: []};
+        nfRegistryService.groups = [{identifier: 1, identity: 'Group 1', configurable: true, checked: true, users: []}];
+
+        nfRegistryApi = new NfRegistryApi();
+        snackBarService = new fdsSnackBarsModule.FdsSnackBarService();
+        dataTableService = new covalentCore.TdDataTableService();
+        comp = new NfRegistryAddUserToGroups(nfRegistryApi, dataTableService, nfRegistryService, {
+            close: function () {
+            }
+        }, snackBarService, {user: nfRegistryService.user});
+
+        // Spy
+        spyOn(nfRegistryApi, 'getUserGroup').and.callFake(function () {
+        }).and.returnValue(rxjs.Observable.of({identifier: 1, identity: 'Group 1'}));
+        spyOn(nfRegistryApi, 'updateUserGroup').and.callFake(function () {
+        }).and.returnValue(rxjs.Observable.of({identifier: 1, identity: 'Group 1'}));
+        spyOn(comp.dialogRef, 'close');
+        spyOn(comp.snackBarService, 'openCoaster');
+        spyOn(comp, 'filterGroups').and.callThrough();
+
+        // initialize the component
+        comp.ngOnInit();
+
+        //assertions
+        expect(comp.filterGroups).toHaveBeenCalled();
+        expect(comp.filteredUserGroups[0].identity).toEqual('Group 1');
+        expect(comp.filteredUserGroups.length).toBe(1);
+        expect(comp).toBeDefined();
+    });
+
+    it('should make a call to the api to add user to selected groups', function () {
+        // select a group
+        comp.filteredUserGroups[0].checked = true;
+
+        // the function to test
+        comp.addToSelectedGroups();
+
+        //assertions
+        expect(comp.dialogRef.close).toHaveBeenCalled();
+        expect(comp.snackBarService.openCoaster).toHaveBeenCalled();
+    });
+
+    it('should determine if all groups are selected', function () {
+        // select a group
+        comp.filteredUserGroups[0].checked = true;
+
+        // the function to test
+        comp.determineAllUserGroupsSelectedState();
+
+        //assertions
+        expect(comp.allGroupsSelected).toBe(true);
+        expect(comp.isAddToSelectedGroupsDisabled).toBe(false);
+    });
+
+    it('should determine if all groups are not selected', function () {
+        // select a group
+        comp.filteredUserGroups[0].checked = false;
+
+        // the function to test
+        comp.determineAllUserGroupsSelectedState();
+
+        //assertions
+        expect(comp.allGroupsSelected).toBe(false);
+        expect(comp.isAddToSelectedGroupsDisabled).toBe(true);
+    });
+
+    it('should select all groups.', function () {
+        // The function to test
+        comp.selectAllUserGroups();
+
+        //assertions
+        expect(comp.filteredUserGroups[0].checked).toBe(true);
+        expect(comp.isAddToSelectedGroupsDisabled).toBe(false);
+        expect(comp.allGroupsSelected).toBe(true);
+    });
+
+    it('should deselect all groups.', function () {
+        // select a group
+        comp.filteredUserGroups[0].checked = true;
+
+        // The function to test
+        comp.deselectAllUserGroups();
+
+        //assertions
+        expect(comp.filteredUserGroups[0].checked).toBe(false);
+        expect(comp.isAddToSelectedGroupsDisabled).toBe(true);
+        expect(comp.allGroupsSelected).toBe(false);
+    });
+
+    it('should toggle all groups `checked` properties to true.', function () {
+        //Spy
+        spyOn(comp, 'selectAllUserGroups').and.callFake(function () {
+        });
+
+        comp.allGroupsSelected = true;
+
+        // The function to test
+        comp.toggleUserGroupsSelectAll();
+
+        //assertions
+        expect(comp.selectAllUserGroups).toHaveBeenCalled();
+    });
+
+    it('should toggle all groups `checked` properties to false.', function () {
+        //Spy
+        spyOn(comp, 'deselectAllUserGroups').and.callFake(function () {
+        });
+
+        comp.allGroupsSelected = false;
+
+        // The function to test
+        comp.toggleUserGroupsSelectAll();
+
+        //assertions
+        expect(comp.deselectAllUserGroups).toHaveBeenCalled();
+    });
+
+    it('should sort `groups` by `column`', function () {
+        // object to be updated by the test
+        var column = {name: 'name', label: 'Group Name', sortable: true};
+
+        // The function to test
+        comp.sortUserGroups(column);
+
+        //assertions
+        var filterGroupsCall = comp.filterGroups.calls.mostRecent();
+        expect(filterGroupsCall.args[0]).toBe('name');
+        expect(filterGroupsCall.args[1]).toBe('ASC');
+    });
+
+    it('should cancel the addition of the user to any group', function () {
+        // the function to test
+        comp.cancel();
+
+        //assertions
+        expect(comp.dialogRef.close).toHaveBeenCalled();
+    });
+});
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/6f26290d/nifi-registry-core/nifi-registry-web-ui/src/main/webapp/components/administration/users/dialogs/add-user/nf-registry-add-user.html
----------------------------------------------------------------------
diff --git a/nifi-registry-core/nifi-registry-web-ui/src/main/webapp/components/administration/users/dialogs/add-user/nf-registry-add-user.html b/nifi-registry-core/nifi-registry-web-ui/src/main/webapp/components/administration/users/dialogs/add-user/nf-registry-add-user.html
new file mode 100644
index 0000000..d7423ac
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-web-ui/src/main/webapp/components/administration/users/dialogs/add-user/nf-registry-add-user.html
@@ -0,0 +1,46 @@
+<!--
+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.
+-->
+
+<div id="nifi-registry-admin-add-user-dialog">
+    <div class="pad-bottom-md" fxLayout="row" fxLayoutAlign="space-between center">
+        <span class="md-card-title">Add User</span>
+        <button mat-icon-button (click)="cancel()">
+            <mat-icon color="primary">close</mat-icon>
+        </button>
+    </div>
+    <div fxLayout="column" fxLayoutAlign="space-between start" class="pad-bottom-md">
+        <div class="pad-bottom-md fill-available-width">
+            <mat-input-container floatPlaceholder="always" fxFlex>
+                <input #newUserInput matInput floatPlaceholder="always" placeholder="Identity/Username">
+            </mat-input-container>
+        </div>
+        <mat-checkbox [(ngModel)]="keepDialogOpen">
+            Keep this dialog open after adding user
+        </mat-checkbox>
+    </div>
+    <div fxLayout="row">
+        <span fxFlex></span>
+        <button (click)="cancel()" color="fds-regular" mat-raised-button
+                i18n="Cancel creation of new user|A button for cancelling the creation of a new user in the registry.@@nf-admin-workflow-create-bucket-button">
+            Cancel
+        </button>
+        <button [disabled]="newUserInput.value.length === 0" class="push-left-sm" id="add-new-user-button" (click)="addUser(newUserInput)" color="fds-primary" mat-raised-button
+                i18n="Add new user button|A button for adding a new user in the registry.@@nf-admin-workflow-add-user-button">
+            Add
+        </button>
+    </div>
+</div>

http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/6f26290d/nifi-registry-core/nifi-registry-web-ui/src/main/webapp/components/administration/users/dialogs/add-user/nf-registry-add-user.js
----------------------------------------------------------------------
diff --git a/nifi-registry-core/nifi-registry-web-ui/src/main/webapp/components/administration/users/dialogs/add-user/nf-registry-add-user.js b/nifi-registry-core/nifi-registry-web-ui/src/main/webapp/components/administration/users/dialogs/add-user/nf-registry-add-user.js
new file mode 100644
index 0000000..586b7f7
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-web-ui/src/main/webapp/components/administration/users/dialogs/add-user/nf-registry-add-user.js
@@ -0,0 +1,107 @@
+/*
+ * 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.
+ */
+
+var ngCore = require('@angular/core');
+var NfRegistryService = require('nifi-registry/services/nf-registry.service.js');
+var NfRegistryApi = require('nifi-registry/services/nf-registry.api.js');
+var ngMaterial = require('@angular/material');
+var fdsSnackBarsModule = require('@flow-design-system/snackbars');
+
+/**
+ * NfRegistryAddUser constructor.
+ *
+ * @param nfRegistryApi         The api service.
+ * @param nfRegistryService     The nf-registry.service module.
+ * @param fdsSnackBarService    The FDS snack bar service module.
+ * @param matDialogRef          The angular material dialog ref.
+ * @constructor
+ */
+function NfRegistryAddUser(nfRegistryApi, nfRegistryService, fdsSnackBarService, matDialogRef) {
+    // Services
+    this.snackBarService = fdsSnackBarService;
+    this.nfRegistryService = nfRegistryService;
+    this.nfRegistryApi = nfRegistryApi;
+    this.dialogRef = matDialogRef;
+    // local state
+    this.keepDialogOpen = false;
+};
+
+NfRegistryAddUser.prototype = {
+    constructor: NfRegistryAddUser,
+
+    /**
+     * Create a new user.
+     *
+     * @param addUserInput     The addUserInput element.
+     */
+    addUser: function (addUserInput) {
+        var self = this;
+        this.nfRegistryApi.addUser(addUserInput.value).subscribe(function (user) {
+            if (!user.error) {
+                self.nfRegistryService.users.push(user);
+                self.nfRegistryService.allUsersAndGroupsSelected = false;
+                self.nfRegistryService.filterUsersAndGroups();
+                if (self.keepDialogOpen !== true) {
+                    self.dialogRef.close();
+                }
+                self.snackBarService.openCoaster({
+                    title: 'Success',
+                    message: 'User has been added.',
+                    verticalPosition: 'bottom',
+                    horizontalPosition: 'right',
+                    icon: 'fa fa-check-circle-o',
+                    color: '#1EB475',
+                    duration: 3000
+                });
+            } else {
+                self.dialogRef.close();
+            }
+        });
+    },
+
+    /**
+     * Cancel creation of a new bucket and close dialog.
+     */
+    cancel: function () {
+        this.dialogRef.close();
+    },
+
+    /**
+     * Focus the new user input.
+     */
+    ngAfterViewChecked: function () {
+        this.newUserInput.nativeElement.focus();
+    }
+};
+
+NfRegistryAddUser.annotations = [
+    new ngCore.Component({
+        template: require('./nf-registry-add-user.html!text'),
+        queries: {
+            newUserInput: new ngCore.ViewChild('newUserInput')
+        }
+    })
+];
+
+NfRegistryAddUser.parameters = [
+    NfRegistryApi,
+    NfRegistryService,
+    fdsSnackBarsModule.FdsSnackBarService,
+    ngMaterial.MatDialogRef
+];
+
+module.exports = NfRegistryAddUser;

http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/6f26290d/nifi-registry-core/nifi-registry-web-ui/src/main/webapp/components/administration/users/dialogs/add-user/nf-registry-add-user.spec.js
----------------------------------------------------------------------
diff --git a/nifi-registry-core/nifi-registry-web-ui/src/main/webapp/components/administration/users/dialogs/add-user/nf-registry-add-user.spec.js b/nifi-registry-core/nifi-registry-web-ui/src/main/webapp/components/administration/users/dialogs/add-user/nf-registry-add-user.spec.js
new file mode 100644
index 0000000..17f12f5
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-web-ui/src/main/webapp/components/administration/users/dialogs/add-user/nf-registry-add-user.spec.js
@@ -0,0 +1,86 @@
+/*
+ * 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.
+ */
+
+var NfRegistryApi = require('nifi-registry/services/nf-registry.api.js');
+var NfRegistryService = require('nifi-registry/services/nf-registry.service.js');
+var NfRegistryAddUser = require('nifi-registry/components/administration/users/dialogs/add-user/nf-registry-add-user.js');
+var rxjs = require('rxjs/Rx');
+
+describe('NfRegistryAddUser Component isolated unit tests', function () {
+    var comp;
+    var nfRegistryService;
+    var nfRegistryApi;
+
+    beforeEach(function () {
+        nfRegistryService = new NfRegistryService();
+        nfRegistryApi = new NfRegistryApi();
+        comp = new NfRegistryAddUser(nfRegistryApi,
+            nfRegistryService,
+            {
+                openCoaster: function () {
+                }
+            },
+            {
+                close: function () {
+                }
+            });
+
+        // Spy
+        spyOn(nfRegistryApi, 'addUser').and.callFake(function () {
+        }).and.returnValue(rxjs.Observable.of([{
+            'identifier': '2e04b4fb-9513-47bb-aa74-1ae34616bfdc',
+            'identity': 'New User #1'
+        }]));
+        spyOn(nfRegistryService, 'filterUsersAndGroups');
+        spyOn(comp.dialogRef, 'close');
+    });
+
+    it('should make a call to the api to create a new user and close the dialog', function () {
+        // the function to test
+        comp.addUser({value: 'New User #1'});
+
+        //assertions
+        expect(comp).toBeDefined();
+        expect(nfRegistryService.users.length).toBe(1);
+        expect(nfRegistryService.allUsersAndGroupsSelected).toBe(false);
+        expect(nfRegistryService.filterUsersAndGroups).toHaveBeenCalled();
+        expect(comp.dialogRef.close).toHaveBeenCalled();
+    });
+
+    it('should make a call to the api to create a new user and keep the dialog open', function () {
+        // setup the component
+        comp.keepDialogOpen = true;
+
+        // the function to test
+        comp.addUser({value: 'New User #1'});
+
+        //assertions
+        expect(comp).toBeDefined();
+        expect(nfRegistryService.users.length).toBe(1);
+        expect(nfRegistryService.allUsersAndGroupsSelected).toBe(false);
+        expect(nfRegistryService.filterUsersAndGroups).toHaveBeenCalled();
+        expect(comp.dialogRef.close.calls.count()).toEqual(0);
+    });
+
+    it('should cancel the creation of a new user', function () {
+        // the function to test
+        comp.cancel();
+
+        //assertions
+        expect(comp.dialogRef.close).toHaveBeenCalled();
+    });
+});
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/6f26290d/nifi-registry-core/nifi-registry-web-ui/src/main/webapp/components/administration/users/dialogs/add-users-to-group/nf-registry-add-users-to-group.html
----------------------------------------------------------------------
diff --git a/nifi-registry-core/nifi-registry-web-ui/src/main/webapp/components/administration/users/dialogs/add-users-to-group/nf-registry-add-users-to-group.html b/nifi-registry-core/nifi-registry-web-ui/src/main/webapp/components/administration/users/dialogs/add-users-to-group/nf-registry-add-users-to-group.html
new file mode 100644
index 0000000..6f8670c
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-web-ui/src/main/webapp/components/administration/users/dialogs/add-users-to-group/nf-registry-add-users-to-group.html
@@ -0,0 +1,80 @@
+<!--
+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.
+-->
+
+<div id="nifi-registry-admin-add-selected-users-to-group-dialog">
+    <div class="pad-bottom-md" fxLayout="row" fxLayoutAlign="space-between center">
+        <span class="md-card-title">Add users to group</span>
+        <button mat-icon-button (click)="cancel()">
+            <mat-icon color="primary">close</mat-icon>
+        </button>
+    </div>
+    <div *ngIf="filteredUsers.length > 0" class="pad-bottom-md">
+        <div id="nifi-registry-users-administration-list-container-column-header" class="td-data-table">
+            <div class="td-data-table-column" (click)="sortUsers(column)"
+                 *ngFor="let column of usersColumns"
+                 fxFlex="{{column.width}}">
+                {{column.label}}
+                <i *ngIf="column.active && column.sortable && column.sortOrder === 'ASC'" class="fa fa-caret-up"
+                   aria-hidden="true"></i>
+                <i *ngIf="column.active && column.sortable && column.sortOrder === 'DESC'" class="fa fa-caret-down"
+                   aria-hidden="true"></i>
+            </div>
+            <div class="td-data-table-column">
+                <mat-checkbox [(ngModel)]="allUsersSelected"
+                              (checked)="allUsersSelected"
+                              (change)="toggleUsersSelectAll()"></mat-checkbox>
+            </div>
+        </div>
+        <div id="nifi-registry-add-selected-users-to-group-list-container">
+            <div fxLayout="row" fxLayoutAlign="space-between center"
+                 class="td-data-table-row"
+                 [ngClass]="{'selected' : user.checked}"
+                 *ngFor="let user of filteredUsers"
+                 (click)="user.checked = !user.checked;determineAllUsersSelectedState();">
+                <div class="td-data-table-cell" *ngFor="let column of usersColumns"
+                     fxFlex="{{column.width}}">
+                    <div class="ellipsis" matTooltip="{{column.format ? column.format(user[column.name]) : user[column.name]}}">
+                        {{column.format ? column.format(user[column.name]) : user[column.name]}}
+                    </div>
+                </div>
+                <div class="td-data-table-cell">
+                    <mat-checkbox [(ngModel)]="user.checked"
+                                  [checked]="user.checked"
+                                  (change)="determineAllUsersSelectedState()"
+                                  (click)="user.checked = !user.checked;determineAllUsersSelectedState()">
+                    </mat-checkbox>
+                </div>
+            </div>
+        </div>
+    </div>
+    <div class="mat-padding push-bottom-md" *ngIf="filteredUsers.length === 0" layout="row"
+         layout-align="center center">
+        <h3>All users belong to this group.</h3>
+    </div>
+    <div fxLayout="row">
+        <span fxFlex></span>
+        <button (click)="cancel()" color="fds-regular" mat-raised-button
+                i18n="Cancel addition of selected users to group|A button for cancelling the addition of selected users to a group in the registry.@@nf-admin-workflow-cancel-add-selected-users-to-group-button">
+            Cancel
+        </button>
+        <button [disabled]="isAddSelectedUsersToGroupDisabled" class="push-left-sm" (click)="addSelectedUsersToGroup()"
+                color="fds-primary" mat-raised-button
+                i18n="Add selected users to group button|A button for adding users to an existing group in the registry.@@nf-admin-workflow-add-selected-users-to-group-button">
+            Add
+        </button>
+    </div>
+</div>

http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/6f26290d/nifi-registry-core/nifi-registry-web-ui/src/main/webapp/components/administration/users/dialogs/add-users-to-group/nf-registry-add-users-to-group.js
----------------------------------------------------------------------
diff --git a/nifi-registry-core/nifi-registry-web-ui/src/main/webapp/components/administration/users/dialogs/add-users-to-group/nf-registry-add-users-to-group.js b/nifi-registry-core/nifi-registry-web-ui/src/main/webapp/components/administration/users/dialogs/add-users-to-group/nf-registry-add-users-to-group.js
new file mode 100644
index 0000000..fff468b
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-web-ui/src/main/webapp/components/administration/users/dialogs/add-users-to-group/nf-registry-add-users-to-group.js
@@ -0,0 +1,247 @@
+/*
+ * 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.
+ */
+
+var covalentCore = require('@covalent/core');
+var NfRegistryApi = require('nifi-registry/services/nf-registry.api.js');
+var ngCore = require('@angular/core');
+var fdsSnackBarsModule = require('@flow-design-system/snackbars');
+var NfRegistryService = require('nifi-registry/services/nf-registry.service.js');
+var ngMaterial = require('@angular/material');
+var $ = require('jquery');
+
+/**
+ * NfRegistryAddUsersToGroup constructor.
+ *
+ * @param nfRegistryApi         The api service.
+ * @param tdDataTableService    The covalent data table service module.
+ * @param nfRegistryService     The nf-registry.service module.
+ * @param matDialogRef          The angular material dialog ref.
+ * @param fdsSnackBarService    The FDS snack bar service module.
+ * @param data                  The data passed into this component.
+ * @constructor
+ */
+function NfRegistryAddUsersToGroup(nfRegistryApi, tdDataTableService, nfRegistryService, matDialogRef, fdsSnackBarService, data) {
+    //  Services
+    this.dataTableService = tdDataTableService;
+    this.snackBarService = fdsSnackBarService;
+    this.nfRegistryService = nfRegistryService;
+    this.nfRegistryApi = nfRegistryApi;
+    this.dialogRef = matDialogRef;
+    this.data = data;
+
+    // local state
+    //make an independent copy of the users for sorting and selecting within the scope of this component
+    this.users = $.extend(true, [], this.nfRegistryService.users);
+    this.filteredUsers = [];
+    this.isAddSelectedUsersToGroupDisabled = true;
+    this.usersSearchTerms = [];
+    this.allUsersSelected = false;
+    this.usersColumns = [
+        {
+            name: 'identity',
+            label: 'Display Name',
+            sortable: true,
+            tooltip: 'Group name.',
+            width: 100
+        }
+    ];
+};
+
+NfRegistryAddUsersToGroup.prototype = {
+    constructor: NfRegistryAddUsersToGroup,
+
+    /**
+     * Initialize the component.
+     */
+    ngOnInit: function () {
+        var self = this;
+
+        this.data.group.users.forEach(function (groupUser) {
+            self.users = self.users.filter(function (user) {
+                return (user.identifier !== groupUser.identifier) ? true : false
+            });
+        });
+
+        this.filterUsers();
+        this.deselectAllUsers();
+        this.determineAllUsersSelectedState();
+    },
+
+    /**
+     * Filter users.
+     *
+     * @param {string} [sortBy]       The column name to sort `usersColumns` by.
+     * @param {string} [sortOrder]    The order. Either 'ASC' or 'DES'
+     */
+    filterUsers: function (sortBy, sortOrder) {
+        // if `sortOrder` is `undefined` then use 'ASC'
+        if (sortOrder === undefined) {
+            sortOrder = 'ASC'
+        }
+        // if `sortBy` is `undefined` then find the first sortable column in `dropletColumns`
+        if (sortBy === undefined) {
+            var arrayLength = this.usersColumns.length;
+            for (var i = 0; i < arrayLength; i++) {
+                if (this.usersColumns[i].sortable === true) {
+                    sortBy = this.usersColumns[i].name;
+                    //only one column can be actively sorted so we reset all to inactive
+                    this.usersColumns.forEach(function (c) {
+                        c.active = false;
+                    });
+                    //and set this column as the actively sorted column
+                    this.usersColumns[i].active = true;
+                    this.usersColumns[i].sortOrder = sortOrder;
+                    break;
+                }
+            }
+        }
+
+        var newUsersData = this.users;
+
+        for (var i = 0; i < this.usersSearchTerms.length; i++) {
+            newUsersData = this.dataTableService.filterData(newUsersData, this.usersSearchTerms[i], true);
+        }
+
+        newUsersData = this.dataTableService.sortData(newUsersData, sortBy, sortOrder);
+        this.filteredUsers = newUsersData;
+    },
+
+    /**
+     * Sort `filteredUsers` by `column`.
+     *
+     * @param column    The column to sort by.
+     */
+    sortUsers: function (column) {
+        if (column.sortable) {
+            var sortBy = column.name;
+            var sortOrder = column.sortOrder = (column.sortOrder === 'ASC') ? 'DESC' : 'ASC';
+            this.filterUsers(sortBy, sortOrder);
+        }
+    },
+
+    /**
+     * Checks the `allUsersSelected` property state and either selects
+     * or deselects each of the `filteredUsers`.
+     */
+    toggleUsersSelectAll: function () {
+        if (this.allUsersSelected) {
+            this.selectAllUsers();
+        } else {
+            this.deselectAllUsers();
+        }
+    },
+
+    /**
+     * Sets the `checked` property of each of the `filteredUsers` to true
+     * and sets the `isAddSelectedUsersToGroupDisabled` and the `allUsersSelected`
+     * properties accordingly.
+     */
+    selectAllUsers: function () {
+        this.filteredUsers.forEach(function (c) {
+            c.checked = true;
+        });
+        this.isAddSelectedUsersToGroupDisabled = false;
+        this.allUsersSelected = true;
+    },
+
+    /**
+     * Sets the `checked` property of each group to false
+     * and sets the `isAddSelectedUsersToGroupDisabled` and the `allUsersSelected`
+     * properties accordingly.
+     */
+    deselectAllUsers: function () {
+        this.filteredUsers.forEach(function (c) {
+            c.checked = false;
+        });
+        this.isAddSelectedUsersToGroupDisabled = true;
+        this.allUsersSelected = false;
+    },
+
+    /**
+     * Checks of each of the `filteredUsers`'s checked property state
+     * and sets the `allBucketsSelected` and `isAddSelectedUsersToGroupDisabled`
+     * property accordingly.
+     */
+    determineAllUsersSelectedState: function () {
+        var selected = 0;
+        var allSelected = true;
+        this.filteredUsers.forEach(function (c) {
+            if (c.checked) {
+                selected++;
+            }
+            if (c.checked === undefined || c.checked === false) {
+                allSelected = false;
+            }
+        });
+
+        if (selected > 0) {
+            this.isAddSelectedUsersToGroupDisabled = false;
+        } else {
+            this.isAddSelectedUsersToGroupDisabled = true;
+        }
+
+        this.allUsersSelected = allSelected;
+    },
+
+    /**
+     * Adds each of the selected users to this group.
+     */
+    addSelectedUsersToGroup: function () {
+        var self = this;
+        this.filteredUsers.filter(function (filteredUser) {
+            if(filteredUser.checked) {
+                self.data.group.users.push(filteredUser);
+            }
+        });
+        this.nfRegistryApi.updateUserGroup(self.data.group.identifier, self.data.group.identity, self.data.group.users).subscribe(function (group) {
+            self.dialogRef.close();
+            var snackBarRef = self.snackBarService.openCoaster({
+                title: 'Success',
+                message: 'Selected users have been added to the ' + self.data.group.identity + ' group.',
+                verticalPosition: 'bottom',
+                horizontalPosition: 'right',
+                icon: 'fa fa-check-circle-o',
+                color: '#1EB475',
+                duration: 3000
+            });
+        });
+    },
+
+    /**
+     * Cancel adding selected users to groups and close the dialog.
+     */
+    cancel: function () {
+        this.dialogRef.close();
+    }
+};
+
+NfRegistryAddUsersToGroup.annotations = [
+    new ngCore.Component({
+        template: require('./nf-registry-add-users-to-group.html!text')
+    })
+];
+
+NfRegistryAddUsersToGroup.parameters = [
+    NfRegistryApi,
+    covalentCore.TdDataTableService,
+    NfRegistryService,
+    ngMaterial.MatDialogRef,
+    fdsSnackBarsModule.FdsSnackBarService,
+    ngMaterial.MAT_DIALOG_DATA
+];
+
+module.exports = NfRegistryAddUsersToGroup;

http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/6f26290d/nifi-registry-core/nifi-registry-web-ui/src/main/webapp/components/administration/users/dialogs/add-users-to-group/nf-registry-add-users-to-group.spec.js
----------------------------------------------------------------------
diff --git a/nifi-registry-core/nifi-registry-web-ui/src/main/webapp/components/administration/users/dialogs/add-users-to-group/nf-registry-add-users-to-group.spec.js b/nifi-registry-core/nifi-registry-web-ui/src/main/webapp/components/administration/users/dialogs/add-users-to-group/nf-registry-add-users-to-group.spec.js
new file mode 100644
index 0000000..c1d2c71
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-web-ui/src/main/webapp/components/administration/users/dialogs/add-users-to-group/nf-registry-add-users-to-group.spec.js
@@ -0,0 +1,170 @@
+/*
+ * 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.
+ */
+
+var NfRegistryApi = require('nifi-registry/services/nf-registry.api.js');
+var NfRegistryService = require('nifi-registry/services/nf-registry.service.js');
+var NfRegistryAddUsersToGroup = require('nifi-registry/components/administration/users/dialogs/add-users-to-group/nf-registry-add-users-to-group.js');
+var rxjs = require('rxjs/Rx');
+var covalentCore = require('@covalent/core');
+var fdsSnackBarsModule = require('@flow-design-system/snackbars');
+
+describe('NfRegistryAddUsersToGroup Component isolated unit tests', function () {
+    var comp;
+    var nfRegistryService;
+    var nfRegistryApi;
+    var snackBarService;
+    var dataTableService;
+
+    beforeEach(function () {
+        nfRegistryService = new NfRegistryService();
+        // setup the nfRegistryService
+        nfRegistryService.group = {identifier: 1, identity: 'Group 1', users: []};
+        nfRegistryService.users = [{identifier: 2, identity: 'User 1', checked: true}];
+
+        nfRegistryApi = new NfRegistryApi();
+        snackBarService = new fdsSnackBarsModule.FdsSnackBarService();
+        dataTableService = new covalentCore.TdDataTableService();
+        comp = new NfRegistryAddUsersToGroup(nfRegistryApi, dataTableService, nfRegistryService, {
+            close: function () {
+            }
+        }, snackBarService, {group: nfRegistryService.group});
+
+        // Spy
+        spyOn(nfRegistryApi, 'updateUserGroup').and.callFake(function () {
+        }).and.returnValue(rxjs.Observable.of({identifier: 1, identity: 'Group 1'}));
+        spyOn(comp.dialogRef, 'close');
+        spyOn(comp.snackBarService, 'openCoaster');
+        spyOn(comp, 'filterUsers').and.callThrough();
+
+        // initialize the component
+        comp.ngOnInit();
+
+        //assertions
+        expect(comp.filterUsers).toHaveBeenCalled();
+        expect(comp.filteredUsers[0].identity).toEqual('User 1');
+        expect(comp.filteredUsers.length).toBe(1);
+        expect(comp).toBeDefined();
+    });
+
+    it('should make a call to the api to add selected users to the group', function () {
+        // select a group
+        comp.filteredUsers[0].checked = true;
+
+        // the function to test
+        comp.addSelectedUsersToGroup();
+
+        //assertions
+        expect(comp.dialogRef.close).toHaveBeenCalled();
+        expect(comp.snackBarService.openCoaster).toHaveBeenCalled();
+    });
+
+    it('should determine if all users are selected', function () {
+        // select a group
+        comp.filteredUsers[0].checked = true;
+
+        // the function to test
+        comp.determineAllUsersSelectedState();
+
+        //assertions
+        expect(comp.allUsersSelected).toBe(true);
+        expect(comp.isAddSelectedUsersToGroupDisabled).toBe(false);
+    });
+
+    it('should determine all user groups are not selected', function () {
+        // select a group
+        comp.filteredUsers[0].checked = false;
+
+        // the function to test
+        comp.determineAllUsersSelectedState();
+
+        //assertions
+        expect(comp.allUsersSelected).toBe(false);
+        expect(comp.isAddSelectedUsersToGroupDisabled).toBe(true);
+    });
+
+    it('should select all groups.', function () {
+        // The function to test
+        comp.selectAllUsers();
+
+        //assertions
+        expect(comp.filteredUsers[0].checked).toBe(true);
+        expect(comp.isAddSelectedUsersToGroupDisabled).toBe(false);
+        expect(comp.allUsersSelected).toBe(true);
+    });
+
+    it('should deselect all groups.', function () {
+        // select a group
+        comp.filteredUsers[0].checked = true;
+
+        // The function to test
+        comp.deselectAllUsers();
+
+        //assertions
+        expect(comp.filteredUsers[0].checked).toBe(false);
+        expect(comp.isAddSelectedUsersToGroupDisabled).toBe(true);
+        expect(comp.allUsersSelected).toBe(false);
+    });
+
+    it('should toggle all groups `checked` properties to true.', function () {
+        //Spy
+        spyOn(comp, 'selectAllUsers').and.callFake(function () {
+        });
+
+        comp.allUsersSelected = true;
+
+        // The function to test
+        comp.toggleUsersSelectAll();
+
+        //assertions
+        expect(comp.selectAllUsers).toHaveBeenCalled();
+    });
+
+    it('should toggle all groups `checked` properties to false.', function () {
+        //Spy
+        spyOn(comp, 'deselectAllUsers').and.callFake(function () {
+        });
+
+        comp.allUsersSelected = false;
+
+        // The function to test
+        comp.toggleUsersSelectAll();
+
+        //assertions
+        expect(comp.deselectAllUsers).toHaveBeenCalled();
+    });
+
+    it('should sort `groups` by `column`', function () {
+        // object to be updated by the test
+        var column = {name: 'name', label: 'Group Name', sortable: true};
+
+        // The function to test
+        comp.sortUsers(column);
+
+        //assertions
+        var filterUsersCall = comp.filterUsers.calls.mostRecent();
+        expect(filterUsersCall.args[0]).toBe('name');
+        expect(filterUsersCall.args[1]).toBe('ASC');
+    });
+
+    it('should cancel the creation of a new user', function () {
+        // the function to test
+        comp.cancel();
+
+        //assertions
+        expect(comp.dialogRef.close).toHaveBeenCalled();
+    });
+});
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/6f26290d/nifi-registry-core/nifi-registry-web-ui/src/main/webapp/components/administration/users/dialogs/create-new-group/nf-registry-create-new-group.html
----------------------------------------------------------------------
diff --git a/nifi-registry-core/nifi-registry-web-ui/src/main/webapp/components/administration/users/dialogs/create-new-group/nf-registry-create-new-group.html b/nifi-registry-core/nifi-registry-web-ui/src/main/webapp/components/administration/users/dialogs/create-new-group/nf-registry-create-new-group.html
new file mode 100644
index 0000000..d153651
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-web-ui/src/main/webapp/components/administration/users/dialogs/create-new-group/nf-registry-create-new-group.html
@@ -0,0 +1,46 @@
+<!--
+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.
+-->
+
+<div id="nifi-registry-admin-create-new-group-dialog">
+    <div class="pad-bottom-md" fxLayout="row" fxLayoutAlign="space-between center">
+        <span class="md-card-title">Create New Group</span>
+        <button mat-icon-button (click)="cancel()">
+            <mat-icon color="primary">close</mat-icon>
+        </button>
+    </div>
+    <div fxLayout="column" fxLayoutAlign="space-between start" class="pad-bottom-md">
+        <div class="pad-bottom-md fill-available-width">
+            <mat-input-container floatPlaceholder="always" fxFlex>
+                <input #createNewGroupInput matInput floatPlaceholder="always" placeholder="Display Name">
+            </mat-input-container>
+        </div>
+        <mat-checkbox [(ngModel)]="keepDialogOpen">
+            Keep this dialog open after creating group
+        </mat-checkbox>
+    </div>
+    <div fxLayout="row">
+        <span fxFlex></span>
+        <button (click)="cancel()" color="fds-regular" mat-raised-button
+                i18n="Cancel creation of new group|A button for cancelling the creation of a new group in the registry.@@nf-admin-workflow-cancel-create-new-group-button">
+            Cancel
+        </button>
+        <button [disabled]="createNewGroupInput.value.length === 0" class="push-left-sm" (click)="createNewGroup(createNewGroupInput)" color="fds-primary" mat-raised-button
+                i18n="Create new group button|A button for creating a new group in the registry.@@nf-admin-workflow-create-new-group-button">
+            Create
+        </button>
+    </div>
+</div>

http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/6f26290d/nifi-registry-core/nifi-registry-web-ui/src/main/webapp/components/administration/users/dialogs/create-new-group/nf-registry-create-new-group.js
----------------------------------------------------------------------
diff --git a/nifi-registry-core/nifi-registry-web-ui/src/main/webapp/components/administration/users/dialogs/create-new-group/nf-registry-create-new-group.js b/nifi-registry-core/nifi-registry-web-ui/src/main/webapp/components/administration/users/dialogs/create-new-group/nf-registry-create-new-group.js
new file mode 100644
index 0000000..bf8075e
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-web-ui/src/main/webapp/components/administration/users/dialogs/create-new-group/nf-registry-create-new-group.js
@@ -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.
+ */
+
+var ngCore = require('@angular/core');
+var NfRegistryService = require('nifi-registry/services/nf-registry.service.js');
+var NfRegistryApi = require('nifi-registry/services/nf-registry.api.js');
+var ngMaterial = require('@angular/material');
+var fdsSnackBarsModule = require('@flow-design-system/snackbars');
+
+/**
+ * NfRegistryCreateNewGroup constructor.
+ *
+ * @param nfRegistryApi         The api service.
+ * @param fdsSnackBarService    The FDS snack bar service module.
+ * @param nfRegistryService     The nf-registry.service module.
+ * @param matDialogRef          The angular material dialog ref.
+ * @constructor
+ */
+function NfRegistryCreateNewGroup(nfRegistryApi, fdsSnackBarService, nfRegistryService, matDialogRef) {
+    // Services
+    this.snackBarService = fdsSnackBarService;
+    this.nfRegistryService = nfRegistryService;
+    this.nfRegistryApi = nfRegistryApi;
+    this.dialogRef = matDialogRef;
+    // local state
+    this.keepDialogOpen = false;
+};
+
+NfRegistryCreateNewGroup.prototype = {
+    constructor: NfRegistryCreateNewGroup,
+
+    /**
+     * Create a new group.
+     *
+     * @param createNewGroupInput     The createNewGroupInput element.
+     */
+    createNewGroup: function (createNewGroupInput) {
+        var self = this;
+        // create new group with any selected users added to the new group
+        this.nfRegistryApi.createNewGroup(null, createNewGroupInput.value, this.nfRegistryService.getSelectedUsers()).subscribe(function (group) {
+            if (!group.error) {
+                self.nfRegistryService.groups.push(group);
+                self.nfRegistryService.filterUsersAndGroups();
+                self.nfRegistryService.allUsersAndGroupsSelected = false;
+                if (self.keepDialogOpen !== true) {
+                    self.dialogRef.close();
+                }
+                self.snackBarService.openCoaster({
+                    title: 'Success',
+                    message: 'Group has been added.',
+                    verticalPosition: 'bottom',
+                    horizontalPosition: 'right',
+                    icon: 'fa fa-check-circle-o',
+                    color: '#1EB475',
+                    duration: 3000
+                });
+            } else {
+                self.dialogRef.close();
+            }
+        });
+    },
+
+    /**
+     * Cancel creation of a new bucket and close dialog.
+     */
+    cancel: function () {
+        this.dialogRef.close();
+    },
+
+    /**
+     * Focus the new group input.
+     */
+    ngAfterViewChecked: function () {
+        this.createNewGroupInput.nativeElement.focus();
+    }
+};
+
+NfRegistryCreateNewGroup.annotations = [
+    new ngCore.Component({
+        template: require('./nf-registry-create-new-group.html!text'),
+        queries: {
+            createNewGroupInput: new ngCore.ViewChild('createNewGroupInput')
+        }
+    })
+];
+
+NfRegistryCreateNewGroup.parameters = [
+    NfRegistryApi,
+    fdsSnackBarsModule.FdsSnackBarService,
+    NfRegistryService,
+    ngMaterial.MatDialogRef
+];
+
+module.exports = NfRegistryCreateNewGroup;

http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/6f26290d/nifi-registry-core/nifi-registry-web-ui/src/main/webapp/components/administration/users/dialogs/create-new-group/nf-registry-create-new-group.spec.js
----------------------------------------------------------------------
diff --git a/nifi-registry-core/nifi-registry-web-ui/src/main/webapp/components/administration/users/dialogs/create-new-group/nf-registry-create-new-group.spec.js b/nifi-registry-core/nifi-registry-web-ui/src/main/webapp/components/administration/users/dialogs/create-new-group/nf-registry-create-new-group.spec.js
new file mode 100644
index 0000000..631e415
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-web-ui/src/main/webapp/components/administration/users/dialogs/create-new-group/nf-registry-create-new-group.spec.js
@@ -0,0 +1,85 @@
+/*
+ * 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.
+ */
+
+var NfRegistryApi = require('nifi-registry/services/nf-registry.api.js');
+var NfRegistryService = require('nifi-registry/services/nf-registry.service.js');
+var NfRegistryCreateNewGroup = require('nifi-registry/components/administration/users/dialogs/create-new-group/nf-registry-create-new-group.js');
+var rxjs = require('rxjs/Rx');
+
+describe('NfRegistryCreateNewGroup Component isolated unit tests', function () {
+    var comp;
+    var nfRegistryService;
+    var nfRegistryApi;
+
+    beforeEach(function () {
+        nfRegistryService = new NfRegistryService();
+        nfRegistryApi = new NfRegistryApi();
+        comp = new NfRegistryCreateNewGroup(nfRegistryApi, {
+                openCoaster: function () {
+                }
+            },
+            nfRegistryService,
+            {
+                close: function () {
+                }
+            });
+
+        // Spy
+        spyOn(nfRegistryApi, 'createNewGroup').and.callFake(function () {
+        }).and.returnValue(rxjs.Observable.of([{
+            'identifier': '2e04b4fb-9513-47bb-aa74-1ae34616bfdc',
+            'identity': 'New Group #1'
+        }]));
+        spyOn(nfRegistryService, 'filterUsersAndGroups');
+        spyOn(comp.dialogRef, 'close');
+    });
+
+    it('should make a call to the api to create a new group and close the dialog.', function () {
+        // the function to test
+        comp.createNewGroup({value: 'New Group #1'});
+
+        //assertions
+        expect(comp).toBeDefined();
+        expect(nfRegistryService.groups.length).toBe(1);
+        expect(nfRegistryService.allUsersAndGroupsSelected).toBe(false);
+        expect(nfRegistryService.filterUsersAndGroups).toHaveBeenCalled();
+        expect(comp.dialogRef.close).toHaveBeenCalled();
+    });
+
+    it('should make a call to the api to create a new group and keep the dialog open.', function () {
+        // setup the component
+        comp.keepDialogOpen = true;
+
+        // the function to test
+        comp.createNewGroup({value: 'New Group #1'});
+
+        //assertions
+        expect(comp).toBeDefined();
+        expect(nfRegistryService.groups.length).toBe(1);
+        expect(nfRegistryService.allUsersAndGroupsSelected).toBe(false);
+        expect(nfRegistryService.filterUsersAndGroups).toHaveBeenCalled();
+        expect(comp.dialogRef.close.calls.count()).toEqual(0);
+    });
+
+    it('should cancel the creation of a new group', function () {
+        // the function to test
+        comp.cancel();
+
+        //assertions
+        expect(comp.dialogRef.close).toHaveBeenCalled();
+    });
+});
\ No newline at end of file


[07/51] [partial] nifi-registry git commit: NIFIREG-201 Refactoring project structure to better isolate extensions

Posted by kd...@apache.org.
http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/6f26290d/nifi-registry-core/nifi-registry-web-ui/pom.xml
----------------------------------------------------------------------
diff --git a/nifi-registry-core/nifi-registry-web-ui/pom.xml b/nifi-registry-core/nifi-registry-web-ui/pom.xml
new file mode 100644
index 0000000..ab107b3
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-web-ui/pom.xml
@@ -0,0 +1,493 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+  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.
+-->
+
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+    <modelVersion>4.0.0</modelVersion>
+    <parent>
+        <groupId>org.apache.nifi.registry</groupId>
+        <artifactId>nifi-registry-core</artifactId>
+        <version>0.3.0-SNAPSHOT</version>
+    </parent>
+    <groupId>org.apache.nifi.registry</groupId>
+    <artifactId>nifi-registry-web-ui</artifactId>
+    <version>0.3.0-SNAPSHOT</version>
+    <packaging>war</packaging>
+    <properties>
+        <staging.dir>${project.build.directory}/tmp</staging.dir>
+        <registry.filter>registry-min.properties</registry.filter>
+        <frontend.source>${basedir}/src/main</frontend.source>
+        <frontend.dependency.configs>${basedir}/src/main/frontend</frontend.dependency.configs>
+        <frontend.working.dir>${project.build.directory}/frontend-working-directory</frontend.working.dir>
+        <frontend.assets>${project.build.directory}/${project.build.finalName}/node_modules</frontend.assets>
+    </properties>
+    <build>
+        <!--
+            These filters are used to populate the includes (css and js)
+            for each of the available pages. The property is the name of
+            the file which contains the properties that define which
+            css and js files get included. When running with minify and
+            compression (default) the filter properties will be overridden
+            in the profile. The JSPs that contain the HEAD portion of the
+            pages will not be pre-compiled and will instead be filtered
+            when the war is built.
+        -->
+        <filters>
+            <filter>src/main/resources/filters/${registry.filter}</filter>
+        </filters>
+        <plugins>
+            <!--
+                Precompile jsp's and add entries into the web.xml - the web.xml
+                is automatically places in ${project.build.directory}. Do not
+                precompile index.jsp, etc.
+                These jsp's need to have the artifacts version filtered in to
+                eliminate browser caching issues and set up the proper includes.
+                Since the webResource filter occurs after the precompilation we
+                must exclude them here.
+            -->
+            <plugin>
+                <groupId>org.eclipse.jetty</groupId>
+                <artifactId>jetty-jspc-maven-plugin</artifactId>
+                <version>${jetty.version}</version>
+                <executions>
+                    <execution>
+                        <goals>
+                            <goal>jspc</goal>
+                        </goals>
+                        <configuration>
+                            <keepSources>true</keepSources>
+                            <useProvidedScope>true</useProvidedScope>
+                            <excludes>
+                                **/index.jsp
+                            </excludes>
+                        </configuration>
+                    </execution>
+                </executions>
+            </plugin>
+            <plugin>
+                <groupId>org.apache.maven.plugins</groupId>
+                <artifactId>maven-resources-plugin</artifactId>
+                <executions>
+                    <!--
+                        Filter the web.xml that was generated from jspc to specify the
+                        NiFi Registry base directory. The plugin configuration is
+                        specified here while the execution's are defined below in the
+                        profiles to bind to the appropriate phase.
+                    -->
+                    <execution>
+                        <id>copy-web-xml</id>
+                        <phase>prepare-package</phase>
+                        <goals>
+                            <goal>copy-resources</goal>
+                        </goals>
+                        <configuration>
+                            <outputDirectory>${staging.dir}/WEB-INF</outputDirectory>
+                            <resources>
+                                <resource>
+                                    <directory>${project.build.directory}</directory>
+                                    <filtering>true</filtering>
+                                    <includes>
+                                        <include>web.xml</include>
+                                    </includes>
+                                </resource>
+                            </resources>
+                        </configuration>
+                    </execution>
+                    <!--
+                        Copy build and test configs into frontend working directory.
+                    -->
+                    <execution>
+                        <id>copy-client-side-build-and-test-configs</id>
+                        <phase>initialize</phase>
+                        <goals>
+                            <goal>copy-resources</goal>
+                        </goals>
+                        <configuration>
+                            <outputDirectory>${frontend.working.dir}</outputDirectory>
+                            <resources>
+                                <resource>
+                                    <directory>${frontend.dependency.configs}</directory>
+                                    <filtering>false</filtering>
+                                    <includes>
+                                        <include>*</include>
+                                    </includes>
+                                </resource>
+                            </resources>
+                        </configuration>
+                    </execution>
+                    <!--
+                        Copy src into frontend working directory.
+                    -->
+                    <execution>
+                        <id>copy-source</id>
+                        <phase>initialize</phase>
+                        <goals>
+                            <goal>copy-resources</goal>
+                        </goals>
+                        <configuration>
+                            <outputDirectory>${frontend.working.dir}</outputDirectory>
+                            <resources>
+                                <resource>
+                                    <directory>${frontend.source}</directory>
+                                    <filtering>false</filtering>
+                                    <includes>
+                                        <include>locale/**/*</include>
+                                        <include>webapp/**/*</include>
+                                        <include>platform/**/*</include>
+                                    </includes>
+                                </resource>
+                            </resources>
+                        </configuration>
+                    </execution>
+                    <!--
+                        Stage client side node_modules dependencies for inclusion in .war.
+                    -->
+                    <execution>
+                        <id>copy-client-side-deps</id>
+                        <phase>prepare-package</phase>
+                        <goals>
+                            <goal>copy-resources</goal>
+                        </goals>
+                        <configuration>
+                            <outputDirectory>${frontend.assets}</outputDirectory>
+                            <resources>
+                                <resource>
+                                    <directory>${frontend.working.dir}/node_modules</directory>
+                                    <filtering>false</filtering>
+                                    <includes>
+                                        <!-- roboto -->
+                                        <include>roboto-fontface/fonts/roboto-slab/Roboto-Slab-Regular.ttf</include>
+                                        <include>roboto-fontface/fonts/roboto/Roboto-Regular.ttf</include>
+                                        <include>roboto-fontface/fonts/roboto/Roboto-Medium.ttf</include>
+                                        <include>roboto-fontface/fonts/roboto/Roboto-Light.ttf</include>
+                                        <include>roboto-fontface/fonts/roboto/Roboto-Bold.ttf</include>
+                                        <include>roboto-fontface/LICENSE*</include>
+                                        <!-- covalent -->
+                                        <include>@covalent/core/common/platform.css</include>
+                                        <include>@covalent/core/common/styles/font/MaterialIcons-Regular.woff2</include>
+                                        <include>@covalent/core/common/styles/font/MaterialIcons-Regular.ttf</include>
+                                        <include>@covalent/core/README.md</include>
+                                        <!-- FDS -->
+                                        <include>@nifi-fds/core/common/styles/css/*</include>
+                                        <include>@nifi-fds/core/LICENSE</include>
+                                        <include>@nifi-fds/core/NOTICE</include>
+                                        <include>@nifi-fds/core/README.md</include>
+                                        <!-- font-awesome -->
+                                        <include>font-awesome/css/font-awesome.css</include>
+                                        <include>font-awesome/fonts/fontawesome-webfont.woff2</include>
+                                        <include>font-awesome/fonts/fontawesome-webfont.ttf</include>
+                                        <include>font-awesome/README.md</include>
+                                    </includes>
+                                </resource>
+                            </resources>
+                        </configuration>
+                    </execution>
+                    <!--
+                        Stage client side styles.
+                    -->
+                    <execution>
+                        <id>copy-webapp-client-side-styles</id>
+                        <phase>prepare-package</phase>
+                        <goals>
+                            <goal>copy-resources</goal>
+                        </goals>
+                        <configuration>
+                            <outputDirectory>${project.build.directory}/${project.build.finalName}/css</outputDirectory>
+                            <resources>
+                                <resource>
+                                    <directory>${frontend.working.dir}/webapp/css</directory>
+                                    <filtering>false</filtering>
+                                    <includes>
+                                        <include>*</include>
+                                    </includes>
+                                </resource>
+                            </resources>
+                        </configuration>
+                    </execution>
+                </executions>
+            </plugin>
+            <!--
+                Tell the war plugin where to find the filtered web.xml and
+                filter the head portion of the pages. The correct includes and
+                project version is filtered into these jsp's as a browser cache
+                buster.
+            -->
+            <plugin>
+                <groupId>org.apache.maven.plugins</groupId>
+                <artifactId>maven-war-plugin</artifactId>
+                <configuration>
+                    <webXml>${staging.dir}/WEB-INF/web.xml</webXml>
+                    <webResources>
+                        <resource>
+                            <directory>src/main/webapp/WEB-INF/pages</directory>
+                            <targetPath>WEB-INF/pages</targetPath>
+                            <includes>
+                                <include>index.jsp</include>
+                            </includes>
+                            <filtering>true</filtering>
+                        </resource>
+                    </webResources>
+                </configuration>
+            </plugin>
+            <plugin>
+                <groupId>com.github.eirslett</groupId>
+                <artifactId>frontend-maven-plugin</artifactId>
+                <version>1.5</version>
+                <configuration>
+                    <installDirectory>${frontend.working.dir}</installDirectory>
+                </configuration>
+                <executions>
+                    <!--
+                        Install node and npm.
+                    -->
+                    <execution>
+                        <id>install-node-and-npm</id>
+                        <goals>
+                            <goal>install-node-and-npm</goal>
+                        </goals>
+                        <phase>initialize</phase>
+                        <configuration>
+                            <nodeVersion>v8.10.0</nodeVersion>
+                        </configuration>
+                    </execution>
+                    <!--
+                        Install node_modules (build, test, AND client side dependencies).
+                    -->
+                    <execution>
+                        <id>npm-install</id>
+                        <goals>
+                            <goal>npm</goal>
+                        </goals>
+                        <phase>initialize</phase>
+                        <configuration>
+                            <arguments>--silent --cache-min Infinity install</arguments>
+                            <workingDirectory>${frontend.working.dir}</workingDirectory>
+                        </configuration>
+                    </execution>
+                    <!--
+                        Compile nifi registry web ui SASS into css and gzip compress it.
+                    -->
+                    <execution>
+                        <id>grunt-compile-web-ui-sass</id>
+                        <goals>
+                            <goal>grunt</goal>
+                        </goals>
+                        <phase>generate-resources</phase>
+                        <configuration>
+                            <arguments>compile-web-ui-styles</arguments>
+                            <workingDirectory>${frontend.working.dir}</workingDirectory>
+                        </configuration>
+                    </execution>
+                    <!--
+                        Bundle, minify, and gzip compress all the javascript.
+                    -->
+                    <execution>
+                        <id>grunt-package-web-ui</id>
+                        <goals>
+                            <goal>grunt</goal>
+                        </goals>
+                        <phase>generate-resources</phase>
+                        <configuration>
+                            <arguments>bundle-web-ui</arguments>
+                            <workingDirectory>${frontend.working.dir}</workingDirectory>
+                        </configuration>
+                    </execution>
+                </executions>
+            </plugin>
+            <plugin>
+                <groupId>org.apache.maven.plugins</groupId>
+                <artifactId>maven-resources-plugin</artifactId>
+                <executions>
+                    <!--
+                        Stage the final bundle of JS to be included in the .war
+                    -->
+                    <execution>
+                        <id>copy-web-ui-bundle</id>
+                        <phase>prepare-package</phase>
+                        <goals>
+                            <goal>copy-resources</goal>
+                        </goals>
+                        <configuration>
+                            <outputDirectory>${project.build.directory}/${project.build.finalName}
+                            </outputDirectory>
+                            <resources>
+                                <resource>
+                                    <directory>${frontend.working.dir}/webapp</directory>
+                                    <filtering>false</filtering>
+                                    <includes>
+                                        <include>nf-registry.bundle.*</include>
+                                    </includes>
+                                </resource>
+                            </resources>
+                        </configuration>
+                    </execution>
+                    <!--
+                        Stage the localization files to be included in the .war
+                    -->
+                    <execution>
+                        <id>copy-localization</id>
+                        <phase>prepare-package</phase>
+                        <goals>
+                            <goal>copy-resources</goal>
+                        </goals>
+                        <configuration>
+                            <outputDirectory>${project.build.directory}/${project.build.finalName}
+                            </outputDirectory>
+                            <resources>
+                                <resource>
+                                    <directory>${frontend.working.dir}/locale</directory>
+                                    <filtering>false</filtering>
+                                    <includes>
+                                        <include>*</include>
+                                    </includes>
+                                </resource>
+                            </resources>
+                        </configuration>
+                    </execution>
+                </executions>
+            </plugin>
+            <plugin>
+                <groupId>org.apache.rat</groupId>
+                <artifactId>apache-rat-plugin</artifactId>
+                <configuration>
+                    <excludes combine.children="append">
+                        <exclude>nbactions.xml</exclude>
+                        <exclude>src/main/frontend/package.json</exclude>
+                        <exclude>src/main/frontend/package-lock.json</exclude>
+                        <exclude>src/main/platform/core/README.md</exclude>
+                        <exclude>src/main/frontend/karma-test-shim.js</exclude>
+                        <exclude>src/main/webapp/systemjs-angular-loader.js</exclude>
+                    </excludes>
+                </configuration>
+            </plugin>
+        </plugins>
+    </build>
+    <profiles>
+        <profile>
+            <id>jsUnitTests</id>
+            <activation>
+                <activeByDefault>false</activeByDefault>
+            </activation>
+            <build>
+                <plugins>
+                    <!--
+                        Selenium, Karma/Jasmine JS unit tests.
+                    -->
+                    <plugin>
+                        <groupId>com.github.eirslett</groupId>
+                        <artifactId>frontend-maven-plugin</artifactId>
+                        <version>1.5</version>
+                        <configuration>
+                            <installDirectory>${frontend.working.dir}</installDirectory>
+                        </configuration>
+                        <executions>
+                            <execution>
+                                <id>javascript-tests</id>
+                                <goals>
+                                    <goal>npm</goal>
+                                </goals>
+                                <phase>test</phase>
+                                <configuration>
+                                    <arguments>run test</arguments>
+                                    <workingDirectory>${frontend.working.dir}</workingDirectory>
+                                </configuration>
+                            </execution>
+                        </executions>
+                    </plugin>
+                </plugins>
+            </build>
+        </profile>
+        <profile>
+            <id>development-mode</id>
+            <activation>
+                <activeByDefault>false</activeByDefault>
+            </activation>
+            <properties>
+                <registry.filter>registry.properties</registry.filter>
+            </properties>
+            <build>
+                <plugins>
+                    <plugin>
+                        <groupId>org.apache.maven.plugins</groupId>
+                        <artifactId>maven-resources-plugin</artifactId>
+                        <executions>
+                            <!--
+                                Stage client side node_modules dependencies for inclusion in .war.
+                            -->
+                            <execution>
+                                <id>copy-development-mode-client-side-deps</id>
+                                <phase>prepare-package</phase>
+                                <goals>
+                                    <goal>copy-resources</goal>
+                                </goals>
+                                <configuration>
+                                    <outputDirectory>${frontend.assets}</outputDirectory>
+                                    <resources>
+                                        <resource>
+                                            <directory>${frontend.working.dir}/node_modules</directory>
+                                            <filtering>false</filtering>
+                                            <includes>
+                                                <include>@nifi-fds/**/*</include>
+                                                <include>@angular/**/*</include>
+                                                <include>hammerjs/**/*</include>
+                                                <include>@covalent/**/*</include>
+                                                <include>rxjs/**/*</include>
+                                                <include>moment/**/*</include>
+                                                <include>angular2-moment/**/*</include>
+                                                <include>zone.js/**/*</include>
+                                                <include>core-js/**/*</include>
+                                                <include>superagent/**/*</include>
+                                                <include>querystring/**/*</include>
+                                                <include>tslib/**/*</include>
+                                                <include>systemjs/**/*</include>
+                                                <include>systemjs-plugin-text/**/*</include>
+                                                <include>jquery/**/*</include>
+                                                <include>roboto-fontface/**/*</include>
+                                            </includes>
+                                        </resource>
+                                    </resources>
+                                </configuration>
+                            </execution>
+                        </executions>
+                    </plugin>
+                    <!--
+                        Speed up build time by excluding node, npm, and any node_modules from `mvn clean` since the front-end-maven plugin uses these
+                        directories as cache.
+                    -->
+                    <plugin>
+                        <groupId>org.apache.maven.plugins</groupId>
+                        <artifactId>maven-clean-plugin</artifactId>
+                        <version>3.0.0</version>
+                        <configuration>
+                            <excludeDefaultDirectories>true</excludeDefaultDirectories>
+                            <filesets>
+                                <fileset>
+                                    <directory>${project.build.directory}</directory>
+                                    <includes>
+                                        <include>**</include>
+                                    </includes>
+                                    <excludes>
+                                        <exclude>frontend-working-directory/node/**/*</exclude>
+                                        <exclude>frontend-working-directory/node_modules/**/*</exclude>
+                                    </excludes>
+                                </fileset>
+                            </filesets>
+                        </configuration>
+                    </plugin>
+                </plugins>
+            </build>
+        </profile>
+    </profiles>
+</project>

http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/6f26290d/nifi-registry-core/nifi-registry-web-ui/src/main/frontend/Gruntfile.js
----------------------------------------------------------------------
diff --git a/nifi-registry-core/nifi-registry-web-ui/src/main/frontend/Gruntfile.js b/nifi-registry-core/nifi-registry-web-ui/src/main/frontend/Gruntfile.js
new file mode 100644
index 0000000..8b1fee1
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-web-ui/src/main/frontend/Gruntfile.js
@@ -0,0 +1,78 @@
+/*
+ * 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.
+ */
+
+module.exports = function (grunt) {
+    // load all grunt tasks matching the ['grunt-*', '@*/grunt-*'] patterns
+    require('load-grunt-tasks')(grunt);
+
+    grunt.initConfig({
+        sass: {
+            options: {
+                implementation: require('node-sass'),
+                outputStyle: 'compressed',
+                sourceMap: true
+            },
+            minifyWebUi: {
+                files: [{
+                    './webapp/css/nf-registry.min.css': ['./webapp/theming/nf-registry.scss']
+                }]
+            }
+        },
+        systemjs: {
+            options: {
+                sfx: true,
+                minify: true, // Comment out this line when developing
+                sourceMaps: true,
+                build: {
+                    lowResSourceMaps: true
+                }
+            },
+            bundleWebUi: {
+                options: {
+                    configFile: "./webapp/systemjs.builder.config.js"
+                },
+                files: [{
+                    "src": "./webapp/nf-registry-bootstrap.js",
+                    "dest": "./webapp/nf-registry.bundle.min.js"
+                }]
+            }
+        },
+        compress: {
+            options: {
+                mode: 'gzip'
+            },
+            webUi: {
+                files: [{
+                    expand: true,
+                    src: ['./webapp/nf-registry.bundle.min.js'],
+                    dest: './',
+                    ext: '.bundle.min.js.gz'
+                }]
+            },
+            webUiStyles: {
+                files: [{
+                    expand: true,
+                    src: ['./webapp/css/nf-registry.min.css'],
+                    dest: './',
+                    ext: '.min.css.gz'
+                }]
+            }
+        }
+    });
+    grunt.registerTask('compile-web-ui-styles', ['sass:minifyWebUi', 'compress:webUiStyles']);
+    grunt.registerTask('bundle-web-ui', ['systemjs:bundleWebUi', 'compress:webUi']);
+};

http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/6f26290d/nifi-registry-core/nifi-registry-web-ui/src/main/frontend/karma-test-shim.js
----------------------------------------------------------------------
diff --git a/nifi-registry-core/nifi-registry-web-ui/src/main/frontend/karma-test-shim.js b/nifi-registry-core/nifi-registry-web-ui/src/main/frontend/karma-test-shim.js
new file mode 100644
index 0000000..8b27f94
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-web-ui/src/main/frontend/karma-test-shim.js
@@ -0,0 +1,96 @@
+// /*global jasmine, __karma__, window*/
+Error.stackTraceLimit = 0; // "No stacktrace"" is usually best for app testing.
+
+// Uncomment to get full stacktrace output. Sometimes helpful, usually not.
+// Error.stackTraceLimit = Infinity; //
+
+jasmine.DEFAULT_TIMEOUT_INTERVAL = 1000;
+
+// builtPaths: root paths for output ("built") files
+// get from karma.config.js, then prefix with '/base/'
+var builtPaths = (__karma__.config.builtPaths)
+    .map(function (p) {
+        return '/base/' + p;
+    });
+
+__karma__.loaded = function () {
+};
+
+function isJsFile(path) {
+    return path.slice(-3) == '.js';
+}
+
+function isSpecFile(path) {
+    return /\.spec\.(.*\.)?js$/.test(path);
+}
+
+// Is a "built" file if is JavaScript file in one of the "built" folders
+function isBuiltFile(path) {
+    return isJsFile(path) &&
+        builtPaths.reduce(function (keep, bp) {
+            return keep || (path.substr(0, bp.length) === bp);
+        }, false);
+}
+
+var allSpecFiles = Object.keys(window.__karma__.files)
+    .filter(isSpecFile)
+    .filter(isBuiltFile);
+
+System.config({
+    // Base URL for System.js calls. 'base/' is where Karma serves files from.
+    baseURL: 'base',
+
+    // Map the angular testing umd bundles
+    map: {
+        '@angular/core/testing': 'npm:@angular/core/bundles/core-testing.umd.js',
+        '@angular/common/testing': 'npm:@angular/common/bundles/common-testing.umd.js',
+        '@angular/compiler/testing': 'npm:@angular/compiler/bundles/compiler-testing.umd.js',
+        '@angular/platform-browser/testing': 'npm:@angular/platform-browser/bundles/platform-browser-testing.umd.js',
+        '@angular/platform-browser-dynamic/testing': 'npm:@angular/platform-browser-dynamic/bundles/platform-browser-dynamic-testing.umd.js',
+        '@angular/http/testing': 'npm:@angular/http/bundles/http-testing.umd.js',
+        '@angular/router/testing': 'npm:@angular/router/bundles/router-testing.umd.js',
+        '@angular/forms/testing': 'npm:@angular/forms/bundles/forms-testing.umd.js'
+    }
+});
+
+System.import('webapp/systemjs.spec.config.js')
+    .then(importSystemJsExtras)
+    .then(initTestBed)
+    .then(initTesting);
+
+/** Optional SystemJS configuration extras. Keep going w/o it */
+function importSystemJsExtras() {
+    return System.import('systemjs.config.extras.js')
+        .catch(function (reason) {
+            console.log(
+                'Warning: System.import could not load the optional "systemjs.config.extras.js". Did you omit it by accident? Continuing without it.'
+            );
+            console.log(reason);
+        });
+}
+
+function initTestBed() {
+    return Promise.all([
+        System.import('@angular/core/testing'),
+        System.import('@angular/platform-browser-dynamic/testing')
+    ])
+
+        .then(function (providers) {
+            var coreTesting = providers[0];
+            var browserTesting = providers[1];
+
+            coreTesting.TestBed.initTestEnvironment(
+                browserTesting.BrowserDynamicTestingModule,
+                browserTesting.platformBrowserDynamicTesting());
+        })
+}
+
+// Import all spec files and start karma
+function initTesting() {
+    return Promise.all(
+        allSpecFiles.map(function (moduleName) {
+            return System.import(moduleName);
+        })
+    )
+        .then(__karma__.start, __karma__.error);
+}

http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/6f26290d/nifi-registry-core/nifi-registry-web-ui/src/main/frontend/karma.conf.js
----------------------------------------------------------------------
diff --git a/nifi-registry-core/nifi-registry-web-ui/src/main/frontend/karma.conf.js b/nifi-registry-core/nifi-registry-web-ui/src/main/frontend/karma.conf.js
new file mode 100644
index 0000000..6697152
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-web-ui/src/main/frontend/karma.conf.js
@@ -0,0 +1,176 @@
+/*
+ * 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.
+ */
+
+module.exports = function (config) {
+
+    var appBase = 'webapp/';       // app JS and map files
+
+    config.set({
+        basePath: '',
+        browserNoActivityTimeout: 9999999, //default 10000
+        browserDisconnectTimeout: 999999, // default 2000
+        browserDisconnectTolerance: 1, // default 0
+        captureTimeout: 999999,
+        frameworks: ['jasmine'],
+        customLaunchers: {
+            Chrome_travis_ci: {
+                base: 'ChromeHeadless',
+                flags: ['--no-sandbox']
+            }
+        },
+        plugins: [
+            require('karma-jasmine'),
+            require('karma-chrome-launcher'),
+            require('karma-jasmine-html-reporter'),
+            require('karma-spec-reporter'),
+            require('karma-coverage')
+        ],
+
+        client: {
+            builtPaths: [appBase], // add more spec base paths as needed
+            clearContext: false // leave Jasmine Spec Runner output visible in browser
+        },
+
+        files: [
+            // System.js for module loading
+            'node_modules/systemjs/dist/system.src.js',
+
+            // Polyfills
+            'node_modules/core-js/client/shim.js',
+
+            // zone.js
+            'node_modules/zone.js/dist/zone.js',
+            'node_modules/zone.js/dist/long-stack-trace-zone.js',
+            'node_modules/zone.js/dist/proxy.js',
+            'node_modules/zone.js/dist/sync-test.js',
+            'node_modules/zone.js/dist/jasmine-patch.js',
+            'node_modules/zone.js/dist/async-test.js',
+            'node_modules/zone.js/dist/fake-async-test.js',
+
+            // others
+            'node_modules/hammerjs/hammer.js',
+            'node_modules/moment/moment.js',
+            'node_modules/superagent/superagent.js',
+            'node_modules/tslib/tslib.js',
+
+            // RxJs
+            {pattern: 'node_modules/rxjs/**/*.js', included: false, watched: false},
+            {pattern: 'node_modules/rxjs/**/*.js.map', included: false, watched: false},
+
+            // Paths loaded via module imports:
+            {pattern: 'node_modules/systemjs/**/*.js.map', included: false, watched: false},
+            {pattern: 'node_modules/@angular/**/*.js', included: false, watched: false},
+            {pattern: 'node_modules/@angular/**/*.js.map', included: false, watched: false},
+            {pattern: 'node_modules/@covalent/**/*.js', included: false, watched: false},
+            {pattern: 'node_modules/@covalent/**/*.js.map', included: false, watched: false},
+            {pattern: 'node_modules/@nifi-fds/**/*.js', included: false, watched: false},
+            {pattern: 'node_modules/jquery/**/*.js', included: false, watched: false},
+            {pattern: 'node_modules/roboto-fontface/**/*.ttf', included: false, watched: false},
+            {pattern: 'node_modules/angular2-moment/**/*.js', included: false, watched: false},
+            {pattern: 'node_modules/angular2-moment/**/*.js.map', included: false, watched: false},
+            {pattern: 'node_modules/querystring/**/*.js', included: false, watched: false},
+            {pattern: 'node_modules/systemjs-plugin-text/text.js', included: false, watched: false},
+
+            {pattern: appBase + 'systemjs.spec.config.js', included: false, watched: false},
+            'karma-test-shim.js', // optionally extend SystemJS mapping e.g., with barrels
+
+            // Include the Flow Design System (which includes the Teradata Covalent and
+            // Angular Material themes) in the test suite.
+            {
+                pattern: 'node_modules/@nifi-fds/core/common/styles/css/*.min.css',
+                included: true,
+                watched: true,
+                served: true
+            },
+            {
+                pattern: 'node_modules/@nifi-fds/core/**/*.html',
+                included: true,
+                watched: true,
+                served: true
+            },
+
+            // Include the Nifi Registry styles (currently built based off of the
+            // @nifi-fds/core/common/styles/_globalVars.scss)
+            {
+                pattern: 'webapp/css/*.css',
+                included: true,
+                watched: true
+            },
+            {
+                pattern: 'webapp/**/*.html',
+                included: true,
+                watched: true,
+                served: true
+            },
+
+            // Images
+            {pattern: '**/*.svg', watched: false, included: true, served: true},
+
+            // Paths for debugging with source maps in dev tools
+            {pattern: 'node_modules/@nifi-fds/core/**/*.css.map', included: false, watched: false},
+            {pattern: appBase + '**/*.js.map', included: false, watched: false},
+            {pattern: appBase + '**/*.css.map', included: false, watched: false},
+            {pattern: appBase + '**/*.js', included: false, watched: false}
+        ],
+
+        // Proxied base paths for loading assets
+        proxies: {
+            // required for modules fetched by SystemJS
+            '/base/nifi-registry/node_modules/': '/base/node_modules/',
+            '/base/systemjs-angular-loader.js': '/base/webapp/systemjs-angular-loader.js',
+            '/base/nifi-registry/': '/base/webapp/',
+            '/nifi-registry/images/': '/base/webapp/images/',
+            '/nifi-registry/explorer/nifi-registry/images/': '/base/webapp/images/',
+            '/nifi-registry/explorer/grid-list/buckets/nifi-registry/images/': '/base/webapp/images/'
+        },
+
+        exclude: [],
+        preprocessors: {
+            'webapp/**/!(*spec|*mock|*stub|*config|*extras).js': 'coverage'
+        },
+        reporters: ['kjhtml', 'spec', 'coverage'],
+        coverageReporter: {
+            type: 'html',
+            dir: 'coverage/'
+        },
+        specReporter: {
+            failFast: false
+        },
+        port: 9876,
+        colors: true,
+        logLevel: config.LOG_INFO,
+        autoWatch: true,
+        browsers: ['Chrome'],
+        singleRun: false
+    });
+
+    if (process.env.TRAVIS) {
+        config.set({
+            browsers: ['Chrome_travis_ci']
+        });
+
+        // Override base config
+        config.set({
+            singleRun: true,
+            autoWatch: false,
+            reporters: ['spec', 'coverage'],
+            specReporter: {
+                failFast: true
+            }
+        });
+    }
+}


[23/51] [partial] nifi-registry git commit: NIFIREG-201 Refactoring project structure to better isolate extensions

Posted by kd...@apache.org.
http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/6f26290d/nifi-registry-core/nifi-registry-properties/src/test/resources/conf/bootstrap.with_missing_key_line.conf
----------------------------------------------------------------------
diff --git a/nifi-registry-core/nifi-registry-properties/src/test/resources/conf/bootstrap.with_missing_key_line.conf b/nifi-registry-core/nifi-registry-properties/src/test/resources/conf/bootstrap.with_missing_key_line.conf
new file mode 100644
index 0000000..6ccdaaf
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-properties/src/test/resources/conf/bootstrap.with_missing_key_line.conf
@@ -0,0 +1,60 @@
+#
+# 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.
+#
+
+# Java command to use when running nifi-registry
+java=java
+
+# Username to use when running nifi-registry. This value will be ignored on Windows.
+run.as=
+
+# Configure where nifi-registry's lib and conf directories live
+lib.dir=./lib
+conf.dir=./conf
+
+# How long to wait after telling nifi-registry to shutdown before explicitly killing the Process
+graceful.shutdown.seconds=20
+
+# Disable JSR 199 so that we can use JSP's without running a JDK
+java.arg.1=-Dorg.apache.jasper.compiler.disablejsr199=true
+
+# JVM memory settings
+java.arg.2=-Xms512m
+java.arg.3=-Xmx512m
+
+# Enable Remote Debugging
+#java.arg.debug=-agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=8000
+
+java.arg.4=-Djava.net.preferIPv4Stack=true
+
+# allowRestrictedHeaders is required for Cluster/Node communications to work properly
+java.arg.5=-Dsun.net.http.allowRestrictedHeaders=true
+java.arg.6=-Djava.protocol.handler.pkgs=sun.net.www.protocol
+
+# Java 7 and below have issues with Code Cache. The following lines allow us to run well even with
+# many classes loaded in the JVM.
+#java.arg.7=-XX:ReservedCodeCacheSize=256m
+#java.arg.8=-XX:CodeCacheFlushingMinimumFreeSpace=10m
+#java.arg.9=-XX:+UseCodeCacheFlushing
+#java.arg.11=-XX:PermSize=128M
+#java.arg.12=-XX:MaxPermSize=128M
+
+# The G1GC is still considered experimental but has proven to be very advantageous in providing great
+# performance without significant "stop-the-world" delays.
+#java.arg.10=-XX:+UseG1GC
+
+# Master key in hexadecimal format for encrypted sensitive configuration values
+# nifi.registry.bootstrap.sensitive.key is intentionally absent from this file
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/6f26290d/nifi-registry-core/nifi-registry-properties/src/test/resources/conf/nifi-registry.properties
----------------------------------------------------------------------
diff --git a/nifi-registry-core/nifi-registry-properties/src/test/resources/conf/nifi-registry.properties b/nifi-registry-core/nifi-registry-properties/src/test/resources/conf/nifi-registry.properties
new file mode 100644
index 0000000..a7efedb
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-properties/src/test/resources/conf/nifi-registry.properties
@@ -0,0 +1,45 @@
+#
+# 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.
+#
+
+# web properties #
+nifi.registry.web.war.directory=./target/lib
+nifi.registry.web.http.host=
+nifi.registry.web.http.port=8080
+nifi.registry.web.https.host=
+nifi.registry.web.https.port=
+nifi.registry.web.jetty.working.directory=./target/work/jetty
+nifi.registry.web.jetty.threads=1
+
+# security properties #
+nifi.registry.security.keystore=
+nifi.registry.security.keystoreType=
+nifi.registry.security.keystorePasswd=
+nifi.registry.security.keyPasswd=
+nifi.registry.security.truststore=
+nifi.registry.security.truststoreType=
+nifi.registry.security.truststorePasswd=
+nifi.registry.security.needClientAuth=
+nifi.registry.security.authorizers.configuration.file=
+nifi.registry.security.authorizer=
+nifi.registry.security.identity.providers.configuration.file=
+nifi.registry.security.identity.provider=
+
+# kerberos properties
+nifi.registry.kerberos.krb5.file=/path/to/krb5.conf
+nifi.registry.kerberos.spnego.authentication.expiration=12 hours
+nifi.registry.kerberos.spnego.principal=HTTP/localhost@LOCALHOST
+nifi.registry.kerberos.spnego.keytab.location=/path/to/keytab

http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/6f26290d/nifi-registry-core/nifi-registry-properties/src/test/resources/conf/nifi-registry.with_additional_sensitive_keys.properties
----------------------------------------------------------------------
diff --git a/nifi-registry-core/nifi-registry-properties/src/test/resources/conf/nifi-registry.with_additional_sensitive_keys.properties b/nifi-registry-core/nifi-registry-properties/src/test/resources/conf/nifi-registry.with_additional_sensitive_keys.properties
new file mode 100644
index 0000000..5afb3dd
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-properties/src/test/resources/conf/nifi-registry.with_additional_sensitive_keys.properties
@@ -0,0 +1,55 @@
+#
+# 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.
+#
+
+# web properties #
+nifi.registry.web.war.directory=./target/lib
+nifi.registry.web.http.host=
+nifi.registry.web.http.port=8080
+nifi.registry.web.https.host=
+nifi.registry.web.https.port=
+nifi.registry.web.jetty.working.directory=./target/work/jetty
+nifi.registry.web.jetty.threads=1
+
+# security properties #
+nifi.registry.security.keystore=
+nifi.registry.security.keystoreType=
+nifi.registry.security.keystorePasswd=
+nifi.registry.security.keyPasswd=
+nifi.registry.security.truststore=
+nifi.registry.security.truststoreType=
+nifi.registry.security.truststorePasswd=
+nifi.registry.security.needClientAuth=
+nifi.registry.security.authorizers.configuration.file=
+nifi.registry.security.authorizer=
+nifi.registry.security.identity.providers.configuration.file=
+nifi.registry.security.identity.provider=
+
+# providers properties #
+nifi.registry.providers.configuration.file=
+
+# database properties
+nifi.registry.db.directory=./target/db
+nifi.registry.db.url.append=
+
+# kerberos properties #
+nifi.registry.kerberos.krb5.file=/path/to/krb5.conf
+nifi.registry.kerberos.spnego.authentication.expiration=12 hours
+nifi.registry.kerberos.spnego.principal=HTTP/localhost@LOCALHOST
+nifi.registry.kerberos.spnego.keytab.location=/path/to/keytab
+
+# security properties #
+nifi.registry.sensitive.props.additional.keys=nifi.registry.web.http.port, nifi.registry.web.http.host, nifi.registry.sensitive.props.additional.keys

http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/6f26290d/nifi-registry-core/nifi-registry-properties/src/test/resources/conf/nifi-registry.with_sensitive_props_fully_protected_aes_128.properties
----------------------------------------------------------------------
diff --git a/nifi-registry-core/nifi-registry-properties/src/test/resources/conf/nifi-registry.with_sensitive_props_fully_protected_aes_128.properties b/nifi-registry-core/nifi-registry-properties/src/test/resources/conf/nifi-registry.with_sensitive_props_fully_protected_aes_128.properties
new file mode 100644
index 0000000..90eb64f
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-properties/src/test/resources/conf/nifi-registry.with_sensitive_props_fully_protected_aes_128.properties
@@ -0,0 +1,43 @@
+#
+# 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.
+#
+
+# web properties #
+nifi.registry.web.war.directory=./target/lib
+nifi.registry.web.http.host=
+nifi.registry.web.http.port=8080
+nifi.registry.web.https.host=
+nifi.registry.web.https.port=
+nifi.registry.web.jetty.working.directory=./target/work/jetty
+nifi.registry.web.jetty.threads=1
+
+# security properties #
+nifi.registry.security.keystore=/path/to/keystore.jks
+nifi.registry.security.keystoreType=JKS
+nifi.registry.security.keystorePasswd=6WUpex+VZiN05LXu||joWJMuoSzYniEC7IAoingTimlG7+RGk8I2irl/WTlIuMcg
+nifi.registry.security.keystorePasswd.protected=aes/gcm/128
+nifi.registry.security.keyPasswd=6WUpex+VZiN05LXu||joWJMuoSzYniEC7IAoingTimlG7+RGk8I2irl/WTlIuMcg
+nifi.registry.security.keyPasswd.protected=aes/gcm/128
+nifi.registry.security.truststore=
+nifi.registry.security.truststoreType=
+nifi.registry.security.truststorePasswd=
+nifi.registry.security.needClientAuth=
+nifi.registry.security.authorizers.configuration.file=
+nifi.registry.security.authorizer=
+nifi.registry.security.identity.providers.configuration.file=
+nifi.registry.security.identity.provider=
+
+nifi.registry.sensitive.props.additional.keys=

http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/6f26290d/nifi-registry-core/nifi-registry-properties/src/test/resources/conf/nifi-registry.with_sensitive_props_protected_aes_128.properties
----------------------------------------------------------------------
diff --git a/nifi-registry-core/nifi-registry-properties/src/test/resources/conf/nifi-registry.with_sensitive_props_protected_aes_128.properties b/nifi-registry-core/nifi-registry-properties/src/test/resources/conf/nifi-registry.with_sensitive_props_protected_aes_128.properties
new file mode 100644
index 0000000..6ecd281
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-properties/src/test/resources/conf/nifi-registry.with_sensitive_props_protected_aes_128.properties
@@ -0,0 +1,43 @@
+#
+# 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.
+#
+
+# web properties #
+nifi.registry.web.war.directory=./target/lib
+nifi.registry.web.http.host=
+nifi.registry.web.http.port=8080
+nifi.registry.web.https.host=
+nifi.registry.web.https.port=
+nifi.registry.web.jetty.working.directory=./target/work/jetty
+nifi.registry.web.jetty.threads=1
+
+# security properties #
+nifi.registry.security.keystore=/path/to/keystore.jks
+nifi.registry.security.keystoreType=JKS
+nifi.registry.security.keystorePasswd=6WUpex+VZiN05LXu||joWJMuoSzYniEC7IAoingTimlG7+RGk8I2irl/WTlIuMcg
+nifi.registry.security.keystorePasswd.protected=aes/gcm/128
+nifi.registry.security.keyPasswd=6WUpex+VZiN05LXu||joWJMuoSzYniEC7IAoingTimlG7+RGk8I2irl/WTlIuMcg
+nifi.registry.security.keyPasswd.protected=aes/gcm/128
+nifi.registry.security.truststore=
+nifi.registry.security.truststoreType=
+nifi.registry.security.truststorePasswd=
+nifi.registry.security.needClientAuth=
+nifi.registry.security.authorizers.configuration.file=
+nifi.registry.security.authorizer=
+nifi.registry.security.identity.providers.configuration.file=
+nifi.registry.security.identity.provider=
+
+nifi.registry.sensitive.props.additional.keys=nifi.registry.web.http.port

http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/6f26290d/nifi-registry-core/nifi-registry-properties/src/test/resources/conf/nifi-registry.with_sensitive_props_protected_aes_128_password.properties
----------------------------------------------------------------------
diff --git a/nifi-registry-core/nifi-registry-properties/src/test/resources/conf/nifi-registry.with_sensitive_props_protected_aes_128_password.properties b/nifi-registry-core/nifi-registry-properties/src/test/resources/conf/nifi-registry.with_sensitive_props_protected_aes_128_password.properties
new file mode 100644
index 0000000..a3b272d
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-properties/src/test/resources/conf/nifi-registry.with_sensitive_props_protected_aes_128_password.properties
@@ -0,0 +1,43 @@
+#
+# 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.
+#
+
+# web properties #
+nifi.registry.web.war.directory=./target/lib
+nifi.registry.web.http.host=
+nifi.registry.web.http.port=8080
+nifi.registry.web.https.host=
+nifi.registry.web.https.port=
+nifi.registry.web.jetty.working.directory=./target/work/jetty
+nifi.registry.web.jetty.threads=1
+
+# security properties #
+nifi.registry.security.keystore=/path/to/keystore.jks
+nifi.registry.security.keystoreType=JKS
+nifi.registry.security.keystorePasswd=oa6Aaz5tlFprPuKt||IlVgftF2VqvBIambkP5HVDbRoyKzZl8wwKSw4O9tjHTALA
+nifi.registry.security.keystorePasswd.protected=aes/gcm/128
+nifi.registry.security.keyPasswd=oa6Aaz5tlFprPuKt||IlVgftF2VqvBIambkP5HVDbRoyKzZl8wwKSw4O9tjHTALA
+nifi.registry.security.keyPasswd.protected=aes/gcm/128
+nifi.registry.security.truststore=
+nifi.registry.security.truststoreType=
+nifi.registry.security.truststorePasswd=
+nifi.registry.security.needClientAuth=
+nifi.registry.security.authorizers.configuration.file=
+nifi.registry.security.authorizer=
+nifi.registry.security.identity.providers.configuration.file=
+nifi.registry.security.identity.provider=
+
+nifi.registry.sensitive.props.additional.keys=nifi.registry.web.http.port

http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/6f26290d/nifi-registry-core/nifi-registry-properties/src/test/resources/conf/nifi-registry.with_sensitive_props_protected_aes_256.properties
----------------------------------------------------------------------
diff --git a/nifi-registry-core/nifi-registry-properties/src/test/resources/conf/nifi-registry.with_sensitive_props_protected_aes_256.properties b/nifi-registry-core/nifi-registry-properties/src/test/resources/conf/nifi-registry.with_sensitive_props_protected_aes_256.properties
new file mode 100644
index 0000000..97aaba0
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-properties/src/test/resources/conf/nifi-registry.with_sensitive_props_protected_aes_256.properties
@@ -0,0 +1,43 @@
+#
+# 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.
+#
+
+# web properties #
+nifi.registry.web.war.directory=./target/lib
+nifi.registry.web.http.host=
+nifi.registry.web.http.port=8080
+nifi.registry.web.https.host=
+nifi.registry.web.https.port=
+nifi.registry.web.jetty.working.directory=./target/work/jetty
+nifi.registry.web.jetty.threads=1
+
+# security properties #
+nifi.registry.security.keystore=/path/to/keystore.jks
+nifi.registry.security.keystoreType=JKS
+nifi.registry.security.keystorePasswd=oBjT92hIGRElIGOh||MZ6uYuWNBrOA6usq/Jt3DaD2e4otNirZDytac/w/KFe0HOkrJR03vcbo
+nifi.registry.security.keystorePasswd.protected=aes/gcm/256
+nifi.registry.security.keyPasswd=ac/BaE35SL/esLiJ||+ULRvRLYdIDA2VqpE0eQXDEMjaLBMG2kbKOdOwBk/hGebDKlVg==
+nifi.registry.security.keyPasswd.protected=aes/gcm/256
+nifi.registry.security.truststore=
+nifi.registry.security.truststoreType=
+nifi.registry.security.truststorePasswd=
+nifi.registry.security.needClientAuth=
+nifi.registry.security.authorizers.configuration.file=
+nifi.registry.security.authorizer=
+nifi.registry.security.identity.providers.configuration.file=
+nifi.registry.security.identity.provider=
+
+nifi.registry.sensitive.props.additional.keys=nifi.registry.web.http.port

http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/6f26290d/nifi-registry-core/nifi-registry-properties/src/test/resources/conf/nifi-registry.with_sensitive_props_protected_aes_multiple_malformed.properties
----------------------------------------------------------------------
diff --git a/nifi-registry-core/nifi-registry-properties/src/test/resources/conf/nifi-registry.with_sensitive_props_protected_aes_multiple_malformed.properties b/nifi-registry-core/nifi-registry-properties/src/test/resources/conf/nifi-registry.with_sensitive_props_protected_aes_multiple_malformed.properties
new file mode 100644
index 0000000..d408df0
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-properties/src/test/resources/conf/nifi-registry.with_sensitive_props_protected_aes_multiple_malformed.properties
@@ -0,0 +1,43 @@
+#
+# 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.
+#
+
+# web properties #
+nifi.registry.web.war.directory=./target/lib
+nifi.registry.web.http.host=
+nifi.registry.web.http.port=8080
+nifi.registry.web.https.host=
+nifi.registry.web.https.port=
+nifi.registry.web.jetty.working.directory=./target/work/jetty
+nifi.registry.web.jetty.threads=1
+
+# security properties #
+nifi.registry.security.keystore=/path/to/keystore.jks
+nifi.registry.security.keystoreType=JKS
+nifi.registry.security.keystorePasswd=6WUpex+VZiN05LXu||thisIsAnIntentionallyMalformedCipherValue
+nifi.registry.security.keystorePasswd.protected=aes/gcm/128
+nifi.registry.security.keyPasswd=6WUpex+VZiN05LXu||thisIsAnIntentionallyMalformedCipherValue
+nifi.registry.security.keyPasswd.protected=aes/gcm/128
+nifi.registry.security.truststore=
+nifi.registry.security.truststoreType=
+nifi.registry.security.truststorePasswd=
+nifi.registry.security.needClientAuth=
+nifi.registry.security.authorizers.configuration.file=
+nifi.registry.security.authorizer=
+nifi.registry.security.identity.providers.configuration.file=
+nifi.registry.security.identity.provider=
+
+nifi.registry.sensitive.props.additional.keys=

http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/6f26290d/nifi-registry-core/nifi-registry-properties/src/test/resources/conf/nifi-registry.with_sensitive_props_protected_aes_single_malformed.properties
----------------------------------------------------------------------
diff --git a/nifi-registry-core/nifi-registry-properties/src/test/resources/conf/nifi-registry.with_sensitive_props_protected_aes_single_malformed.properties b/nifi-registry-core/nifi-registry-properties/src/test/resources/conf/nifi-registry.with_sensitive_props_protected_aes_single_malformed.properties
new file mode 100644
index 0000000..8552f9e
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-properties/src/test/resources/conf/nifi-registry.with_sensitive_props_protected_aes_single_malformed.properties
@@ -0,0 +1,43 @@
+#
+# 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.
+#
+
+# web properties #
+nifi.registry.web.war.directory=./target/lib
+nifi.registry.web.http.host=
+nifi.registry.web.http.port=8080
+nifi.registry.web.https.host=
+nifi.registry.web.https.port=
+nifi.registry.web.jetty.working.directory=./target/work/jetty
+nifi.registry.web.jetty.threads=1
+
+# security properties #
+nifi.registry.security.keystore=/path/to/keystore.jks
+nifi.registry.security.keystoreType=JKS
+nifi.registry.security.keystorePasswd=6WUpex+VZiN05LXu||thisIsAnIntentionallyMalformedCipherValue
+nifi.registry.security.keystorePasswd.protected=aes/gcm/128
+nifi.registry.security.keyPasswd=6WUpex+VZiN05LXu||joWJMuoSzYniEC7IAoingTimlG7+RGk8I2irl/WTlIuMcg
+nifi.registry.security.keyPasswd.protected=aes/gcm/128
+nifi.registry.security.truststore=
+nifi.registry.security.truststoreType=
+nifi.registry.security.truststorePasswd=
+nifi.registry.security.needClientAuth=
+nifi.registry.security.authorizers.configuration.file=
+nifi.registry.security.authorizer=
+nifi.registry.security.identity.providers.configuration.file=
+nifi.registry.security.identity.provider=
+
+nifi.registry.sensitive.props.additional.keys=

http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/6f26290d/nifi-registry-core/nifi-registry-properties/src/test/resources/conf/nifi-registry.with_sensitive_props_protected_unknown.properties
----------------------------------------------------------------------
diff --git a/nifi-registry-core/nifi-registry-properties/src/test/resources/conf/nifi-registry.with_sensitive_props_protected_unknown.properties b/nifi-registry-core/nifi-registry-properties/src/test/resources/conf/nifi-registry.with_sensitive_props_protected_unknown.properties
new file mode 100644
index 0000000..8bd6f4f
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-properties/src/test/resources/conf/nifi-registry.with_sensitive_props_protected_unknown.properties
@@ -0,0 +1,43 @@
+#
+# 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.
+#
+
+# web properties #
+nifi.registry.web.war.directory=./target/lib
+nifi.registry.web.http.host=
+nifi.registry.web.http.port=8080
+nifi.registry.web.https.host=
+nifi.registry.web.https.port=
+nifi.registry.web.jetty.working.directory=./target/work/jetty
+nifi.registry.web.jetty.threads=1
+
+# security properties #
+nifi.registry.security.keystore=/path/to/keystore.jks
+nifi.registry.security.keystoreType=JKS
+nifi.registry.security.keystorePasswd=oBjT92hIGRElIGOh||MZ6uYuWNBrOA6usq/Jt3DaD2e4otNirZDytac/w/KFe0HOkrJR03vcbo
+nifi.registry.security.keystorePasswd.protected=unknown
+nifi.registry.security.keyPasswd=ac/BaE35SL/esLiJ||+ULRvRLYdIDA2VqpE0eQXDEMjaLBMG2kbKOdOwBk/hGebDKlVg==
+nifi.registry.security.keyPasswd.protected=unknown
+nifi.registry.security.truststore=
+nifi.registry.security.truststoreType=
+nifi.registry.security.truststorePasswd=
+nifi.registry.security.needClientAuth=
+nifi.registry.security.authorizers.configuration.file=
+nifi.registry.security.authorizer=
+nifi.registry.security.identity.providers.configuration.file=
+nifi.registry.security.identity.provider=
+
+nifi.registry.sensitive.props.additional.keys=nifi.registry.web.http.port, nifi.registry.web.http.host

http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/6f26290d/nifi-registry-core/nifi-registry-properties/src/test/resources/conf/nifi-registry.with_sensitive_props_unprotected.properties
----------------------------------------------------------------------
diff --git a/nifi-registry-core/nifi-registry-properties/src/test/resources/conf/nifi-registry.with_sensitive_props_unprotected.properties b/nifi-registry-core/nifi-registry-properties/src/test/resources/conf/nifi-registry.with_sensitive_props_unprotected.properties
new file mode 100644
index 0000000..b0f9f40
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-properties/src/test/resources/conf/nifi-registry.with_sensitive_props_unprotected.properties
@@ -0,0 +1,41 @@
+#
+# 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.
+#
+
+# web properties #
+nifi.registry.web.war.directory=./target/lib
+nifi.registry.web.http.host=
+nifi.registry.web.http.port=8080
+nifi.registry.web.https.host=
+nifi.registry.web.https.port=
+nifi.registry.web.jetty.working.directory=./target/work/jetty
+nifi.registry.web.jetty.threads=1
+
+# security properties #
+nifi.registry.security.keystore=path/to/keystore.jks
+nifi.registry.security.keystoreType=JKS
+nifi.registry.security.keystorePasswd=thisIsABadKeystorePassword
+nifi.registry.security.keyPasswd=thisIsABadKeyPassword
+nifi.registry.security.truststore=
+nifi.registry.security.truststoreType=
+nifi.registry.security.truststorePasswd=
+nifi.registry.security.needClientAuth=
+nifi.registry.security.authorizers.configuration.file=
+nifi.registry.security.authorizer=
+nifi.registry.security.identity.providers.configuration.file=
+nifi.registry.security.identity.provider=
+
+nifi.registry.sensitive.props.additional.keys=nifi.registry.web.http.port, nifi.registry.web.http.host

http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/6f26290d/nifi-registry-core/nifi-registry-properties/src/test/resources/conf/nifi-registry.with_sensitive_props_unprotected_extra_line.properties
----------------------------------------------------------------------
diff --git a/nifi-registry-core/nifi-registry-properties/src/test/resources/conf/nifi-registry.with_sensitive_props_unprotected_extra_line.properties b/nifi-registry-core/nifi-registry-properties/src/test/resources/conf/nifi-registry.with_sensitive_props_unprotected_extra_line.properties
new file mode 100644
index 0000000..34b80a3
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-properties/src/test/resources/conf/nifi-registry.with_sensitive_props_unprotected_extra_line.properties
@@ -0,0 +1,42 @@
+#
+# 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.
+#
+
+# web properties #
+nifi.registry.web.war.directory=./target/lib
+nifi.registry.web.http.host=
+nifi.registry.web.http.port=8080
+nifi.registry.web.https.host=
+nifi.registry.web.https.port=
+nifi.registry.web.jetty.working.directory=./target/work/jetty
+nifi.registry.web.jetty.threads=1
+
+# security properties #
+nifi.registry.security.keystore=path/to/keystore.jks
+nifi.registry.security.keystoreType=JKS
+nifi.registry.security.keystorePasswd=thisIsABadKeystorePassword
+nifi.registry.security.keyPasswd=thisIsABadKeyPassword
+nifi.registry.security.keyPasswd.protected=
+nifi.registry.security.truststore=
+nifi.registry.security.truststoreType=
+nifi.registry.security.truststorePasswd=
+nifi.registry.security.needClientAuth=
+nifi.registry.security.authorizers.configuration.file=
+nifi.registry.security.authorizer=
+nifi.registry.security.identity.providers.configuration.file=
+nifi.registry.security.identity.provider=
+
+nifi.registry.sensitive.props.additional.keys=nifi.registry.web.http.port, nifi.registry.web.http.host

http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/6f26290d/nifi-registry-core/nifi-registry-provider-api/pom.xml
----------------------------------------------------------------------
diff --git a/nifi-registry-core/nifi-registry-provider-api/pom.xml b/nifi-registry-core/nifi-registry-provider-api/pom.xml
new file mode 100644
index 0000000..44dd56d
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-provider-api/pom.xml
@@ -0,0 +1,28 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+  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.
+-->
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+    <parent>
+        <groupId>org.apache.nifi.registry</groupId>
+        <artifactId>nifi-registry-core</artifactId>
+        <version>0.3.0-SNAPSHOT</version>
+    </parent>
+    <modelVersion>4.0.0</modelVersion>
+    <artifactId>nifi-registry-provider-api</artifactId>
+    <packaging>jar</packaging>
+
+    <dependencies>
+    </dependencies>
+</project>

http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/6f26290d/nifi-registry-core/nifi-registry-provider-api/src/main/java/org/apache/nifi/registry/flow/FlowPersistenceException.java
----------------------------------------------------------------------
diff --git a/nifi-registry-core/nifi-registry-provider-api/src/main/java/org/apache/nifi/registry/flow/FlowPersistenceException.java b/nifi-registry-core/nifi-registry-provider-api/src/main/java/org/apache/nifi/registry/flow/FlowPersistenceException.java
new file mode 100644
index 0000000..4287fc8
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-provider-api/src/main/java/org/apache/nifi/registry/flow/FlowPersistenceException.java
@@ -0,0 +1,31 @@
+/*
+ * 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.flow;
+
+/**
+ * An Exception for errors encountered when a FlowPersistenceProvider saves or retrieves a flow.
+ */
+public class FlowPersistenceException extends RuntimeException {
+
+    public FlowPersistenceException(String message) {
+        super(message);
+    }
+
+    public FlowPersistenceException(String message, Throwable cause) {
+        super(message, cause);
+    }
+}

http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/6f26290d/nifi-registry-core/nifi-registry-provider-api/src/main/java/org/apache/nifi/registry/flow/FlowPersistenceProvider.java
----------------------------------------------------------------------
diff --git a/nifi-registry-core/nifi-registry-provider-api/src/main/java/org/apache/nifi/registry/flow/FlowPersistenceProvider.java b/nifi-registry-core/nifi-registry-provider-api/src/main/java/org/apache/nifi/registry/flow/FlowPersistenceProvider.java
new file mode 100644
index 0000000..90c872f
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-provider-api/src/main/java/org/apache/nifi/registry/flow/FlowPersistenceProvider.java
@@ -0,0 +1,71 @@
+/*
+ * 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.flow;
+
+import org.apache.nifi.registry.provider.Provider;
+
+/**
+ * A service that can store and retrieve flow contents.
+ *
+ * The flow contents will be a serialized VersionProcessGroup which came from the flowContents
+ * field of a VersionedFlowSnapshot.
+ *
+ * NOTE: Although this interface is intended to be an extension point, it is not yet considered stable and thus may
+ * change across releases until the registry matures.
+ */
+public interface FlowPersistenceProvider extends Provider {
+
+    /**
+     * Persists the serialized content.
+     *
+     * @param context the context for the content being persisted
+     * @param content the serialized flow content to persist
+     * @throws FlowPersistenceException if the content could not be persisted
+     */
+    void saveFlowContent(FlowSnapshotContext context, byte[] content) throws FlowPersistenceException;
+
+    /**
+     * Retrieves the serialized content.
+     *
+     * @param bucketId the bucket id where the flow snapshot is located
+     * @param flowId the id of the versioned flow the snapshot belongs to
+     * @param version the version of the snapshot
+     * @return the bytes for the requested snapshot, or null if not found
+     * @throws FlowPersistenceException if the snapshot could not be retrieved due to an error in underlying provider
+     */
+    byte[] getFlowContent(String bucketId, String flowId, int version) throws FlowPersistenceException;
+
+    /**
+     * Deletes all content for the versioned flow with the given id in the given bucket.
+     *
+     * @param bucketId the bucket the versioned flow belongs to
+     * @param flowId the id of the versioned flow
+     * @throws FlowPersistenceException if the snapshots could not be deleted due to an error in underlying provider
+     */
+    void deleteAllFlowContent(String bucketId, String flowId) throws FlowPersistenceException;
+
+    /**
+     * Deletes the content for the given snapshot.
+     *
+     * @param bucketId the bucket id where the snapshot is located
+     * @param flowId the id of the versioned flow the snapshot belongs to
+     * @param version the version of the snapshot
+     * @throws FlowPersistenceException if the snapshot could not be deleted due to an error in underlying provider
+     */
+    void deleteFlowContent(String bucketId, String flowId, int version) throws FlowPersistenceException;
+
+}

http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/6f26290d/nifi-registry-core/nifi-registry-provider-api/src/main/java/org/apache/nifi/registry/flow/FlowSnapshotContext.java
----------------------------------------------------------------------
diff --git a/nifi-registry-core/nifi-registry-provider-api/src/main/java/org/apache/nifi/registry/flow/FlowSnapshotContext.java b/nifi-registry-core/nifi-registry-provider-api/src/main/java/org/apache/nifi/registry/flow/FlowSnapshotContext.java
new file mode 100644
index 0000000..9c2a818
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-provider-api/src/main/java/org/apache/nifi/registry/flow/FlowSnapshotContext.java
@@ -0,0 +1,64 @@
+/*
+ * 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.flow;
+
+/**
+ * The context that will be passed to the flow provider when saving a snapshot of a versioned flow.
+ */
+public interface FlowSnapshotContext {
+
+    /**
+     * @return the id of the bucket this snapshot belongs to
+     */
+    String getBucketId();
+
+    /**
+     * @return the name of the bucket this snapshot belongs to
+     */
+    String getBucketName();
+
+    /**
+     * @return the id of the versioned flow this snapshot belongs to
+     */
+    String getFlowId();
+
+    /**
+     * @return the name of the versioned flow this snapshot belongs to
+     */
+    String getFlowName();
+
+    /**
+     * @return the version of the snapshot
+     */
+    int getVersion();
+
+    /**
+     * @return the comments for the snapshot
+     */
+    String getComments();
+
+    /**
+     * @return the timestamp the snapshot was created
+     */
+    long getSnapshotTimestamp();
+
+    /**
+     * @return the author of the snapshot
+     */
+    String getAuthor();
+
+}

http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/6f26290d/nifi-registry-core/nifi-registry-provider-api/src/main/java/org/apache/nifi/registry/hook/Event.java
----------------------------------------------------------------------
diff --git a/nifi-registry-core/nifi-registry-provider-api/src/main/java/org/apache/nifi/registry/hook/Event.java b/nifi-registry-core/nifi-registry-provider-api/src/main/java/org/apache/nifi/registry/hook/Event.java
new file mode 100644
index 0000000..2948962
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-provider-api/src/main/java/org/apache/nifi/registry/hook/Event.java
@@ -0,0 +1,50 @@
+/*
+ * 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.hook;
+
+import java.util.List;
+
+/**
+ * An event that will be passed to EventHookProviders.
+ */
+public interface Event {
+
+    /**
+     * @return the type of the event
+     */
+    EventType getEventType();
+
+    /**
+     * @return the fields of the event in the order they were added to the event
+     */
+    List<EventField> getFields();
+
+    /**
+     * @param fieldName the name of the field to return
+     * @return the EventField with the given name, or null if it does not exist
+     */
+    EventField getField(EventFieldName fieldName);
+
+    /**
+     * Will be called before publishing the event to ensure the event contains the required
+     * fields for the given event type in the order specified by the type.
+     *
+     * @throws IllegalStateException if the event does not contain the required fields
+     */
+    void validate() throws IllegalStateException;
+
+}

http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/6f26290d/nifi-registry-core/nifi-registry-provider-api/src/main/java/org/apache/nifi/registry/hook/EventField.java
----------------------------------------------------------------------
diff --git a/nifi-registry-core/nifi-registry-provider-api/src/main/java/org/apache/nifi/registry/hook/EventField.java b/nifi-registry-core/nifi-registry-provider-api/src/main/java/org/apache/nifi/registry/hook/EventField.java
new file mode 100644
index 0000000..4859266
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-provider-api/src/main/java/org/apache/nifi/registry/hook/EventField.java
@@ -0,0 +1,33 @@
+/*
+ * 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.hook;
+
+/**
+ * A field for an event.
+ */
+public interface EventField {
+
+    /**
+     * @return the name of the field
+     */
+    EventFieldName getName();
+
+    /**
+     * @return the value of the field
+     */
+    String getValue();
+}

http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/6f26290d/nifi-registry-core/nifi-registry-provider-api/src/main/java/org/apache/nifi/registry/hook/EventFieldName.java
----------------------------------------------------------------------
diff --git a/nifi-registry-core/nifi-registry-provider-api/src/main/java/org/apache/nifi/registry/hook/EventFieldName.java b/nifi-registry-core/nifi-registry-provider-api/src/main/java/org/apache/nifi/registry/hook/EventFieldName.java
new file mode 100644
index 0000000..35b0cfe
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-provider-api/src/main/java/org/apache/nifi/registry/hook/EventFieldName.java
@@ -0,0 +1,30 @@
+/*
+ * 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.hook;
+
+/**
+ * Enumeration of possible field names for an EventField.
+ */
+public enum EventFieldName {
+
+    BUCKET_ID,
+    FLOW_ID,
+    VERSION,
+    USER,
+    COMMENT;
+
+}

http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/6f26290d/nifi-registry-core/nifi-registry-provider-api/src/main/java/org/apache/nifi/registry/hook/EventHookException.java
----------------------------------------------------------------------
diff --git a/nifi-registry-core/nifi-registry-provider-api/src/main/java/org/apache/nifi/registry/hook/EventHookException.java b/nifi-registry-core/nifi-registry-provider-api/src/main/java/org/apache/nifi/registry/hook/EventHookException.java
new file mode 100644
index 0000000..2d91735
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-provider-api/src/main/java/org/apache/nifi/registry/hook/EventHookException.java
@@ -0,0 +1,31 @@
+/*
+ * 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.hook;
+
+/**
+ * An Exception for errors encountered when a EventHookProvider executes an action before/after a commit.
+ */
+public class EventHookException extends RuntimeException {
+
+    public EventHookException(String message) {
+        super(message);
+    }
+
+    public EventHookException(String message, Throwable cause) {
+        super(message, cause);
+    }
+}

http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/6f26290d/nifi-registry-core/nifi-registry-provider-api/src/main/java/org/apache/nifi/registry/hook/EventHookProvider.java
----------------------------------------------------------------------
diff --git a/nifi-registry-core/nifi-registry-provider-api/src/main/java/org/apache/nifi/registry/hook/EventHookProvider.java b/nifi-registry-core/nifi-registry-provider-api/src/main/java/org/apache/nifi/registry/hook/EventHookProvider.java
new file mode 100644
index 0000000..8d9f51c
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-provider-api/src/main/java/org/apache/nifi/registry/hook/EventHookProvider.java
@@ -0,0 +1,53 @@
+/*
+ * 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.hook;
+
+import org.apache.nifi.registry.provider.Provider;
+
+/**
+ * An extension point that will be passed events produced by actions take in the registry.
+ *
+ * The list of event types can be found in {@link org.apache.nifi.registry.hook.EventType}.
+ *
+ * NOTE: Although this interface is intended to be an extension point, it is not yet considered stable and thus may
+ * change across releases until the registry matures.
+ */
+public interface EventHookProvider extends Provider {
+
+    /**
+     * Handles the given event.
+     *
+     * @param event the event to handle
+     * @throws EventHookException if an error occurs handling the event
+     */
+    void handle(Event event) throws EventHookException;
+
+    /**
+     * Examines the values from the 'Whitelisted Event Type ' properties in the hook provider definition to determine
+     * if the Event should be invoked for this particular EventType
+     *
+     * @param eventType
+     *  EventType that has been fired by the framework.
+     *
+     * @return
+     *  True if the hook provider should be 'handled' and false otherwise.
+     */
+    default boolean shouldHandle(EventType eventType) {
+        return true;
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/6f26290d/nifi-registry-core/nifi-registry-provider-api/src/main/java/org/apache/nifi/registry/hook/EventType.java
----------------------------------------------------------------------
diff --git a/nifi-registry-core/nifi-registry-provider-api/src/main/java/org/apache/nifi/registry/hook/EventType.java b/nifi-registry-core/nifi-registry-provider-api/src/main/java/org/apache/nifi/registry/hook/EventType.java
new file mode 100644
index 0000000..c11a60c
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-provider-api/src/main/java/org/apache/nifi/registry/hook/EventType.java
@@ -0,0 +1,71 @@
+/*
+ * 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.hook;
+
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+
+/**
+ * Enumeration of possible EventTypes with the expected fields for each event.
+ *
+ * Producers of events must produce events with the fields in the same order specified here.
+ */
+public enum EventType {
+
+    CREATE_BUCKET(
+            EventFieldName.BUCKET_ID,
+            EventFieldName.USER),
+    CREATE_FLOW(
+            EventFieldName.BUCKET_ID,
+            EventFieldName.FLOW_ID,
+            EventFieldName.USER),
+    CREATE_FLOW_VERSION(
+            EventFieldName.BUCKET_ID,
+            EventFieldName.FLOW_ID,
+            EventFieldName.VERSION,
+            EventFieldName.USER,
+            EventFieldName.COMMENT),
+    REGISTRY_START(),
+    UPDATE_BUCKET(
+            EventFieldName.BUCKET_ID,
+            EventFieldName.USER),
+    UPDATE_FLOW(
+            EventFieldName.BUCKET_ID,
+            EventFieldName.FLOW_ID,
+            EventFieldName.USER),
+    DELETE_BUCKET(
+            EventFieldName.BUCKET_ID,
+            EventFieldName.USER),
+    DELETE_FLOW(
+            EventFieldName.BUCKET_ID,
+            EventFieldName.FLOW_ID,
+            EventFieldName.USER);
+
+
+    private List<EventFieldName> fieldNames;
+
+    EventType(EventFieldName... fieldNames) {
+        this.fieldNames = Collections.unmodifiableList(Arrays.asList(fieldNames));
+    }
+
+    public List<EventFieldName> getFieldNames() {
+        return this.fieldNames;
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/6f26290d/nifi-registry-core/nifi-registry-provider-api/src/main/java/org/apache/nifi/registry/hook/WhitelistFilteringEventHookProvider.java
----------------------------------------------------------------------
diff --git a/nifi-registry-core/nifi-registry-provider-api/src/main/java/org/apache/nifi/registry/hook/WhitelistFilteringEventHookProvider.java b/nifi-registry-core/nifi-registry-provider-api/src/main/java/org/apache/nifi/registry/hook/WhitelistFilteringEventHookProvider.java
new file mode 100644
index 0000000..24ac3a4
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-provider-api/src/main/java/org/apache/nifi/registry/hook/WhitelistFilteringEventHookProvider.java
@@ -0,0 +1,70 @@
+/*
+ * 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.hook;
+
+import org.apache.nifi.registry.provider.ProviderConfigurationContext;
+import org.apache.nifi.registry.provider.ProviderCreationException;
+
+import java.util.HashSet;
+import java.util.Map;
+import java.util.Set;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+public abstract class WhitelistFilteringEventHookProvider
+        implements EventHookProvider {
+
+    static final String EVENT_WHITELIST_PREFIX = "Whitelisted Event Type ";
+    static final Pattern EVENT_WHITELIST_PATTERN = Pattern.compile(EVENT_WHITELIST_PREFIX + "\\S+");
+
+    protected Set<EventType> whiteListEvents = null;
+
+    @Override
+    public void onConfigured(ProviderConfigurationContext configurationContext) throws ProviderCreationException {
+        whiteListEvents = new HashSet<>();
+        for (Map.Entry<String,String> entry : configurationContext.getProperties().entrySet()) {
+            Matcher matcher = EVENT_WHITELIST_PATTERN.matcher(entry.getKey());
+            if (matcher.matches() && (entry.getValue() != null && entry.getValue().length() > 0)) {
+                whiteListEvents.add(EventType.valueOf(entry.getValue()));
+            }
+
+        }
+    }
+
+    /**
+     * Standard method for deciding if the EventType should be handled by the Hook provider or not.
+     *
+     * @param eventType
+     *  EventType that was fired by the framework.
+     *
+     * @return
+     *  True if the EventType is in the whitelist set and false otherwise.
+     */
+    @Override
+    public boolean shouldHandle(EventType eventType) {
+        if (whiteListEvents != null && whiteListEvents.size() > 0) {
+            if (whiteListEvents.contains(eventType)) {
+                return true;
+            }
+        } else {
+            // If the whitelist property is not set or empty we want to fire for all events.
+            return true;
+        }
+        return false;
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/6f26290d/nifi-registry-core/nifi-registry-provider-api/src/main/java/org/apache/nifi/registry/provider/Provider.java
----------------------------------------------------------------------
diff --git a/nifi-registry-core/nifi-registry-provider-api/src/main/java/org/apache/nifi/registry/provider/Provider.java b/nifi-registry-core/nifi-registry-provider-api/src/main/java/org/apache/nifi/registry/provider/Provider.java
new file mode 100644
index 0000000..4a4be28
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-provider-api/src/main/java/org/apache/nifi/registry/provider/Provider.java
@@ -0,0 +1,32 @@
+/*
+ * 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.provider;
+
+/**
+ * Base interface for providers.
+ */
+public interface Provider {
+
+    /**
+     * Called to configure the Provider.
+     *
+     * @param configurationContext the context containing configuration for the given provider
+     * @throws ProviderCreationException if an error occurs while the provider is configured
+     */
+    void onConfigured(ProviderConfigurationContext configurationContext) throws ProviderCreationException;
+
+}

http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/6f26290d/nifi-registry-core/nifi-registry-provider-api/src/main/java/org/apache/nifi/registry/provider/ProviderConfigurationContext.java
----------------------------------------------------------------------
diff --git a/nifi-registry-core/nifi-registry-provider-api/src/main/java/org/apache/nifi/registry/provider/ProviderConfigurationContext.java b/nifi-registry-core/nifi-registry-provider-api/src/main/java/org/apache/nifi/registry/provider/ProviderConfigurationContext.java
new file mode 100644
index 0000000..b4f7ed6
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-provider-api/src/main/java/org/apache/nifi/registry/provider/ProviderConfigurationContext.java
@@ -0,0 +1,36 @@
+/*
+ * 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.provider;
+
+import java.util.Map;
+
+/**
+ * A context that will passed to providers in order to obtain configuration.
+ */
+public interface ProviderConfigurationContext {
+
+    /**
+     * Retrieves all properties the provider currently understands regardless
+     * of whether a value has been set for them or not. If no value is present
+     * then its value is null and thus any registered default for the property
+     * descriptor applies.
+     *
+     * @return Map of all properties
+     */
+    Map<String, String> getProperties();
+
+}

http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/6f26290d/nifi-registry-core/nifi-registry-provider-api/src/main/java/org/apache/nifi/registry/provider/ProviderCreationException.java
----------------------------------------------------------------------
diff --git a/nifi-registry-core/nifi-registry-provider-api/src/main/java/org/apache/nifi/registry/provider/ProviderCreationException.java b/nifi-registry-core/nifi-registry-provider-api/src/main/java/org/apache/nifi/registry/provider/ProviderCreationException.java
new file mode 100644
index 0000000..d1e106c
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-provider-api/src/main/java/org/apache/nifi/registry/provider/ProviderCreationException.java
@@ -0,0 +1,39 @@
+/*
+ * 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.provider;
+
+/**
+ * An exception that will be thrown if a provider can not be created.
+ */
+public class ProviderCreationException extends RuntimeException {
+
+    public ProviderCreationException() {
+    }
+
+    public ProviderCreationException(String message) {
+        super(message);
+    }
+
+    public ProviderCreationException(String message, Throwable cause) {
+        super(message, cause);
+    }
+
+    public ProviderCreationException(Throwable cause) {
+        super(cause);
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/6f26290d/nifi-registry-core/nifi-registry-resources/pom.xml
----------------------------------------------------------------------
diff --git a/nifi-registry-core/nifi-registry-resources/pom.xml b/nifi-registry-core/nifi-registry-resources/pom.xml
new file mode 100644
index 0000000..7ecdf49
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-resources/pom.xml
@@ -0,0 +1,50 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+  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.
+-->
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+    <modelVersion>4.0.0</modelVersion>
+    <parent>
+        <groupId>org.apache.nifi.registry</groupId>
+        <artifactId>nifi-registry-core</artifactId>
+        <version>0.3.0-SNAPSHOT</version>
+    </parent>
+    <artifactId>nifi-registry-resources</artifactId>
+    <packaging>pom</packaging>
+    <description>holds common resources used to build installers</description>
+    <build>
+        <plugins>
+            <plugin>
+                <artifactId>maven-assembly-plugin</artifactId>
+                <configuration>
+                    <attach>true</attach>
+                </configuration>
+                <executions>
+                    <execution>
+                        <id>make shared resource</id>
+                        <goals>
+                            <goal>single</goal>
+                        </goals>
+                        <phase>package</phase>
+                        <configuration>
+                            <descriptors>
+                                <descriptor>src/main/assembly/dependencies.xml</descriptor>
+                            </descriptors>
+                        </configuration>
+                    </execution>
+                </executions>
+            </plugin>
+        </plugins>
+    </build>
+</project>

http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/6f26290d/nifi-registry-core/nifi-registry-resources/src/main/assembly/dependencies.xml
----------------------------------------------------------------------
diff --git a/nifi-registry-core/nifi-registry-resources/src/main/assembly/dependencies.xml b/nifi-registry-core/nifi-registry-resources/src/main/assembly/dependencies.xml
new file mode 100644
index 0000000..fed8098
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-resources/src/main/assembly/dependencies.xml
@@ -0,0 +1,36 @@
+<?xml version="1.0"?>
+<!--
+  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.
+-->
+<assembly>
+    <id>resources</id>
+    <formats>
+        <format>zip</format>
+    </formats>
+    <includeBaseDirectory>false</includeBaseDirectory>
+    <fileSets>
+        <fileSet>
+            <directory>src/main/resources</directory>
+            <outputDirectory>/</outputDirectory>
+        </fileSet>
+        <fileSet>
+            <directory>src/main/resources/bin</directory>
+            <outputDirectory>/bin/</outputDirectory>
+            <includes>
+                <include>nifi-registry.sh</include>
+            </includes>
+            <fileMode>0750</fileMode>
+        </fileSet>
+    </fileSets>
+</assembly>

http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/6f26290d/nifi-registry-core/nifi-registry-resources/src/main/resources/bin/dump-nifi-registry.bat
----------------------------------------------------------------------
diff --git a/nifi-registry-core/nifi-registry-resources/src/main/resources/bin/dump-nifi-registry.bat b/nifi-registry-core/nifi-registry-resources/src/main/resources/bin/dump-nifi-registry.bat
new file mode 100644
index 0000000..9134aab
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-resources/src/main/resources/bin/dump-nifi-registry.bat
@@ -0,0 +1,49 @@
+@echo off
+rem
+rem    Licensed to the Apache Software Foundation (ASF) under one or more
+rem    contributor license agreements.  See the NOTICE file distributed with
+rem    this work for additional information regarding copyright ownership.
+rem    The ASF licenses this file to You under the Apache License, Version 2.0
+rem    (the "License"); you may not use this file except in compliance with
+rem    the License.  You may obtain a copy of the License at
+rem
+rem       http://www.apache.org/licenses/LICENSE-2.0
+rem
+rem    Unless required by applicable law or agreed to in writing, software
+rem    distributed under the License is distributed on an "AS IS" BASIS,
+rem    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+rem    See the License for the specific language governing permissions and
+rem    limitations under the License.
+rem
+
+rem Use JAVA_HOME if it's set; otherwise, just use java
+
+if "%JAVA_HOME%" == "" goto noJavaHome
+if not exist "%JAVA_HOME%\bin\java.exe" goto noJavaHome
+set JAVA_EXE=%JAVA_HOME%\bin\java.exe
+goto startNiFiRegistry
+
+:noJavaHome
+echo The JAVA_HOME environment variable is not defined correctly.
+echo Instead the PATH will be used to find the java executable.
+echo.
+set JAVA_EXE=java
+goto startNiFiRegistry
+
+:startNiFiRegistry
+set NIFI_REGISTRY_ROOT=%~dp0..\
+pushd "%NIFI_REGISTRY%"
+set LIB_DIR=%NIFI_REGISTRY_ROOT%\lib
+set SHARED_DIR=%NIFI_REGISTRY_ROOT%\lib\shared
+set BOOTSTRAP_DIR=%NIFI_REGISTRY_ROOT%\lib\bootstrap
+set CONF_DIR=conf
+
+set BOOTSTRAP_CONF_FILE=%CONF_DIR%\bootstrap.conf
+set JAVA_ARGS=-Dorg.apache.nifi.registry.bootstrap.config.file=%BOOTSTRAP_CONF_FILE%
+
+SET JAVA_PARAMS=-cp %CONF_DIR%;%LIB_DIR%\*;%SHARED_DIR%\*;%BOOTSTRAP_DIR%\* -Xms12m -Xmx24m %JAVA_ARGS% org.apache.nifi.registry.NiFiRegistry
+set BOOTSTRAP_ACTION=dump
+
+cmd.exe /C "%JAVA_EXE%" %JAVA_PARAMS% %BOOTSTRAP_ACTION%
+
+popd

http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/6f26290d/nifi-registry-core/nifi-registry-resources/src/main/resources/bin/nifi-registry-env.sh
----------------------------------------------------------------------
diff --git a/nifi-registry-core/nifi-registry-resources/src/main/resources/bin/nifi-registry-env.sh b/nifi-registry-core/nifi-registry-resources/src/main/resources/bin/nifi-registry-env.sh
new file mode 100644
index 0000000..216d484
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-resources/src/main/resources/bin/nifi-registry-env.sh
@@ -0,0 +1,28 @@
+#!/bin/sh
+#
+#    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.
+#
+
+# The java implementation to use.
+#export JAVA_HOME=/usr/java/jdk1.8.0/
+
+export NIFI_REGISTRY_HOME=$(cd "${SCRIPT_DIR}" && cd .. && pwd)
+
+#The directory for the NiFi Registry pid file
+export NIFI_REGISTRY_PID_DIR="${NIFI_REGISTRY_HOME}/run"
+
+#The directory for NiFi Registry log files
+export NIFI_REGISTRY_LOG_DIR="${NIFI_REGISTRY_HOME}/logs"
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/6f26290d/nifi-registry-core/nifi-registry-resources/src/main/resources/bin/nifi-registry.sh
----------------------------------------------------------------------
diff --git a/nifi-registry-core/nifi-registry-resources/src/main/resources/bin/nifi-registry.sh b/nifi-registry-core/nifi-registry-resources/src/main/resources/bin/nifi-registry.sh
new file mode 100644
index 0000000..aaf16fd
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-resources/src/main/resources/bin/nifi-registry.sh
@@ -0,0 +1,357 @@
+#!/bin/sh
+#
+#    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.
+
+# Script structure inspired from Apache Karaf and other Apache projects with similar startup approaches
+
+# Discover the path of the file
+
+
+# Since MacOS X, FreeBSD and some other systems lack gnu readlink, we use a more portable
+# approach based on following StackOverflow comment http://stackoverflow.com/a/1116890/888876
+
+TARGET_FILE=$0
+
+cd $(dirname $TARGET_FILE)
+TARGET_FILE=$(basename $TARGET_FILE)
+
+# Iterate down a (possible) chain of symlinks
+while [ -L "$TARGET_FILE" ]
+do
+    TARGET_FILE=$(readlink $TARGET_FILE)
+    cd $(dirname $TARGET_FILE)
+    TARGET_FILE=$(basename $TARGET_FILE)
+done
+
+# Compute the canonicalized name by finding the physical path
+# for the directory we're in and appending the target file.
+PHYS_DIR=$(pwd -P)
+
+SCRIPT_DIR=$PHYS_DIR
+PROGNAME=$(basename "$0")
+
+. "${SCRIPT_DIR}/nifi-registry-env.sh"
+
+
+
+warn() {
+    echo "${PROGNAME}: $*"
+}
+
+die() {
+    warn "$*"
+    exit 1
+}
+
+detectOS() {
+    # OS specific support (must be 'true' or 'false').
+    cygwin=false;
+    aix=false;
+    os400=false;
+    darwin=false;
+    case "$(uname)" in
+        CYGWIN*)
+            cygwin=true
+            ;;
+        AIX*)
+            aix=true
+            ;;
+        OS400*)
+            os400=true
+            ;;
+        Darwin)
+            darwin=true
+            ;;
+    esac
+    # For AIX, set an environment variable
+    if ${aix}; then
+         export LDR_CNTRL=MAXDATA=0xB0000000@DSA
+         echo ${LDR_CNTRL}
+    fi
+    # In addition to those, go around the linux space and query the widely
+    # adopted /etc/os-release to detect linux variants
+    if [ -f /etc/os-release ]
+    then
+        . /etc/os-release
+    fi
+}
+
+unlimitFD() {
+    # Use the maximum available, or set MAX_FD != -1 to use that
+    if [ "x${MAX_FD}" = "x" ]; then
+        MAX_FD="maximum"
+    fi
+
+    # Increase the maximum file descriptors if we can
+    if [ "${os400}" = "false" ] && [ "${cygwin}" = "false" ]; then
+        MAX_FD_LIMIT=$(ulimit -H -n)
+        if [ "${MAX_FD_LIMIT}" != 'unlimited' ]; then
+            if [ $? -eq 0 ]; then
+                if [ "${MAX_FD}" = "maximum" -o "${MAX_FD}" = "max" ]; then
+                    # use the system max
+                    MAX_FD="${MAX_FD_LIMIT}"
+                fi
+
+                ulimit -n ${MAX_FD} > /dev/null
+                # echo "ulimit -n" `ulimit -n`
+                if [ $? -ne 0 ]; then
+                    warn "Could not set maximum file descriptor limit: ${MAX_FD}"
+                fi
+            else
+                warn "Could not query system maximum file descriptor limit: ${MAX_FD_LIMIT}"
+            fi
+        fi
+    fi
+}
+
+
+
+locateJava() {
+    # Setup the Java Virtual Machine
+    if $cygwin ; then
+        [ -n "${JAVA}" ] && JAVA=$(cygpath --unix "${JAVA}")
+        [ -n "${JAVA_HOME}" ] && JAVA_HOME=$(cygpath --unix "${JAVA_HOME}")
+    fi
+
+    if [ "x${JAVA}" = "x" ] && [ -r /etc/gentoo-release ] ; then
+        JAVA_HOME=$(java-config --jre-home)
+    fi
+    if [ "x${JAVA}" = "x" ]; then
+        if [ "x${JAVA_HOME}" != "x" ]; then
+            if [ ! -d "${JAVA_HOME}" ]; then
+                die "JAVA_HOME is not valid: ${JAVA_HOME}"
+            fi
+            JAVA="${JAVA_HOME}/bin/java"
+        else
+            warn "JAVA_HOME not set; results may vary"
+            JAVA=$(type java)
+            JAVA=$(expr "${JAVA}" : '.* \(/.*\)$')
+            if [ "x${JAVA}" = "x" ]; then
+                die "java command not found"
+            fi
+        fi
+    fi
+    # if command is env, attempt to add more to the classpath
+    if [ "$1" = "env" ]; then
+        [ "x${TOOLS_JAR}" =  "x" ] && [ -n "${JAVA_HOME}" ] && TOOLS_JAR=$(find -H "${JAVA_HOME}" -name "tools.jar")
+        [ "x${TOOLS_JAR}" =  "x" ] && [ -n "${JAVA_HOME}" ] && TOOLS_JAR=$(find -H "${JAVA_HOME}" -name "classes.jar")
+        if [ "x${TOOLS_JAR}" =  "x" ]; then
+             warn "Could not locate tools.jar or classes.jar. Please set manually to avail all command features."
+        fi
+    fi
+
+}
+
+init() {
+    # Determine if there is special OS handling we must perform
+    detectOS
+
+    # Unlimit the number of file descriptors if possible
+    unlimitFD
+
+    # Locate the Java VM to execute
+    locateJava "$1"
+}
+
+
+install() {
+    detectOS
+
+    if [ "${darwin}" = "true"  ] || [ "${cygwin}" = "true" ]; then
+        echo 'Installing Apache NiFi Registry as a service is not supported on OS X or Cygwin.'
+        exit 1
+    fi
+
+    SVC_NAME=nifi-registry
+    if [ "x$2" != "x" ] ; then
+        SVC_NAME=$2
+    fi
+
+    # since systemd seems to honour /etc/init.d we don't still create native systemd services
+    # yet...
+    initd_dir='/etc/init.d'
+    SVC_FILE="${initd_dir}/${SVC_NAME}"
+
+    if [ ! -w  "${initd_dir}" ]; then
+        echo "Current user does not have write permissions to ${initd_dir}. Cannot install NiFi Registry as a service."
+        exit 1
+    fi
+
+# Create the init script, overwriting anything currently present
+cat <<SERVICEDESCRIPTOR > ${SVC_FILE}
+#!/bin/sh
+
+#
+#    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.
+#
+# chkconfig: 2345 20 80
+# description: Apache NiFi Registry is a complementary application that provides a central location for storage and management of shared resources across one or more instances of NiFi and/or MiNiFi.
+
+# Make use of the configured NIFI_REGISTRY_HOME directory and pass service requests to the nifi-registry.sh executable
+NIFI_REGISTRY_HOME=${NIFI_REGISTRY_HOME}
+bin_dir=\${NIFI_REGISTRY_HOME}/bin
+nifi_registry_executable=\${bin_dir}/nifi-registry.sh
+
+\${nifi_registry_executable} "\$@"
+SERVICEDESCRIPTOR
+
+    if [ ! -f "${SVC_FILE}" ]; then
+        echo "Could not create service file ${SVC_FILE}"
+        exit 1
+    fi
+
+    # Provide the user execute access on the file
+    chmod u+x ${SVC_FILE}
+
+
+    # If SLES or OpenSuse...
+    if [ "${ID}" = "opensuse" ] || [ "${ID}" = "sles" ]; then
+        rm -f "/etc/rc.d/rc2.d/S65${SVC_NAME}"
+        ln -s "/etc/init.d/${SVC_NAME}" "/etc/rc.d/rc2.d/S65${SVC_NAME}" || { echo "Could not create link /etc/rc.d/rc2.d/S65${SVC_NAME}"; exit 1; }
+        rm -f "/etc/rc.d/rc2.d/K65${SVC_NAME}"
+        ln -s "/etc/init.d/${SVC_NAME}" "/etc/rc.d/rc2.d/K65${SVC_NAME}" || { echo "Could not create link /etc/rc.d/rc2.d/K65${SVC_NAME}"; exit 1; }
+        echo "Service ${SVC_NAME} installed"
+    # Anything other fallback to the old approach
+    else
+        rm -f "/etc/rc2.d/S65${SVC_NAME}"
+        ln -s "/etc/init.d/${SVC_NAME}" "/etc/rc2.d/S65${SVC_NAME}" || { echo "Could not create link /etc/rc2.d/S65${SVC_NAME}"; exit 1; }
+        rm -f "/etc/rc2.d/K65${SVC_NAME}"
+        ln -s "/etc/init.d/${SVC_NAME}" "/etc/rc2.d/K65${SVC_NAME}" || { echo "Could not create link /etc/rc2.d/K65${SVC_NAME}"; exit 1; }
+        echo "Service ${SVC_NAME} installed"
+    fi
+}
+
+run() {
+    BOOTSTRAP_CONF_DIR="${NIFI_REGISTRY_HOME}/conf"
+    BOOTSTRAP_CONF="${BOOTSTRAP_CONF_DIR}/bootstrap.conf";
+    BOOTSTRAP_LIBS="${NIFI_REGISTRY_HOME}/lib/bootstrap/*"
+    SHARED_LIBS="${NIFI_REGISTRY_HOME}/lib/shared/*"
+
+    run_as_user=$(grep '^\s*run.as' "${BOOTSTRAP_CONF}" | cut -d'=' -f2)
+    # If the run as user is the same as that starting the process, ignore this configuration
+    if [ "${run_as_user}" = "$(whoami)" ]; then
+        unset run_as_user
+    fi
+
+    if $cygwin; then
+        if [ -n "${run_as_user}" ]; then
+            echo "The run.as option is not supported in a Cygwin environment. Exiting."
+            exit 1
+        fi;
+
+        NIFI_REGISTRY_HOME=$(cygpath --path --windows "${NIFI_REGISTRY_HOME}")
+        NIFI_REGISTRY_LOG_DIR=$(cygpath --path --windows "${NIFI_REGISTRY_LOG_DIR}")
+        NIFI_REGISTRY_PID_DIR=$(cygpath --path --windows "${NIFI_REGISTRY_PID_DIR}")
+        BOOTSTRAP_CONF=$(cygpath --path --windows "${BOOTSTRAP_CONF}")
+        BOOTSTRAP_CONF_DIR=$(cygpath --path --windows "${BOOTSTRAP_CONF_DIR}")
+        BOOTSTRAP_LIBS=$(cygpath --path --windows "${BOOTSTRAP_LIBS}")
+        SHARED_LIBS=$(cygpath --path --windows "${SHARED_LIBS}")
+        BOOTSTRAP_CLASSPATH="${BOOTSTRAP_CONF_DIR};${SHARED_LIBS};${BOOTSTRAP_LIBS}"
+        if [ -n "${TOOLS_JAR}" ]; then
+            TOOLS_JAR=$(cygpath --path --windows "${TOOLS_JAR}")
+            BOOTSTRAP_CLASSPATH="${TOOLS_JAR};${BOOTSTRAP_CLASSPATH}"
+        fi
+    else
+        if [ -n "${run_as_user}" ]; then
+            if ! id -u "${run_as_user}" >/dev/null 2>&1; then
+                echo "The specified run.as user ${run_as_user} does not exist. Exiting."
+                exit 1
+            fi
+        fi;
+        BOOTSTRAP_CLASSPATH="${BOOTSTRAP_CONF_DIR}:${SHARED_LIBS}:${BOOTSTRAP_LIBS}"
+        if [ -n "${TOOLS_JAR}" ]; then
+            BOOTSTRAP_CLASSPATH="${TOOLS_JAR}:${BOOTSTRAP_CLASSPATH}"
+        fi
+    fi
+
+    echo
+    echo "Java home: ${JAVA_HOME}"
+    echo "NiFi Registry home: ${NIFI_REGISTRY_HOME}"
+    echo
+    echo "Bootstrap Config File: ${BOOTSTRAP_CONF}"
+    echo
+
+    # run 'start' in the background because the process will continue to run, monitoring NiFi Registry.
+    # all other commands will terminate quickly so want to just wait for them
+
+    #setup directory parameters
+    BOOTSTRAP_LOG_PARAMS="-Dorg.apache.nifi.registry.bootstrap.config.log.dir='${NIFI_REGISTRY_LOG_DIR}'"
+    BOOTSTRAP_PID_PARAMS="-Dorg.apache.nifi.registry.bootstrap.config.pid.dir='${NIFI_REGISTRY_PID_DIR}'"
+    BOOTSTRAP_CONF_PARAMS="-Dorg.apache.nifi.registry.bootstrap.config.file='${BOOTSTRAP_CONF}'"
+
+    BOOTSTRAP_DIR_PARAMS="${BOOTSTRAP_LOG_PARAMS} ${BOOTSTRAP_PID_PARAMS} ${BOOTSTRAP_CONF_PARAMS}"
+
+    run_nifi_registry_cmd="'${JAVA}' -cp '${BOOTSTRAP_CLASSPATH}' -Xms12m -Xmx24m ${BOOTSTRAP_DIR_PARAMS} org.apache.nifi.registry.bootstrap.RunNiFiRegistry $@"
+
+    if [ -n "${run_as_user}" ]; then
+      # Provide SCRIPT_DIR and execute nifi-env for the run.as user command
+      run_nifi_registry_cmd="sudo -u ${run_as_user} sh -c \"SCRIPT_DIR='${SCRIPT_DIR}' && . '${SCRIPT_DIR}/nifi-registry-env.sh' && ${run_nifi_registry_cmd}\""
+    fi
+
+    if [ "$1" = "run" ]; then
+      # Use exec to handover PID to RunNiFi java process, instead of forking it as a child process
+      run_nifi_registry_cmd="exec ${run_nifi_registry_cmd}"
+    fi
+
+    if [ "$1" = "start" ]; then
+        ( eval "cd ${NIFI_REGISTRY_HOME} && ${run_nifi_registry_cmd}" & )> /dev/null 1>&-
+    else
+        eval "cd ${NIFI_REGISTRY_HOME} && ${run_nifi_registry_cmd}"
+    fi
+    EXIT_STATUS=$?
+
+    # Wait just a bit (3 secs) to wait for the logging to finish and then echo a new-line.
+    # We do this to avoid having logs spewed on the console after running the command and then not giving
+    # control back to the user
+    sleep 3
+    echo
+}
+
+main() {
+    init "$1"
+    run "$@"
+}
+
+
+case "$1" in
+    install)
+        install "$@"
+        ;;
+    start|stop|run|status|dump|env)
+        main "$@"
+        ;;
+    restart)
+        init
+        run "stop"
+        run "start"
+        ;;
+    *)
+        echo "Usage nifi-registry {start|stop|run|restart|status|dump|install}"
+        ;;
+esac


[08/51] [partial] nifi-registry git commit: NIFIREG-201 Refactoring project structure to better isolate extensions

Posted by kd...@apache.org.
http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/6f26290d/nifi-registry-core/nifi-registry-web-docs/src/main/webapp/js/jquery.min.js
----------------------------------------------------------------------
diff --git a/nifi-registry-core/nifi-registry-web-docs/src/main/webapp/js/jquery.min.js b/nifi-registry-core/nifi-registry-web-docs/src/main/webapp/js/jquery.min.js
new file mode 100644
index 0000000..4c5be4c
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-web-docs/src/main/webapp/js/jquery.min.js
@@ -0,0 +1,4 @@
+/*! jQuery v3.1.1 | (c) jQuery Foundation | jquery.org/license */
+!function(a,b){"use strict";"object"==typeof module&&"object"==typeof module.exports?module.exports=a.document?b(a,!0):function(a){if(!a.document)throw new Error("jQuery requires a window with a document");return b(a)}:b(a)}("undefined"!=typeof window?window:this,function(a,b){"use strict";var c=[],d=a.document,e=Object.getPrototypeOf,f=c.slice,g=c.concat,h=c.push,i=c.indexOf,j={},k=j.toString,l=j.hasOwnProperty,m=l.toString,n=m.call(Object),o={};function p(a,b){b=b||d;var c=b.createElement("script");c.text=a,b.head.appendChild(c).parentNode.removeChild(c)}var q="3.1.1",r=function(a,b){return new r.fn.init(a,b)},s=/^[\s\uFEFF\xA0]+|[\s\uFEFF\xA0]+$/g,t=/^-ms-/,u=/-([a-z])/g,v=function(a,b){return b.toUpperCase()};r.fn=r.prototype={jquery:q,constructor:r,length:0,toArray:function(){return f.call(this)},get:function(a){return null==a?f.call(this):a<0?this[a+this.length]:this[a]},pushStack:function(a){var b=r.merge(this.constructor(),a);return b.prevObject=this,b},each:function(a){retu
 rn r.each(this,a)},map:function(a){return this.pushStack(r.map(this,function(b,c){return a.call(b,c,b)}))},slice:function(){return this.pushStack(f.apply(this,arguments))},first:function(){return this.eq(0)},last:function(){return this.eq(-1)},eq:function(a){var b=this.length,c=+a+(a<0?b:0);return this.pushStack(c>=0&&c<b?[this[c]]:[])},end:function(){return this.prevObject||this.constructor()},push:h,sort:c.sort,splice:c.splice},r.extend=r.fn.extend=function(){var a,b,c,d,e,f,g=arguments[0]||{},h=1,i=arguments.length,j=!1;for("boolean"==typeof g&&(j=g,g=arguments[h]||{},h++),"object"==typeof g||r.isFunction(g)||(g={}),h===i&&(g=this,h--);h<i;h++)if(null!=(a=arguments[h]))for(b in a)c=g[b],d=a[b],g!==d&&(j&&d&&(r.isPlainObject(d)||(e=r.isArray(d)))?(e?(e=!1,f=c&&r.isArray(c)?c:[]):f=c&&r.isPlainObject(c)?c:{},g[b]=r.extend(j,f,d)):void 0!==d&&(g[b]=d));return g},r.extend({expando:"jQuery"+(q+Math.random()).replace(/\D/g,""),isReady:!0,error:function(a){throw new Error(a)},noop:funct
 ion(){},isFunction:function(a){return"function"===r.type(a)},isArray:Array.isArray,isWindow:function(a){return null!=a&&a===a.window},isNumeric:function(a){var b=r.type(a);return("number"===b||"string"===b)&&!isNaN(a-parseFloat(a))},isPlainObject:function(a){var b,c;return!(!a||"[object Object]"!==k.call(a))&&(!(b=e(a))||(c=l.call(b,"constructor")&&b.constructor,"function"==typeof c&&m.call(c)===n))},isEmptyObject:function(a){var b;for(b in a)return!1;return!0},type:function(a){return null==a?a+"":"object"==typeof a||"function"==typeof a?j[k.call(a)]||"object":typeof a},globalEval:function(a){p(a)},camelCase:function(a){return a.replace(t,"ms-").replace(u,v)},nodeName:function(a,b){return a.nodeName&&a.nodeName.toLowerCase()===b.toLowerCase()},each:function(a,b){var c,d=0;if(w(a)){for(c=a.length;d<c;d++)if(b.call(a[d],d,a[d])===!1)break}else for(d in a)if(b.call(a[d],d,a[d])===!1)break;return a},trim:function(a){return null==a?"":(a+"").replace(s,"")},makeArray:function(a,b){var c=b
 ||[];return null!=a&&(w(Object(a))?r.merge(c,"string"==typeof a?[a]:a):h.call(c,a)),c},inArray:function(a,b,c){return null==b?-1:i.call(b,a,c)},merge:function(a,b){for(var c=+b.length,d=0,e=a.length;d<c;d++)a[e++]=b[d];return a.length=e,a},grep:function(a,b,c){for(var d,e=[],f=0,g=a.length,h=!c;f<g;f++)d=!b(a[f],f),d!==h&&e.push(a[f]);return e},map:function(a,b,c){var d,e,f=0,h=[];if(w(a))for(d=a.length;f<d;f++)e=b(a[f],f,c),null!=e&&h.push(e);else for(f in a)e=b(a[f],f,c),null!=e&&h.push(e);return g.apply([],h)},guid:1,proxy:function(a,b){var c,d,e;if("string"==typeof b&&(c=a[b],b=a,a=c),r.isFunction(a))return d=f.call(arguments,2),e=function(){return a.apply(b||this,d.concat(f.call(arguments)))},e.guid=a.guid=a.guid||r.guid++,e},now:Date.now,support:o}),"function"==typeof Symbol&&(r.fn[Symbol.iterator]=c[Symbol.iterator]),r.each("Boolean Number String Function Array Date RegExp Object Error Symbol".split(" "),function(a,b){j["[object "+b+"]"]=b.toLowerCase()});function w(a){var b=
 !!a&&"length"in a&&a.length,c=r.type(a);return"function"!==c&&!r.isWindow(a)&&("array"===c||0===b||"number"==typeof b&&b>0&&b-1 in a)}var x=function(a){var b,c,d,e,f,g,h,i,j,k,l,m,n,o,p,q,r,s,t,u="sizzle"+1*new Date,v=a.document,w=0,x=0,y=ha(),z=ha(),A=ha(),B=function(a,b){return a===b&&(l=!0),0},C={}.hasOwnProperty,D=[],E=D.pop,F=D.push,G=D.push,H=D.slice,I=function(a,b){for(var c=0,d=a.length;c<d;c++)if(a[c]===b)return c;return-1},J="checked|selected|async|autofocus|autoplay|controls|defer|disabled|hidden|ismap|loop|multiple|open|readonly|required|scoped",K="[\\x20\\t\\r\\n\\f]",L="(?:\\\\.|[\\w-]|[^\0-\\xa0])+",M="\\["+K+"*("+L+")(?:"+K+"*([*^$|!~]?=)"+K+"*(?:'((?:\\\\.|[^\\\\'])*)'|\"((?:\\\\.|[^\\\\\"])*)\"|("+L+"))|)"+K+"*\\]",N=":("+L+")(?:\\((('((?:\\\\.|[^\\\\'])*)'|\"((?:\\\\.|[^\\\\\"])*)\")|((?:\\\\.|[^\\\\()[\\]]|"+M+")*)|.*)\\)|)",O=new RegExp(K+"+","g"),P=new RegExp("^"+K+"+|((?:^|[^\\\\])(?:\\\\.)*)"+K+"+$","g"),Q=new RegExp("^"+K+"*,"+K+"*"),R=new RegExp("^"+K+"*([>
 +~]|"+K+")"+K+"*"),S=new RegExp("="+K+"*([^\\]'\"]*?)"+K+"*\\]","g"),T=new RegExp(N),U=new RegExp("^"+L+"$"),V={ID:new RegExp("^#("+L+")"),CLASS:new RegExp("^\\.("+L+")"),TAG:new RegExp("^("+L+"|[*])"),ATTR:new RegExp("^"+M),PSEUDO:new RegExp("^"+N),CHILD:new RegExp("^:(only|first|last|nth|nth-last)-(child|of-type)(?:\\("+K+"*(even|odd|(([+-]|)(\\d*)n|)"+K+"*(?:([+-]|)"+K+"*(\\d+)|))"+K+"*\\)|)","i"),bool:new RegExp("^(?:"+J+")$","i"),needsContext:new RegExp("^"+K+"*[>+~]|:(even|odd|eq|gt|lt|nth|first|last)(?:\\("+K+"*((?:-\\d)?\\d*)"+K+"*\\)|)(?=[^-]|$)","i")},W=/^(?:input|select|textarea|button)$/i,X=/^h\d$/i,Y=/^[^{]+\{\s*\[native \w/,Z=/^(?:#([\w-]+)|(\w+)|\.([\w-]+))$/,$=/[+~]/,_=new RegExp("\\\\([\\da-f]{1,6}"+K+"?|("+K+")|.)","ig"),aa=function(a,b,c){var d="0x"+b-65536;return d!==d||c?b:d<0?String.fromCharCode(d+65536):String.fromCharCode(d>>10|55296,1023&d|56320)},ba=/([\0-\x1f\x7f]|^-?\d)|^-$|[^\0-\x1f\x7f-\uFFFF\w-]/g,ca=function(a,b){return b?"\0"===a?"\ufffd":a.slice(0,-
 1)+"\\"+a.charCodeAt(a.length-1).toString(16)+" ":"\\"+a},da=function(){m()},ea=ta(function(a){return a.disabled===!0&&("form"in a||"label"in a)},{dir:"parentNode",next:"legend"});try{G.apply(D=H.call(v.childNodes),v.childNodes),D[v.childNodes.length].nodeType}catch(fa){G={apply:D.length?function(a,b){F.apply(a,H.call(b))}:function(a,b){var c=a.length,d=0;while(a[c++]=b[d++]);a.length=c-1}}}function ga(a,b,d,e){var f,h,j,k,l,o,r,s=b&&b.ownerDocument,w=b?b.nodeType:9;if(d=d||[],"string"!=typeof a||!a||1!==w&&9!==w&&11!==w)return d;if(!e&&((b?b.ownerDocument||b:v)!==n&&m(b),b=b||n,p)){if(11!==w&&(l=Z.exec(a)))if(f=l[1]){if(9===w){if(!(j=b.getElementById(f)))return d;if(j.id===f)return d.push(j),d}else if(s&&(j=s.getElementById(f))&&t(b,j)&&j.id===f)return d.push(j),d}else{if(l[2])return G.apply(d,b.getElementsByTagName(a)),d;if((f=l[3])&&c.getElementsByClassName&&b.getElementsByClassName)return G.apply(d,b.getElementsByClassName(f)),d}if(c.qsa&&!A[a+" "]&&(!q||!q.test(a))){if(1!==w)s=
 b,r=a;else if("object"!==b.nodeName.toLowerCase()){(k=b.getAttribute("id"))?k=k.replace(ba,ca):b.setAttribute("id",k=u),o=g(a),h=o.length;while(h--)o[h]="#"+k+" "+sa(o[h]);r=o.join(","),s=$.test(a)&&qa(b.parentNode)||b}if(r)try{return G.apply(d,s.querySelectorAll(r)),d}catch(x){}finally{k===u&&b.removeAttribute("id")}}}return i(a.replace(P,"$1"),b,d,e)}function ha(){var a=[];function b(c,e){return a.push(c+" ")>d.cacheLength&&delete b[a.shift()],b[c+" "]=e}return b}function ia(a){return a[u]=!0,a}function ja(a){var b=n.createElement("fieldset");try{return!!a(b)}catch(c){return!1}finally{b.parentNode&&b.parentNode.removeChild(b),b=null}}function ka(a,b){var c=a.split("|"),e=c.length;while(e--)d.attrHandle[c[e]]=b}function la(a,b){var c=b&&a,d=c&&1===a.nodeType&&1===b.nodeType&&a.sourceIndex-b.sourceIndex;if(d)return d;if(c)while(c=c.nextSibling)if(c===b)return-1;return a?1:-1}function ma(a){return function(b){var c=b.nodeName.toLowerCase();return"input"===c&&b.type===a}}function na(a
 ){return function(b){var c=b.nodeName.toLowerCase();return("input"===c||"button"===c)&&b.type===a}}function oa(a){return function(b){return"form"in b?b.parentNode&&b.disabled===!1?"label"in b?"label"in b.parentNode?b.parentNode.disabled===a:b.disabled===a:b.isDisabled===a||b.isDisabled!==!a&&ea(b)===a:b.disabled===a:"label"in b&&b.disabled===a}}function pa(a){return ia(function(b){return b=+b,ia(function(c,d){var e,f=a([],c.length,b),g=f.length;while(g--)c[e=f[g]]&&(c[e]=!(d[e]=c[e]))})})}function qa(a){return a&&"undefined"!=typeof a.getElementsByTagName&&a}c=ga.support={},f=ga.isXML=function(a){var b=a&&(a.ownerDocument||a).documentElement;return!!b&&"HTML"!==b.nodeName},m=ga.setDocument=function(a){var b,e,g=a?a.ownerDocument||a:v;return g!==n&&9===g.nodeType&&g.documentElement?(n=g,o=n.documentElement,p=!f(n),v!==n&&(e=n.defaultView)&&e.top!==e&&(e.addEventListener?e.addEventListener("unload",da,!1):e.attachEvent&&e.attachEvent("onunload",da)),c.attributes=ja(function(a){return 
 a.className="i",!a.getAttribute("className")}),c.getElementsByTagName=ja(function(a){return a.appendChild(n.createComment("")),!a.getElementsByTagName("*").length}),c.getElementsByClassName=Y.test(n.getElementsByClassName),c.getById=ja(function(a){return o.appendChild(a).id=u,!n.getElementsByName||!n.getElementsByName(u).length}),c.getById?(d.filter.ID=function(a){var b=a.replace(_,aa);return function(a){return a.getAttribute("id")===b}},d.find.ID=function(a,b){if("undefined"!=typeof b.getElementById&&p){var c=b.getElementById(a);return c?[c]:[]}}):(d.filter.ID=function(a){var b=a.replace(_,aa);return function(a){var c="undefined"!=typeof a.getAttributeNode&&a.getAttributeNode("id");return c&&c.value===b}},d.find.ID=function(a,b){if("undefined"!=typeof b.getElementById&&p){var c,d,e,f=b.getElementById(a);if(f){if(c=f.getAttributeNode("id"),c&&c.value===a)return[f];e=b.getElementsByName(a),d=0;while(f=e[d++])if(c=f.getAttributeNode("id"),c&&c.value===a)return[f]}return[]}}),d.find.TA
 G=c.getElementsByTagName?function(a,b){return"undefined"!=typeof b.getElementsByTagName?b.getElementsByTagName(a):c.qsa?b.querySelectorAll(a):void 0}:function(a,b){var c,d=[],e=0,f=b.getElementsByTagName(a);if("*"===a){while(c=f[e++])1===c.nodeType&&d.push(c);return d}return f},d.find.CLASS=c.getElementsByClassName&&function(a,b){if("undefined"!=typeof b.getElementsByClassName&&p)return b.getElementsByClassName(a)},r=[],q=[],(c.qsa=Y.test(n.querySelectorAll))&&(ja(function(a){o.appendChild(a).innerHTML="<a id='"+u+"'></a><select id='"+u+"-\r\\' msallowcapture=''><option selected=''></option></select>",a.querySelectorAll("[msallowcapture^='']").length&&q.push("[*^$]="+K+"*(?:''|\"\")"),a.querySelectorAll("[selected]").length||q.push("\\["+K+"*(?:value|"+J+")"),a.querySelectorAll("[id~="+u+"-]").length||q.push("~="),a.querySelectorAll(":checked").length||q.push(":checked"),a.querySelectorAll("a#"+u+"+*").length||q.push(".#.+[+~]")}),ja(function(a){a.innerHTML="<a href='' disabled='dis
 abled'></a><select disabled='disabled'><option/></select>";var b=n.createElement("input");b.setAttribute("type","hidden"),a.appendChild(b).setAttribute("name","D"),a.querySelectorAll("[name=d]").length&&q.push("name"+K+"*[*^$|!~]?="),2!==a.querySelectorAll(":enabled").length&&q.push(":enabled",":disabled"),o.appendChild(a).disabled=!0,2!==a.querySelectorAll(":disabled").length&&q.push(":enabled",":disabled"),a.querySelectorAll("*,:x"),q.push(",.*:")})),(c.matchesSelector=Y.test(s=o.matches||o.webkitMatchesSelector||o.mozMatchesSelector||o.oMatchesSelector||o.msMatchesSelector))&&ja(function(a){c.disconnectedMatch=s.call(a,"*"),s.call(a,"[s!='']:x"),r.push("!=",N)}),q=q.length&&new RegExp(q.join("|")),r=r.length&&new RegExp(r.join("|")),b=Y.test(o.compareDocumentPosition),t=b||Y.test(o.contains)?function(a,b){var c=9===a.nodeType?a.documentElement:a,d=b&&b.parentNode;return a===d||!(!d||1!==d.nodeType||!(c.contains?c.contains(d):a.compareDocumentPosition&&16&a.compareDocumentPosition
 (d)))}:function(a,b){if(b)while(b=b.parentNode)if(b===a)return!0;return!1},B=b?function(a,b){if(a===b)return l=!0,0;var d=!a.compareDocumentPosition-!b.compareDocumentPosition;return d?d:(d=(a.ownerDocument||a)===(b.ownerDocument||b)?a.compareDocumentPosition(b):1,1&d||!c.sortDetached&&b.compareDocumentPosition(a)===d?a===n||a.ownerDocument===v&&t(v,a)?-1:b===n||b.ownerDocument===v&&t(v,b)?1:k?I(k,a)-I(k,b):0:4&d?-1:1)}:function(a,b){if(a===b)return l=!0,0;var c,d=0,e=a.parentNode,f=b.parentNode,g=[a],h=[b];if(!e||!f)return a===n?-1:b===n?1:e?-1:f?1:k?I(k,a)-I(k,b):0;if(e===f)return la(a,b);c=a;while(c=c.parentNode)g.unshift(c);c=b;while(c=c.parentNode)h.unshift(c);while(g[d]===h[d])d++;return d?la(g[d],h[d]):g[d]===v?-1:h[d]===v?1:0},n):n},ga.matches=function(a,b){return ga(a,null,null,b)},ga.matchesSelector=function(a,b){if((a.ownerDocument||a)!==n&&m(a),b=b.replace(S,"='$1']"),c.matchesSelector&&p&&!A[b+" "]&&(!r||!r.test(b))&&(!q||!q.test(b)))try{var d=s.call(a,b);if(d||c.discon
 nectedMatch||a.document&&11!==a.document.nodeType)return d}catch(e){}return ga(b,n,null,[a]).length>0},ga.contains=function(a,b){return(a.ownerDocument||a)!==n&&m(a),t(a,b)},ga.attr=function(a,b){(a.ownerDocument||a)!==n&&m(a);var e=d.attrHandle[b.toLowerCase()],f=e&&C.call(d.attrHandle,b.toLowerCase())?e(a,b,!p):void 0;return void 0!==f?f:c.attributes||!p?a.getAttribute(b):(f=a.getAttributeNode(b))&&f.specified?f.value:null},ga.escape=function(a){return(a+"").replace(ba,ca)},ga.error=function(a){throw new Error("Syntax error, unrecognized expression: "+a)},ga.uniqueSort=function(a){var b,d=[],e=0,f=0;if(l=!c.detectDuplicates,k=!c.sortStable&&a.slice(0),a.sort(B),l){while(b=a[f++])b===a[f]&&(e=d.push(f));while(e--)a.splice(d[e],1)}return k=null,a},e=ga.getText=function(a){var b,c="",d=0,f=a.nodeType;if(f){if(1===f||9===f||11===f){if("string"==typeof a.textContent)return a.textContent;for(a=a.firstChild;a;a=a.nextSibling)c+=e(a)}else if(3===f||4===f)return a.nodeValue}else while(b=a[
 d++])c+=e(b);return c},d=ga.selectors={cacheLength:50,createPseudo:ia,match:V,attrHandle:{},find:{},relative:{">":{dir:"parentNode",first:!0}," ":{dir:"parentNode"},"+":{dir:"previousSibling",first:!0},"~":{dir:"previousSibling"}},preFilter:{ATTR:function(a){return a[1]=a[1].replace(_,aa),a[3]=(a[3]||a[4]||a[5]||"").replace(_,aa),"~="===a[2]&&(a[3]=" "+a[3]+" "),a.slice(0,4)},CHILD:function(a){return a[1]=a[1].toLowerCase(),"nth"===a[1].slice(0,3)?(a[3]||ga.error(a[0]),a[4]=+(a[4]?a[5]+(a[6]||1):2*("even"===a[3]||"odd"===a[3])),a[5]=+(a[7]+a[8]||"odd"===a[3])):a[3]&&ga.error(a[0]),a},PSEUDO:function(a){var b,c=!a[6]&&a[2];return V.CHILD.test(a[0])?null:(a[3]?a[2]=a[4]||a[5]||"":c&&T.test(c)&&(b=g(c,!0))&&(b=c.indexOf(")",c.length-b)-c.length)&&(a[0]=a[0].slice(0,b),a[2]=c.slice(0,b)),a.slice(0,3))}},filter:{TAG:function(a){var b=a.replace(_,aa).toLowerCase();return"*"===a?function(){return!0}:function(a){return a.nodeName&&a.nodeName.toLowerCase()===b}},CLASS:function(a){var b=y[a+"
  "];return b||(b=new RegExp("(^|"+K+")"+a+"("+K+"|$)"))&&y(a,function(a){return b.test("string"==typeof a.className&&a.className||"undefined"!=typeof a.getAttribute&&a.getAttribute("class")||"")})},ATTR:function(a,b,c){return function(d){var e=ga.attr(d,a);return null==e?"!="===b:!b||(e+="","="===b?e===c:"!="===b?e!==c:"^="===b?c&&0===e.indexOf(c):"*="===b?c&&e.indexOf(c)>-1:"$="===b?c&&e.slice(-c.length)===c:"~="===b?(" "+e.replace(O," ")+" ").indexOf(c)>-1:"|="===b&&(e===c||e.slice(0,c.length+1)===c+"-"))}},CHILD:function(a,b,c,d,e){var f="nth"!==a.slice(0,3),g="last"!==a.slice(-4),h="of-type"===b;return 1===d&&0===e?function(a){return!!a.parentNode}:function(b,c,i){var j,k,l,m,n,o,p=f!==g?"nextSibling":"previousSibling",q=b.parentNode,r=h&&b.nodeName.toLowerCase(),s=!i&&!h,t=!1;if(q){if(f){while(p){m=b;while(m=m[p])if(h?m.nodeName.toLowerCase()===r:1===m.nodeType)return!1;o=p="only"===a&&!o&&"nextSibling"}return!0}if(o=[g?q.firstChild:q.lastChild],g&&s){m=q,l=m[u]||(m[u]={}),k=l[
 m.uniqueID]||(l[m.uniqueID]={}),j=k[a]||[],n=j[0]===w&&j[1],t=n&&j[2],m=n&&q.childNodes[n];while(m=++n&&m&&m[p]||(t=n=0)||o.pop())if(1===m.nodeType&&++t&&m===b){k[a]=[w,n,t];break}}else if(s&&(m=b,l=m[u]||(m[u]={}),k=l[m.uniqueID]||(l[m.uniqueID]={}),j=k[a]||[],n=j[0]===w&&j[1],t=n),t===!1)while(m=++n&&m&&m[p]||(t=n=0)||o.pop())if((h?m.nodeName.toLowerCase()===r:1===m.nodeType)&&++t&&(s&&(l=m[u]||(m[u]={}),k=l[m.uniqueID]||(l[m.uniqueID]={}),k[a]=[w,t]),m===b))break;return t-=e,t===d||t%d===0&&t/d>=0}}},PSEUDO:function(a,b){var c,e=d.pseudos[a]||d.setFilters[a.toLowerCase()]||ga.error("unsupported pseudo: "+a);return e[u]?e(b):e.length>1?(c=[a,a,"",b],d.setFilters.hasOwnProperty(a.toLowerCase())?ia(function(a,c){var d,f=e(a,b),g=f.length;while(g--)d=I(a,f[g]),a[d]=!(c[d]=f[g])}):function(a){return e(a,0,c)}):e}},pseudos:{not:ia(function(a){var b=[],c=[],d=h(a.replace(P,"$1"));return d[u]?ia(function(a,b,c,e){var f,g=d(a,null,e,[]),h=a.length;while(h--)(f=g[h])&&(a[h]=!(b[h]=f))}):fu
 nction(a,e,f){return b[0]=a,d(b,null,f,c),b[0]=null,!c.pop()}}),has:ia(function(a){return function(b){return ga(a,b).length>0}}),contains:ia(function(a){return a=a.replace(_,aa),function(b){return(b.textContent||b.innerText||e(b)).indexOf(a)>-1}}),lang:ia(function(a){return U.test(a||"")||ga.error("unsupported lang: "+a),a=a.replace(_,aa).toLowerCase(),function(b){var c;do if(c=p?b.lang:b.getAttribute("xml:lang")||b.getAttribute("lang"))return c=c.toLowerCase(),c===a||0===c.indexOf(a+"-");while((b=b.parentNode)&&1===b.nodeType);return!1}}),target:function(b){var c=a.location&&a.location.hash;return c&&c.slice(1)===b.id},root:function(a){return a===o},focus:function(a){return a===n.activeElement&&(!n.hasFocus||n.hasFocus())&&!!(a.type||a.href||~a.tabIndex)},enabled:oa(!1),disabled:oa(!0),checked:function(a){var b=a.nodeName.toLowerCase();return"input"===b&&!!a.checked||"option"===b&&!!a.selected},selected:function(a){return a.parentNode&&a.parentNode.selectedIndex,a.selected===!0},em
 pty:function(a){for(a=a.firstChild;a;a=a.nextSibling)if(a.nodeType<6)return!1;return!0},parent:function(a){return!d.pseudos.empty(a)},header:function(a){return X.test(a.nodeName)},input:function(a){return W.test(a.nodeName)},button:function(a){var b=a.nodeName.toLowerCase();return"input"===b&&"button"===a.type||"button"===b},text:function(a){var b;return"input"===a.nodeName.toLowerCase()&&"text"===a.type&&(null==(b=a.getAttribute("type"))||"text"===b.toLowerCase())},first:pa(function(){return[0]}),last:pa(function(a,b){return[b-1]}),eq:pa(function(a,b,c){return[c<0?c+b:c]}),even:pa(function(a,b){for(var c=0;c<b;c+=2)a.push(c);return a}),odd:pa(function(a,b){for(var c=1;c<b;c+=2)a.push(c);return a}),lt:pa(function(a,b,c){for(var d=c<0?c+b:c;--d>=0;)a.push(d);return a}),gt:pa(function(a,b,c){for(var d=c<0?c+b:c;++d<b;)a.push(d);return a})}},d.pseudos.nth=d.pseudos.eq;for(b in{radio:!0,checkbox:!0,file:!0,password:!0,image:!0})d.pseudos[b]=ma(b);for(b in{submit:!0,reset:!0})d.pseudos[b
 ]=na(b);function ra(){}ra.prototype=d.filters=d.pseudos,d.setFilters=new ra,g=ga.tokenize=function(a,b){var c,e,f,g,h,i,j,k=z[a+" "];if(k)return b?0:k.slice(0);h=a,i=[],j=d.preFilter;while(h){c&&!(e=Q.exec(h))||(e&&(h=h.slice(e[0].length)||h),i.push(f=[])),c=!1,(e=R.exec(h))&&(c=e.shift(),f.push({value:c,type:e[0].replace(P," ")}),h=h.slice(c.length));for(g in d.filter)!(e=V[g].exec(h))||j[g]&&!(e=j[g](e))||(c=e.shift(),f.push({value:c,type:g,matches:e}),h=h.slice(c.length));if(!c)break}return b?h.length:h?ga.error(a):z(a,i).slice(0)};function sa(a){for(var b=0,c=a.length,d="";b<c;b++)d+=a[b].value;return d}function ta(a,b,c){var d=b.dir,e=b.next,f=e||d,g=c&&"parentNode"===f,h=x++;return b.first?function(b,c,e){while(b=b[d])if(1===b.nodeType||g)return a(b,c,e);return!1}:function(b,c,i){var j,k,l,m=[w,h];if(i){while(b=b[d])if((1===b.nodeType||g)&&a(b,c,i))return!0}else while(b=b[d])if(1===b.nodeType||g)if(l=b[u]||(b[u]={}),k=l[b.uniqueID]||(l[b.uniqueID]={}),e&&e===b.nodeName.toLower
 Case())b=b[d]||b;else{if((j=k[f])&&j[0]===w&&j[1]===h)return m[2]=j[2];if(k[f]=m,m[2]=a(b,c,i))return!0}return!1}}function ua(a){return a.length>1?function(b,c,d){var e=a.length;while(e--)if(!a[e](b,c,d))return!1;return!0}:a[0]}function va(a,b,c){for(var d=0,e=b.length;d<e;d++)ga(a,b[d],c);return c}function wa(a,b,c,d,e){for(var f,g=[],h=0,i=a.length,j=null!=b;h<i;h++)(f=a[h])&&(c&&!c(f,d,e)||(g.push(f),j&&b.push(h)));return g}function xa(a,b,c,d,e,f){return d&&!d[u]&&(d=xa(d)),e&&!e[u]&&(e=xa(e,f)),ia(function(f,g,h,i){var j,k,l,m=[],n=[],o=g.length,p=f||va(b||"*",h.nodeType?[h]:h,[]),q=!a||!f&&b?p:wa(p,m,a,h,i),r=c?e||(f?a:o||d)?[]:g:q;if(c&&c(q,r,h,i),d){j=wa(r,n),d(j,[],h,i),k=j.length;while(k--)(l=j[k])&&(r[n[k]]=!(q[n[k]]=l))}if(f){if(e||a){if(e){j=[],k=r.length;while(k--)(l=r[k])&&j.push(q[k]=l);e(null,r=[],j,i)}k=r.length;while(k--)(l=r[k])&&(j=e?I(f,l):m[k])>-1&&(f[j]=!(g[j]=l))}}else r=wa(r===g?r.splice(o,r.length):r),e?e(null,g,r,i):G.apply(g,r)})}function ya(a){for(var b
 ,c,e,f=a.length,g=d.relative[a[0].type],h=g||d.relative[" "],i=g?1:0,k=ta(function(a){return a===b},h,!0),l=ta(function(a){return I(b,a)>-1},h,!0),m=[function(a,c,d){var e=!g&&(d||c!==j)||((b=c).nodeType?k(a,c,d):l(a,c,d));return b=null,e}];i<f;i++)if(c=d.relative[a[i].type])m=[ta(ua(m),c)];else{if(c=d.filter[a[i].type].apply(null,a[i].matches),c[u]){for(e=++i;e<f;e++)if(d.relative[a[e].type])break;return xa(i>1&&ua(m),i>1&&sa(a.slice(0,i-1).concat({value:" "===a[i-2].type?"*":""})).replace(P,"$1"),c,i<e&&ya(a.slice(i,e)),e<f&&ya(a=a.slice(e)),e<f&&sa(a))}m.push(c)}return ua(m)}function za(a,b){var c=b.length>0,e=a.length>0,f=function(f,g,h,i,k){var l,o,q,r=0,s="0",t=f&&[],u=[],v=j,x=f||e&&d.find.TAG("*",k),y=w+=null==v?1:Math.random()||.1,z=x.length;for(k&&(j=g===n||g||k);s!==z&&null!=(l=x[s]);s++){if(e&&l){o=0,g||l.ownerDocument===n||(m(l),h=!p);while(q=a[o++])if(q(l,g||n,h)){i.push(l);break}k&&(w=y)}c&&((l=!q&&l)&&r--,f&&t.push(l))}if(r+=s,c&&s!==r){o=0;while(q=b[o++])q(t,u,g,h);
 if(f){if(r>0)while(s--)t[s]||u[s]||(u[s]=E.call(i));u=wa(u)}G.apply(i,u),k&&!f&&u.length>0&&r+b.length>1&&ga.uniqueSort(i)}return k&&(w=y,j=v),t};return c?ia(f):f}return h=ga.compile=function(a,b){var c,d=[],e=[],f=A[a+" "];if(!f){b||(b=g(a)),c=b.length;while(c--)f=ya(b[c]),f[u]?d.push(f):e.push(f);f=A(a,za(e,d)),f.selector=a}return f},i=ga.select=function(a,b,c,e){var f,i,j,k,l,m="function"==typeof a&&a,n=!e&&g(a=m.selector||a);if(c=c||[],1===n.length){if(i=n[0]=n[0].slice(0),i.length>2&&"ID"===(j=i[0]).type&&9===b.nodeType&&p&&d.relative[i[1].type]){if(b=(d.find.ID(j.matches[0].replace(_,aa),b)||[])[0],!b)return c;m&&(b=b.parentNode),a=a.slice(i.shift().value.length)}f=V.needsContext.test(a)?0:i.length;while(f--){if(j=i[f],d.relative[k=j.type])break;if((l=d.find[k])&&(e=l(j.matches[0].replace(_,aa),$.test(i[0].type)&&qa(b.parentNode)||b))){if(i.splice(f,1),a=e.length&&sa(i),!a)return G.apply(c,e),c;break}}}return(m||h(a,n))(e,b,!p,c,!b||$.test(a)&&qa(b.parentNode)||b),c},c.sortSta
 ble=u.split("").sort(B).join("")===u,c.detectDuplicates=!!l,m(),c.sortDetached=ja(function(a){return 1&a.compareDocumentPosition(n.createElement("fieldset"))}),ja(function(a){return a.innerHTML="<a href='#'></a>","#"===a.firstChild.getAttribute("href")})||ka("type|href|height|width",function(a,b,c){if(!c)return a.getAttribute(b,"type"===b.toLowerCase()?1:2)}),c.attributes&&ja(function(a){return a.innerHTML="<input/>",a.firstChild.setAttribute("value",""),""===a.firstChild.getAttribute("value")})||ka("value",function(a,b,c){if(!c&&"input"===a.nodeName.toLowerCase())return a.defaultValue}),ja(function(a){return null==a.getAttribute("disabled")})||ka(J,function(a,b,c){var d;if(!c)return a[b]===!0?b.toLowerCase():(d=a.getAttributeNode(b))&&d.specified?d.value:null}),ga}(a);r.find=x,r.expr=x.selectors,r.expr[":"]=r.expr.pseudos,r.uniqueSort=r.unique=x.uniqueSort,r.text=x.getText,r.isXMLDoc=x.isXML,r.contains=x.contains,r.escapeSelector=x.escape;var y=function(a,b,c){var d=[],e=void 0!==c
 ;while((a=a[b])&&9!==a.nodeType)if(1===a.nodeType){if(e&&r(a).is(c))break;d.push(a)}return d},z=function(a,b){for(var c=[];a;a=a.nextSibling)1===a.nodeType&&a!==b&&c.push(a);return c},A=r.expr.match.needsContext,B=/^<([a-z][^\/\0>:\x20\t\r\n\f]*)[\x20\t\r\n\f]*\/?>(?:<\/\1>|)$/i,C=/^.[^:#\[\.,]*$/;function D(a,b,c){return r.isFunction(b)?r.grep(a,function(a,d){return!!b.call(a,d,a)!==c}):b.nodeType?r.grep(a,function(a){return a===b!==c}):"string"!=typeof b?r.grep(a,function(a){return i.call(b,a)>-1!==c}):C.test(b)?r.filter(b,a,c):(b=r.filter(b,a),r.grep(a,function(a){return i.call(b,a)>-1!==c&&1===a.nodeType}))}r.filter=function(a,b,c){var d=b[0];return c&&(a=":not("+a+")"),1===b.length&&1===d.nodeType?r.find.matchesSelector(d,a)?[d]:[]:r.find.matches(a,r.grep(b,function(a){return 1===a.nodeType}))},r.fn.extend({find:function(a){var b,c,d=this.length,e=this;if("string"!=typeof a)return this.pushStack(r(a).filter(function(){for(b=0;b<d;b++)if(r.contains(e[b],this))return!0}));for(c=t
 his.pushStack([]),b=0;b<d;b++)r.find(a,e[b],c);return d>1?r.uniqueSort(c):c},filter:function(a){return this.pushStack(D(this,a||[],!1))},not:function(a){return this.pushStack(D(this,a||[],!0))},is:function(a){return!!D(this,"string"==typeof a&&A.test(a)?r(a):a||[],!1).length}});var E,F=/^(?:\s*(<[\w\W]+>)[^>]*|#([\w-]+))$/,G=r.fn.init=function(a,b,c){var e,f;if(!a)return this;if(c=c||E,"string"==typeof a){if(e="<"===a[0]&&">"===a[a.length-1]&&a.length>=3?[null,a,null]:F.exec(a),!e||!e[1]&&b)return!b||b.jquery?(b||c).find(a):this.constructor(b).find(a);if(e[1]){if(b=b instanceof r?b[0]:b,r.merge(this,r.parseHTML(e[1],b&&b.nodeType?b.ownerDocument||b:d,!0)),B.test(e[1])&&r.isPlainObject(b))for(e in b)r.isFunction(this[e])?this[e](b[e]):this.attr(e,b[e]);return this}return f=d.getElementById(e[2]),f&&(this[0]=f,this.length=1),this}return a.nodeType?(this[0]=a,this.length=1,this):r.isFunction(a)?void 0!==c.ready?c.ready(a):a(r):r.makeArray(a,this)};G.prototype=r.fn,E=r(d);var H=/^(?:par
 ents|prev(?:Until|All))/,I={children:!0,contents:!0,next:!0,prev:!0};r.fn.extend({has:function(a){var b=r(a,this),c=b.length;return this.filter(function(){for(var a=0;a<c;a++)if(r.contains(this,b[a]))return!0})},closest:function(a,b){var c,d=0,e=this.length,f=[],g="string"!=typeof a&&r(a);if(!A.test(a))for(;d<e;d++)for(c=this[d];c&&c!==b;c=c.parentNode)if(c.nodeType<11&&(g?g.index(c)>-1:1===c.nodeType&&r.find.matchesSelector(c,a))){f.push(c);break}return this.pushStack(f.length>1?r.uniqueSort(f):f)},index:function(a){return a?"string"==typeof a?i.call(r(a),this[0]):i.call(this,a.jquery?a[0]:a):this[0]&&this[0].parentNode?this.first().prevAll().length:-1},add:function(a,b){return this.pushStack(r.uniqueSort(r.merge(this.get(),r(a,b))))},addBack:function(a){return this.add(null==a?this.prevObject:this.prevObject.filter(a))}});function J(a,b){while((a=a[b])&&1!==a.nodeType);return a}r.each({parent:function(a){var b=a.parentNode;return b&&11!==b.nodeType?b:null},parents:function(a){retu
 rn y(a,"parentNode")},parentsUntil:function(a,b,c){return y(a,"parentNode",c)},next:function(a){return J(a,"nextSibling")},prev:function(a){return J(a,"previousSibling")},nextAll:function(a){return y(a,"nextSibling")},prevAll:function(a){return y(a,"previousSibling")},nextUntil:function(a,b,c){return y(a,"nextSibling",c)},prevUntil:function(a,b,c){return y(a,"previousSibling",c)},siblings:function(a){return z((a.parentNode||{}).firstChild,a)},children:function(a){return z(a.firstChild)},contents:function(a){return a.contentDocument||r.merge([],a.childNodes)}},function(a,b){r.fn[a]=function(c,d){var e=r.map(this,b,c);return"Until"!==a.slice(-5)&&(d=c),d&&"string"==typeof d&&(e=r.filter(d,e)),this.length>1&&(I[a]||r.uniqueSort(e),H.test(a)&&e.reverse()),this.pushStack(e)}});var K=/[^\x20\t\r\n\f]+/g;function L(a){var b={};return r.each(a.match(K)||[],function(a,c){b[c]=!0}),b}r.Callbacks=function(a){a="string"==typeof a?L(a):r.extend({},a);var b,c,d,e,f=[],g=[],h=-1,i=function(){for(e
 =a.once,d=b=!0;g.length;h=-1){c=g.shift();while(++h<f.length)f[h].apply(c[0],c[1])===!1&&a.stopOnFalse&&(h=f.length,c=!1)}a.memory||(c=!1),b=!1,e&&(f=c?[]:"")},j={add:function(){return f&&(c&&!b&&(h=f.length-1,g.push(c)),function d(b){r.each(b,function(b,c){r.isFunction(c)?a.unique&&j.has(c)||f.push(c):c&&c.length&&"string"!==r.type(c)&&d(c)})}(arguments),c&&!b&&i()),this},remove:function(){return r.each(arguments,function(a,b){var c;while((c=r.inArray(b,f,c))>-1)f.splice(c,1),c<=h&&h--}),this},has:function(a){return a?r.inArray(a,f)>-1:f.length>0},empty:function(){return f&&(f=[]),this},disable:function(){return e=g=[],f=c="",this},disabled:function(){return!f},lock:function(){return e=g=[],c||b||(f=c=""),this},locked:function(){return!!e},fireWith:function(a,c){return e||(c=c||[],c=[a,c.slice?c.slice():c],g.push(c),b||i()),this},fire:function(){return j.fireWith(this,arguments),this},fired:function(){return!!d}};return j};function M(a){return a}function N(a){throw a}function O(a,b
 ,c){var d;try{a&&r.isFunction(d=a.promise)?d.call(a).done(b).fail(c):a&&r.isFunction(d=a.then)?d.call(a,b,c):b.call(void 0,a)}catch(a){c.call(void 0,a)}}r.extend({Deferred:function(b){var c=[["notify","progress",r.Callbacks("memory"),r.Callbacks("memory"),2],["resolve","done",r.Callbacks("once memory"),r.Callbacks("once memory"),0,"resolved"],["reject","fail",r.Callbacks("once memory"),r.Callbacks("once memory"),1,"rejected"]],d="pending",e={state:function(){return d},always:function(){return f.done(arguments).fail(arguments),this},"catch":function(a){return e.then(null,a)},pipe:function(){var a=arguments;return r.Deferred(function(b){r.each(c,function(c,d){var e=r.isFunction(a[d[4]])&&a[d[4]];f[d[1]](function(){var a=e&&e.apply(this,arguments);a&&r.isFunction(a.promise)?a.promise().progress(b.notify).done(b.resolve).fail(b.reject):b[d[0]+"With"](this,e?[a]:arguments)})}),a=null}).promise()},then:function(b,d,e){var f=0;function g(b,c,d,e){return function(){var h=this,i=arguments,j=
 function(){var a,j;if(!(b<f)){if(a=d.apply(h,i),a===c.promise())throw new TypeError("Thenable self-resolution");j=a&&("object"==typeof a||"function"==typeof a)&&a.then,r.isFunction(j)?e?j.call(a,g(f,c,M,e),g(f,c,N,e)):(f++,j.call(a,g(f,c,M,e),g(f,c,N,e),g(f,c,M,c.notifyWith))):(d!==M&&(h=void 0,i=[a]),(e||c.resolveWith)(h,i))}},k=e?j:function(){try{j()}catch(a){r.Deferred.exceptionHook&&r.Deferred.exceptionHook(a,k.stackTrace),b+1>=f&&(d!==N&&(h=void 0,i=[a]),c.rejectWith(h,i))}};b?k():(r.Deferred.getStackHook&&(k.stackTrace=r.Deferred.getStackHook()),a.setTimeout(k))}}return r.Deferred(function(a){c[0][3].add(g(0,a,r.isFunction(e)?e:M,a.notifyWith)),c[1][3].add(g(0,a,r.isFunction(b)?b:M)),c[2][3].add(g(0,a,r.isFunction(d)?d:N))}).promise()},promise:function(a){return null!=a?r.extend(a,e):e}},f={};return r.each(c,function(a,b){var g=b[2],h=b[5];e[b[1]]=g.add,h&&g.add(function(){d=h},c[3-a][2].disable,c[0][2].lock),g.add(b[3].fire),f[b[0]]=function(){return f[b[0]+"With"](this===f?v
 oid 0:this,arguments),this},f[b[0]+"With"]=g.fireWith}),e.promise(f),b&&b.call(f,f),f},when:function(a){var b=arguments.length,c=b,d=Array(c),e=f.call(arguments),g=r.Deferred(),h=function(a){return function(c){d[a]=this,e[a]=arguments.length>1?f.call(arguments):c,--b||g.resolveWith(d,e)}};if(b<=1&&(O(a,g.done(h(c)).resolve,g.reject),"pending"===g.state()||r.isFunction(e[c]&&e[c].then)))return g.then();while(c--)O(e[c],h(c),g.reject);return g.promise()}});var P=/^(Eval|Internal|Range|Reference|Syntax|Type|URI)Error$/;r.Deferred.exceptionHook=function(b,c){a.console&&a.console.warn&&b&&P.test(b.name)&&a.console.warn("jQuery.Deferred exception: "+b.message,b.stack,c)},r.readyException=function(b){a.setTimeout(function(){throw b})};var Q=r.Deferred();r.fn.ready=function(a){return Q.then(a)["catch"](function(a){r.readyException(a)}),this},r.extend({isReady:!1,readyWait:1,holdReady:function(a){a?r.readyWait++:r.ready(!0)},ready:function(a){(a===!0?--r.readyWait:r.isReady)||(r.isReady=!0,a
 !==!0&&--r.readyWait>0||Q.resolveWith(d,[r]))}}),r.ready.then=Q.then;function R(){d.removeEventListener("DOMContentLoaded",R),
+a.removeEventListener("load",R),r.ready()}"complete"===d.readyState||"loading"!==d.readyState&&!d.documentElement.doScroll?a.setTimeout(r.ready):(d.addEventListener("DOMContentLoaded",R),a.addEventListener("load",R));var S=function(a,b,c,d,e,f,g){var h=0,i=a.length,j=null==c;if("object"===r.type(c)){e=!0;for(h in c)S(a,b,h,c[h],!0,f,g)}else if(void 0!==d&&(e=!0,r.isFunction(d)||(g=!0),j&&(g?(b.call(a,d),b=null):(j=b,b=function(a,b,c){return j.call(r(a),c)})),b))for(;h<i;h++)b(a[h],c,g?d:d.call(a[h],h,b(a[h],c)));return e?a:j?b.call(a):i?b(a[0],c):f},T=function(a){return 1===a.nodeType||9===a.nodeType||!+a.nodeType};function U(){this.expando=r.expando+U.uid++}U.uid=1,U.prototype={cache:function(a){var b=a[this.expando];return b||(b={},T(a)&&(a.nodeType?a[this.expando]=b:Object.defineProperty(a,this.expando,{value:b,configurable:!0}))),b},set:function(a,b,c){var d,e=this.cache(a);if("string"==typeof b)e[r.camelCase(b)]=c;else for(d in b)e[r.camelCase(d)]=b[d];return e},get:function(a,
 b){return void 0===b?this.cache(a):a[this.expando]&&a[this.expando][r.camelCase(b)]},access:function(a,b,c){return void 0===b||b&&"string"==typeof b&&void 0===c?this.get(a,b):(this.set(a,b,c),void 0!==c?c:b)},remove:function(a,b){var c,d=a[this.expando];if(void 0!==d){if(void 0!==b){r.isArray(b)?b=b.map(r.camelCase):(b=r.camelCase(b),b=b in d?[b]:b.match(K)||[]),c=b.length;while(c--)delete d[b[c]]}(void 0===b||r.isEmptyObject(d))&&(a.nodeType?a[this.expando]=void 0:delete a[this.expando])}},hasData:function(a){var b=a[this.expando];return void 0!==b&&!r.isEmptyObject(b)}};var V=new U,W=new U,X=/^(?:\{[\w\W]*\}|\[[\w\W]*\])$/,Y=/[A-Z]/g;function Z(a){return"true"===a||"false"!==a&&("null"===a?null:a===+a+""?+a:X.test(a)?JSON.parse(a):a)}function $(a,b,c){var d;if(void 0===c&&1===a.nodeType)if(d="data-"+b.replace(Y,"-$&").toLowerCase(),c=a.getAttribute(d),"string"==typeof c){try{c=Z(c)}catch(e){}W.set(a,b,c)}else c=void 0;return c}r.extend({hasData:function(a){return W.hasData(a)||V.h
 asData(a)},data:function(a,b,c){return W.access(a,b,c)},removeData:function(a,b){W.remove(a,b)},_data:function(a,b,c){return V.access(a,b,c)},_removeData:function(a,b){V.remove(a,b)}}),r.fn.extend({data:function(a,b){var c,d,e,f=this[0],g=f&&f.attributes;if(void 0===a){if(this.length&&(e=W.get(f),1===f.nodeType&&!V.get(f,"hasDataAttrs"))){c=g.length;while(c--)g[c]&&(d=g[c].name,0===d.indexOf("data-")&&(d=r.camelCase(d.slice(5)),$(f,d,e[d])));V.set(f,"hasDataAttrs",!0)}return e}return"object"==typeof a?this.each(function(){W.set(this,a)}):S(this,function(b){var c;if(f&&void 0===b){if(c=W.get(f,a),void 0!==c)return c;if(c=$(f,a),void 0!==c)return c}else this.each(function(){W.set(this,a,b)})},null,b,arguments.length>1,null,!0)},removeData:function(a){return this.each(function(){W.remove(this,a)})}}),r.extend({queue:function(a,b,c){var d;if(a)return b=(b||"fx")+"queue",d=V.get(a,b),c&&(!d||r.isArray(c)?d=V.access(a,b,r.makeArray(c)):d.push(c)),d||[]},dequeue:function(a,b){b=b||"fx";var
  c=r.queue(a,b),d=c.length,e=c.shift(),f=r._queueHooks(a,b),g=function(){r.dequeue(a,b)};"inprogress"===e&&(e=c.shift(),d--),e&&("fx"===b&&c.unshift("inprogress"),delete f.stop,e.call(a,g,f)),!d&&f&&f.empty.fire()},_queueHooks:function(a,b){var c=b+"queueHooks";return V.get(a,c)||V.access(a,c,{empty:r.Callbacks("once memory").add(function(){V.remove(a,[b+"queue",c])})})}}),r.fn.extend({queue:function(a,b){var c=2;return"string"!=typeof a&&(b=a,a="fx",c--),arguments.length<c?r.queue(this[0],a):void 0===b?this:this.each(function(){var c=r.queue(this,a,b);r._queueHooks(this,a),"fx"===a&&"inprogress"!==c[0]&&r.dequeue(this,a)})},dequeue:function(a){return this.each(function(){r.dequeue(this,a)})},clearQueue:function(a){return this.queue(a||"fx",[])},promise:function(a,b){var c,d=1,e=r.Deferred(),f=this,g=this.length,h=function(){--d||e.resolveWith(f,[f])};"string"!=typeof a&&(b=a,a=void 0),a=a||"fx";while(g--)c=V.get(f[g],a+"queueHooks"),c&&c.empty&&(d++,c.empty.add(h));return h(),e.pro
 mise(b)}});var _=/[+-]?(?:\d*\.|)\d+(?:[eE][+-]?\d+|)/.source,aa=new RegExp("^(?:([+-])=|)("+_+")([a-z%]*)$","i"),ba=["Top","Right","Bottom","Left"],ca=function(a,b){return a=b||a,"none"===a.style.display||""===a.style.display&&r.contains(a.ownerDocument,a)&&"none"===r.css(a,"display")},da=function(a,b,c,d){var e,f,g={};for(f in b)g[f]=a.style[f],a.style[f]=b[f];e=c.apply(a,d||[]);for(f in b)a.style[f]=g[f];return e};function ea(a,b,c,d){var e,f=1,g=20,h=d?function(){return d.cur()}:function(){return r.css(a,b,"")},i=h(),j=c&&c[3]||(r.cssNumber[b]?"":"px"),k=(r.cssNumber[b]||"px"!==j&&+i)&&aa.exec(r.css(a,b));if(k&&k[3]!==j){j=j||k[3],c=c||[],k=+i||1;do f=f||".5",k/=f,r.style(a,b,k+j);while(f!==(f=h()/i)&&1!==f&&--g)}return c&&(k=+k||+i||0,e=c[1]?k+(c[1]+1)*c[2]:+c[2],d&&(d.unit=j,d.start=k,d.end=e)),e}var fa={};function ga(a){var b,c=a.ownerDocument,d=a.nodeName,e=fa[d];return e?e:(b=c.body.appendChild(c.createElement(d)),e=r.css(b,"display"),b.parentNode.removeChild(b),"none"===e&
 &(e="block"),fa[d]=e,e)}function ha(a,b){for(var c,d,e=[],f=0,g=a.length;f<g;f++)d=a[f],d.style&&(c=d.style.display,b?("none"===c&&(e[f]=V.get(d,"display")||null,e[f]||(d.style.display="")),""===d.style.display&&ca(d)&&(e[f]=ga(d))):"none"!==c&&(e[f]="none",V.set(d,"display",c)));for(f=0;f<g;f++)null!=e[f]&&(a[f].style.display=e[f]);return a}r.fn.extend({show:function(){return ha(this,!0)},hide:function(){return ha(this)},toggle:function(a){return"boolean"==typeof a?a?this.show():this.hide():this.each(function(){ca(this)?r(this).show():r(this).hide()})}});var ia=/^(?:checkbox|radio)$/i,ja=/<([a-z][^\/\0>\x20\t\r\n\f]+)/i,ka=/^$|\/(?:java|ecma)script/i,la={option:[1,"<select multiple='multiple'>","</select>"],thead:[1,"<table>","</table>"],col:[2,"<table><colgroup>","</colgroup></table>"],tr:[2,"<table><tbody>","</tbody></table>"],td:[3,"<table><tbody><tr>","</tr></tbody></table>"],_default:[0,"",""]};la.optgroup=la.option,la.tbody=la.tfoot=la.colgroup=la.caption=la.thead,la.th=la.td
 ;function ma(a,b){var c;return c="undefined"!=typeof a.getElementsByTagName?a.getElementsByTagName(b||"*"):"undefined"!=typeof a.querySelectorAll?a.querySelectorAll(b||"*"):[],void 0===b||b&&r.nodeName(a,b)?r.merge([a],c):c}function na(a,b){for(var c=0,d=a.length;c<d;c++)V.set(a[c],"globalEval",!b||V.get(b[c],"globalEval"))}var oa=/<|&#?\w+;/;function pa(a,b,c,d,e){for(var f,g,h,i,j,k,l=b.createDocumentFragment(),m=[],n=0,o=a.length;n<o;n++)if(f=a[n],f||0===f)if("object"===r.type(f))r.merge(m,f.nodeType?[f]:f);else if(oa.test(f)){g=g||l.appendChild(b.createElement("div")),h=(ja.exec(f)||["",""])[1].toLowerCase(),i=la[h]||la._default,g.innerHTML=i[1]+r.htmlPrefilter(f)+i[2],k=i[0];while(k--)g=g.lastChild;r.merge(m,g.childNodes),g=l.firstChild,g.textContent=""}else m.push(b.createTextNode(f));l.textContent="",n=0;while(f=m[n++])if(d&&r.inArray(f,d)>-1)e&&e.push(f);else if(j=r.contains(f.ownerDocument,f),g=ma(l.appendChild(f),"script"),j&&na(g),c){k=0;while(f=g[k++])ka.test(f.type||"")
 &&c.push(f)}return l}!function(){var a=d.createDocumentFragment(),b=a.appendChild(d.createElement("div")),c=d.createElement("input");c.setAttribute("type","radio"),c.setAttribute("checked","checked"),c.setAttribute("name","t"),b.appendChild(c),o.checkClone=b.cloneNode(!0).cloneNode(!0).lastChild.checked,b.innerHTML="<textarea>x</textarea>",o.noCloneChecked=!!b.cloneNode(!0).lastChild.defaultValue}();var qa=d.documentElement,ra=/^key/,sa=/^(?:mouse|pointer|contextmenu|drag|drop)|click/,ta=/^([^.]*)(?:\.(.+)|)/;function ua(){return!0}function va(){return!1}function wa(){try{return d.activeElement}catch(a){}}function xa(a,b,c,d,e,f){var g,h;if("object"==typeof b){"string"!=typeof c&&(d=d||c,c=void 0);for(h in b)xa(a,h,c,d,b[h],f);return a}if(null==d&&null==e?(e=c,d=c=void 0):null==e&&("string"==typeof c?(e=d,d=void 0):(e=d,d=c,c=void 0)),e===!1)e=va;else if(!e)return a;return 1===f&&(g=e,e=function(a){return r().off(a),g.apply(this,arguments)},e.guid=g.guid||(g.guid=r.guid++)),a.each(f
 unction(){r.event.add(this,b,e,d,c)})}r.event={global:{},add:function(a,b,c,d,e){var f,g,h,i,j,k,l,m,n,o,p,q=V.get(a);if(q){c.handler&&(f=c,c=f.handler,e=f.selector),e&&r.find.matchesSelector(qa,e),c.guid||(c.guid=r.guid++),(i=q.events)||(i=q.events={}),(g=q.handle)||(g=q.handle=function(b){return"undefined"!=typeof r&&r.event.triggered!==b.type?r.event.dispatch.apply(a,arguments):void 0}),b=(b||"").match(K)||[""],j=b.length;while(j--)h=ta.exec(b[j])||[],n=p=h[1],o=(h[2]||"").split(".").sort(),n&&(l=r.event.special[n]||{},n=(e?l.delegateType:l.bindType)||n,l=r.event.special[n]||{},k=r.extend({type:n,origType:p,data:d,handler:c,guid:c.guid,selector:e,needsContext:e&&r.expr.match.needsContext.test(e),namespace:o.join(".")},f),(m=i[n])||(m=i[n]=[],m.delegateCount=0,l.setup&&l.setup.call(a,d,o,g)!==!1||a.addEventListener&&a.addEventListener(n,g)),l.add&&(l.add.call(a,k),k.handler.guid||(k.handler.guid=c.guid)),e?m.splice(m.delegateCount++,0,k):m.push(k),r.event.global[n]=!0)}},remove:fu
 nction(a,b,c,d,e){var f,g,h,i,j,k,l,m,n,o,p,q=V.hasData(a)&&V.get(a);if(q&&(i=q.events)){b=(b||"").match(K)||[""],j=b.length;while(j--)if(h=ta.exec(b[j])||[],n=p=h[1],o=(h[2]||"").split(".").sort(),n){l=r.event.special[n]||{},n=(d?l.delegateType:l.bindType)||n,m=i[n]||[],h=h[2]&&new RegExp("(^|\\.)"+o.join("\\.(?:.*\\.|)")+"(\\.|$)"),g=f=m.length;while(f--)k=m[f],!e&&p!==k.origType||c&&c.guid!==k.guid||h&&!h.test(k.namespace)||d&&d!==k.selector&&("**"!==d||!k.selector)||(m.splice(f,1),k.selector&&m.delegateCount--,l.remove&&l.remove.call(a,k));g&&!m.length&&(l.teardown&&l.teardown.call(a,o,q.handle)!==!1||r.removeEvent(a,n,q.handle),delete i[n])}else for(n in i)r.event.remove(a,n+b[j],c,d,!0);r.isEmptyObject(i)&&V.remove(a,"handle events")}},dispatch:function(a){var b=r.event.fix(a),c,d,e,f,g,h,i=new Array(arguments.length),j=(V.get(this,"events")||{})[b.type]||[],k=r.event.special[b.type]||{};for(i[0]=b,c=1;c<arguments.length;c++)i[c]=arguments[c];if(b.delegateTarget=this,!k.preDis
 patch||k.preDispatch.call(this,b)!==!1){h=r.event.handlers.call(this,b,j),c=0;while((f=h[c++])&&!b.isPropagationStopped()){b.currentTarget=f.elem,d=0;while((g=f.handlers[d++])&&!b.isImmediatePropagationStopped())b.rnamespace&&!b.rnamespace.test(g.namespace)||(b.handleObj=g,b.data=g.data,e=((r.event.special[g.origType]||{}).handle||g.handler).apply(f.elem,i),void 0!==e&&(b.result=e)===!1&&(b.preventDefault(),b.stopPropagation()))}return k.postDispatch&&k.postDispatch.call(this,b),b.result}},handlers:function(a,b){var c,d,e,f,g,h=[],i=b.delegateCount,j=a.target;if(i&&j.nodeType&&!("click"===a.type&&a.button>=1))for(;j!==this;j=j.parentNode||this)if(1===j.nodeType&&("click"!==a.type||j.disabled!==!0)){for(f=[],g={},c=0;c<i;c++)d=b[c],e=d.selector+" ",void 0===g[e]&&(g[e]=d.needsContext?r(e,this).index(j)>-1:r.find(e,this,null,[j]).length),g[e]&&f.push(d);f.length&&h.push({elem:j,handlers:f})}return j=this,i<b.length&&h.push({elem:j,handlers:b.slice(i)}),h},addProp:function(a,b){Object.
 defineProperty(r.Event.prototype,a,{enumerable:!0,configurable:!0,get:r.isFunction(b)?function(){if(this.originalEvent)return b(this.originalEvent)}:function(){if(this.originalEvent)return this.originalEvent[a]},set:function(b){Object.defineProperty(this,a,{enumerable:!0,configurable:!0,writable:!0,value:b})}})},fix:function(a){return a[r.expando]?a:new r.Event(a)},special:{load:{noBubble:!0},focus:{trigger:function(){if(this!==wa()&&this.focus)return this.focus(),!1},delegateType:"focusin"},blur:{trigger:function(){if(this===wa()&&this.blur)return this.blur(),!1},delegateType:"focusout"},click:{trigger:function(){if("checkbox"===this.type&&this.click&&r.nodeName(this,"input"))return this.click(),!1},_default:function(a){return r.nodeName(a.target,"a")}},beforeunload:{postDispatch:function(a){void 0!==a.result&&a.originalEvent&&(a.originalEvent.returnValue=a.result)}}}},r.removeEvent=function(a,b,c){a.removeEventListener&&a.removeEventListener(b,c)},r.Event=function(a,b){return this
  instanceof r.Event?(a&&a.type?(this.originalEvent=a,this.type=a.type,this.isDefaultPrevented=a.defaultPrevented||void 0===a.defaultPrevented&&a.returnValue===!1?ua:va,this.target=a.target&&3===a.target.nodeType?a.target.parentNode:a.target,this.currentTarget=a.currentTarget,this.relatedTarget=a.relatedTarget):this.type=a,b&&r.extend(this,b),this.timeStamp=a&&a.timeStamp||r.now(),void(this[r.expando]=!0)):new r.Event(a,b)},r.Event.prototype={constructor:r.Event,isDefaultPrevented:va,isPropagationStopped:va,isImmediatePropagationStopped:va,isSimulated:!1,preventDefault:function(){var a=this.originalEvent;this.isDefaultPrevented=ua,a&&!this.isSimulated&&a.preventDefault()},stopPropagation:function(){var a=this.originalEvent;this.isPropagationStopped=ua,a&&!this.isSimulated&&a.stopPropagation()},stopImmediatePropagation:function(){var a=this.originalEvent;this.isImmediatePropagationStopped=ua,a&&!this.isSimulated&&a.stopImmediatePropagation(),this.stopPropagation()}},r.each({altKey:!0,
 bubbles:!0,cancelable:!0,changedTouches:!0,ctrlKey:!0,detail:!0,eventPhase:!0,metaKey:!0,pageX:!0,pageY:!0,shiftKey:!0,view:!0,"char":!0,charCode:!0,key:!0,keyCode:!0,button:!0,buttons:!0,clientX:!0,clientY:!0,offsetX:!0,offsetY:!0,pointerId:!0,pointerType:!0,screenX:!0,screenY:!0,targetTouches:!0,toElement:!0,touches:!0,which:function(a){var b=a.button;return null==a.which&&ra.test(a.type)?null!=a.charCode?a.charCode:a.keyCode:!a.which&&void 0!==b&&sa.test(a.type)?1&b?1:2&b?3:4&b?2:0:a.which}},r.event.addProp),r.each({mouseenter:"mouseover",mouseleave:"mouseout",pointerenter:"pointerover",pointerleave:"pointerout"},function(a,b){r.event.special[a]={delegateType:b,bindType:b,handle:function(a){var c,d=this,e=a.relatedTarget,f=a.handleObj;return e&&(e===d||r.contains(d,e))||(a.type=f.origType,c=f.handler.apply(this,arguments),a.type=b),c}}}),r.fn.extend({on:function(a,b,c,d){return xa(this,a,b,c,d)},one:function(a,b,c,d){return xa(this,a,b,c,d,1)},off:function(a,b,c){var d,e;if(a&&a.
 preventDefault&&a.handleObj)return d=a.handleObj,r(a.delegateTarget).off(d.namespace?d.origType+"."+d.namespace:d.origType,d.selector,d.handler),this;if("object"==typeof a){for(e in a)this.off(e,b,a[e]);return this}return b!==!1&&"function"!=typeof b||(c=b,b=void 0),c===!1&&(c=va),this.each(function(){r.event.remove(this,a,c,b)})}});var ya=/<(?!area|br|col|embed|hr|img|input|link|meta|param)(([a-z][^\/\0>\x20\t\r\n\f]*)[^>]*)\/>/gi,za=/<script|<style|<link/i,Aa=/checked\s*(?:[^=]|=\s*.checked.)/i,Ba=/^true\/(.*)/,Ca=/^\s*<!(?:\[CDATA\[|--)|(?:\]\]|--)>\s*$/g;function Da(a,b){return r.nodeName(a,"table")&&r.nodeName(11!==b.nodeType?b:b.firstChild,"tr")?a.getElementsByTagName("tbody")[0]||a:a}function Ea(a){return a.type=(null!==a.getAttribute("type"))+"/"+a.type,a}function Fa(a){var b=Ba.exec(a.type);return b?a.type=b[1]:a.removeAttribute("type"),a}function Ga(a,b){var c,d,e,f,g,h,i,j;if(1===b.nodeType){if(V.hasData(a)&&(f=V.access(a),g=V.set(b,f),j=f.events)){delete g.handle,g.event
 s={};for(e in j)for(c=0,d=j[e].length;c<d;c++)r.event.add(b,e,j[e][c])}W.hasData(a)&&(h=W.access(a),i=r.extend({},h),W.set(b,i))}}function Ha(a,b){var c=b.nodeName.toLowerCase();"input"===c&&ia.test(a.type)?b.checked=a.checked:"input"!==c&&"textarea"!==c||(b.defaultValue=a.defaultValue)}function Ia(a,b,c,d){b=g.apply([],b);var e,f,h,i,j,k,l=0,m=a.length,n=m-1,q=b[0],s=r.isFunction(q);if(s||m>1&&"string"==typeof q&&!o.checkClone&&Aa.test(q))return a.each(function(e){var f=a.eq(e);s&&(b[0]=q.call(this,e,f.html())),Ia(f,b,c,d)});if(m&&(e=pa(b,a[0].ownerDocument,!1,a,d),f=e.firstChild,1===e.childNodes.length&&(e=f),f||d)){for(h=r.map(ma(e,"script"),Ea),i=h.length;l<m;l++)j=e,l!==n&&(j=r.clone(j,!0,!0),i&&r.merge(h,ma(j,"script"))),c.call(a[l],j,l);if(i)for(k=h[h.length-1].ownerDocument,r.map(h,Fa),l=0;l<i;l++)j=h[l],ka.test(j.type||"")&&!V.access(j,"globalEval")&&r.contains(k,j)&&(j.src?r._evalUrl&&r._evalUrl(j.src):p(j.textContent.replace(Ca,""),k))}return a}function Ja(a,b,c){for(var 
 d,e=b?r.filter(b,a):a,f=0;null!=(d=e[f]);f++)c||1!==d.nodeType||r.cleanData(ma(d)),d.parentNode&&(c&&r.contains(d.ownerDocument,d)&&na(ma(d,"script")),d.parentNode.removeChild(d));return a}r.extend({htmlPrefilter:function(a){return a.replace(ya,"<$1></$2>")},clone:function(a,b,c){var d,e,f,g,h=a.cloneNode(!0),i=r.contains(a.ownerDocument,a);if(!(o.noCloneChecked||1!==a.nodeType&&11!==a.nodeType||r.isXMLDoc(a)))for(g=ma(h),f=ma(a),d=0,e=f.length;d<e;d++)Ha(f[d],g[d]);if(b)if(c)for(f=f||ma(a),g=g||ma(h),d=0,e=f.length;d<e;d++)Ga(f[d],g[d]);else Ga(a,h);return g=ma(h,"script"),g.length>0&&na(g,!i&&ma(a,"script")),h},cleanData:function(a){for(var b,c,d,e=r.event.special,f=0;void 0!==(c=a[f]);f++)if(T(c)){if(b=c[V.expando]){if(b.events)for(d in b.events)e[d]?r.event.remove(c,d):r.removeEvent(c,d,b.handle);c[V.expando]=void 0}c[W.expando]&&(c[W.expando]=void 0)}}}),r.fn.extend({detach:function(a){return Ja(this,a,!0)},remove:function(a){return Ja(this,a)},text:function(a){return S(this,fu
 nction(a){return void 0===a?r.text(this):this.empty().each(function(){1!==this.nodeType&&11!==this.nodeType&&9!==this.nodeType||(this.textContent=a)})},null,a,arguments.length)},append:function(){return Ia(this,arguments,function(a){if(1===this.nodeType||11===this.nodeType||9===this.nodeType){var b=Da(this,a);b.appendChild(a)}})},prepend:function(){return Ia(this,arguments,function(a){if(1===this.nodeType||11===this.nodeType||9===this.nodeType){var b=Da(this,a);b.insertBefore(a,b.firstChild)}})},before:function(){return Ia(this,arguments,function(a){this.parentNode&&this.parentNode.insertBefore(a,this)})},after:function(){return Ia(this,arguments,function(a){this.parentNode&&this.parentNode.insertBefore(a,this.nextSibling)})},empty:function(){for(var a,b=0;null!=(a=this[b]);b++)1===a.nodeType&&(r.cleanData(ma(a,!1)),a.textContent="");return this},clone:function(a,b){return a=null!=a&&a,b=null==b?a:b,this.map(function(){return r.clone(this,a,b)})},html:function(a){return S(this,funct
 ion(a){var b=this[0]||{},c=0,d=this.length;if(void 0===a&&1===b.nodeType)return b.innerHTML;if("string"==typeof a&&!za.test(a)&&!la[(ja.exec(a)||["",""])[1].toLowerCase()]){a=r.htmlPrefilter(a);try{for(;c<d;c++)b=this[c]||{},1===b.nodeType&&(r.cleanData(ma(b,!1)),b.innerHTML=a);b=0}catch(e){}}b&&this.empty().append(a)},null,a,arguments.length)},replaceWith:function(){var a=[];return Ia(this,arguments,function(b){var c=this.parentNode;r.inArray(this,a)<0&&(r.cleanData(ma(this)),c&&c.replaceChild(b,this))},a)}}),r.each({appendTo:"append",prependTo:"prepend",insertBefore:"before",insertAfter:"after",replaceAll:"replaceWith"},function(a,b){r.fn[a]=function(a){for(var c,d=[],e=r(a),f=e.length-1,g=0;g<=f;g++)c=g===f?this:this.clone(!0),r(e[g])[b](c),h.apply(d,c.get());return this.pushStack(d)}});var Ka=/^margin/,La=new RegExp("^("+_+")(?!px)[a-z%]+$","i"),Ma=function(b){var c=b.ownerDocument.defaultView;return c&&c.opener||(c=a),c.getComputedStyle(b)};!function(){function b(){if(i){i.styl
 e.cssText="box-sizing:border-box;position:relative;display:block;margin:auto;border:1px;padding:1px;top:1%;width:50%",i.innerHTML="",qa.appendChild(h);var b=a.getComputedStyle(i);c="1%"!==b.top,g="2px"===b.marginLeft,e="4px"===b.width,i.style.marginRight="50%",f="4px"===b.marginRight,qa.removeChild(h),i=null}}var c,e,f,g,h=d.createElement("div"),i=d.createElement("div");i.style&&(i.style.backgroundClip="content-box",i.cloneNode(!0).style.backgroundClip="",o.clearCloneStyle="content-box"===i.style.backgroundClip,h.style.cssText="border:0;width:8px;height:0;top:0;left:-9999px;padding:0;margin-top:1px;position:absolute",h.appendChild(i),r.extend(o,{pixelPosition:function(){return b(),c},boxSizingReliable:function(){return b(),e},pixelMarginRight:function(){return b(),f},reliableMarginLeft:function(){return b(),g}}))}();function Na(a,b,c){var d,e,f,g,h=a.style;return c=c||Ma(a),c&&(g=c.getPropertyValue(b)||c[b],""!==g||r.contains(a.ownerDocument,a)||(g=r.style(a,b)),!o.pixelMarginRight(
 )&&La.test(g)&&Ka.test(b)&&(d=h.width,e=h.minWidth,f=h.maxWidth,h.minWidth=h.maxWidth=h.width=g,g=c.width,h.width=d,h.minWidth=e,h.maxWidth=f)),void 0!==g?g+"":g}function Oa(a,b){return{get:function(){return a()?void delete this.get:(this.get=b).apply(this,arguments)}}}var Pa=/^(none|table(?!-c[ea]).+)/,Qa={position:"absolute",visibility:"hidden",display:"block"},Ra={letterSpacing:"0",fontWeight:"400"},Sa=["Webkit","Moz","ms"],Ta=d.createElement("div").style;function Ua(a){if(a in Ta)return a;var b=a[0].toUpperCase()+a.slice(1),c=Sa.length;while(c--)if(a=Sa[c]+b,a in Ta)return a}function Va(a,b,c){var d=aa.exec(b);return d?Math.max(0,d[2]-(c||0))+(d[3]||"px"):b}function Wa(a,b,c,d,e){var f,g=0;for(f=c===(d?"border":"content")?4:"width"===b?1:0;f<4;f+=2)"margin"===c&&(g+=r.css(a,c+ba[f],!0,e)),d?("content"===c&&(g-=r.css(a,"padding"+ba[f],!0,e)),"margin"!==c&&(g-=r.css(a,"border"+ba[f]+"Width",!0,e))):(g+=r.css(a,"padding"+ba[f],!0,e),"padding"!==c&&(g+=r.css(a,"border"+ba[f]+"Width"
 ,!0,e)));return g}function Xa(a,b,c){var d,e=!0,f=Ma(a),g="border-box"===r.css(a,"boxSizing",!1,f);if(a.getClientRects().length&&(d=a.getBoundingClientRect()[b]),d<=0||null==d){if(d=Na(a,b,f),(d<0||null==d)&&(d=a.style[b]),La.test(d))return d;e=g&&(o.boxSizingReliable()||d===a.style[b]),d=parseFloat(d)||0}return d+Wa(a,b,c||(g?"border":"content"),e,f)+"px"}r.extend({cssHooks:{opacity:{get:function(a,b){if(b){var c=Na(a,"opacity");return""===c?"1":c}}}},cssNumber:{animationIterationCount:!0,columnCount:!0,fillOpacity:!0,flexGrow:!0,flexShrink:!0,fontWeight:!0,lineHeight:!0,opacity:!0,order:!0,orphans:!0,widows:!0,zIndex:!0,zoom:!0},cssProps:{"float":"cssFloat"},style:function(a,b,c,d){if(a&&3!==a.nodeType&&8!==a.nodeType&&a.style){var e,f,g,h=r.camelCase(b),i=a.style;return b=r.cssProps[h]||(r.cssProps[h]=Ua(h)||h),g=r.cssHooks[b]||r.cssHooks[h],void 0===c?g&&"get"in g&&void 0!==(e=g.get(a,!1,d))?e:i[b]:(f=typeof c,"string"===f&&(e=aa.exec(c))&&e[1]&&(c=ea(a,b,e),f="number"),null!=c&
 &c===c&&("number"===f&&(c+=e&&e[3]||(r.cssNumber[h]?"":"px")),o.clearCloneStyle||""!==c||0!==b.indexOf("background")||(i[b]="inherit"),g&&"set"in g&&void 0===(c=g.set(a,c,d))||(i[b]=c)),void 0)}},css:function(a,b,c,d){var e,f,g,h=r.camelCase(b);return b=r.cssProps[h]||(r.cssProps[h]=Ua(h)||h),g=r.cssHooks[b]||r.cssHooks[h],g&&"get"in g&&(e=g.get(a,!0,c)),void 0===e&&(e=Na(a,b,d)),"normal"===e&&b in Ra&&(e=Ra[b]),""===c||c?(f=parseFloat(e),c===!0||isFinite(f)?f||0:e):e}}),r.each(["height","width"],function(a,b){r.cssHooks[b]={get:function(a,c,d){if(c)return!Pa.test(r.css(a,"display"))||a.getClientRects().length&&a.getBoundingClientRect().width?Xa(a,b,d):da(a,Qa,function(){return Xa(a,b,d)})},set:function(a,c,d){var e,f=d&&Ma(a),g=d&&Wa(a,b,d,"border-box"===r.css(a,"boxSizing",!1,f),f);return g&&(e=aa.exec(c))&&"px"!==(e[3]||"px")&&(a.style[b]=c,c=r.css(a,b)),Va(a,c,g)}}}),r.cssHooks.marginLeft=Oa(o.reliableMarginLeft,function(a,b){if(b)return(parseFloat(Na(a,"marginLeft"))||a.getBoun
 dingClientRect().left-da(a,{marginLeft:0},function(){return a.getBoundingClientRect().left}))+"px"}),r.each({margin:"",padding:"",border:"Width"},function(a,b){r.cssHooks[a+b]={expand:function(c){for(var d=0,e={},f="string"==typeof c?c.split(" "):[c];d<4;d++)e[a+ba[d]+b]=f[d]||f[d-2]||f[0];return e}},Ka.test(a)||(r.cssHooks[a+b].set=Va)}),r.fn.extend({css:function(a,b){return S(this,function(a,b,c){var d,e,f={},g=0;if(r.isArray(b)){for(d=Ma(a),e=b.length;g<e;g++)f[b[g]]=r.css(a,b[g],!1,d);return f}return void 0!==c?r.style(a,b,c):r.css(a,b)},a,b,arguments.length>1)}});function Ya(a,b,c,d,e){return new Ya.prototype.init(a,b,c,d,e)}r.Tween=Ya,Ya.prototype={constructor:Ya,init:function(a,b,c,d,e,f){this.elem=a,this.prop=c,this.easing=e||r.easing._default,this.options=b,this.start=this.now=this.cur(),this.end=d,this.unit=f||(r.cssNumber[c]?"":"px")},cur:function(){var a=Ya.propHooks[this.prop];return a&&a.get?a.get(this):Ya.propHooks._default.get(this)},run:function(a){var b,c=Ya.propHo
 oks[this.prop];return this.options.duration?this.pos=b=r.easing[this.easing](a,this.options.duration*a,0,1,this.options.duration):this.pos=b=a,this.now=(this.end-this.start)*b+this.start,this.options.step&&this.options.step.call(this.elem,this.now,this),c&&c.set?c.set(this):Ya.propHooks._default.set(this),this}},Ya.prototype.init.prototype=Ya.prototype,Ya.propHooks={_default:{get:function(a){var b;return 1!==a.elem.nodeType||null!=a.elem[a.prop]&&null==a.elem.style[a.prop]?a.elem[a.prop]:(b=r.css(a.elem,a.prop,""),b&&"auto"!==b?b:0)},set:function(a){r.fx.step[a.prop]?r.fx.step[a.prop](a):1!==a.elem.nodeType||null==a.elem.style[r.cssProps[a.prop]]&&!r.cssHooks[a.prop]?a.elem[a.prop]=a.now:r.style(a.elem,a.prop,a.now+a.unit)}}},Ya.propHooks.scrollTop=Ya.propHooks.scrollLeft={set:function(a){a.elem.nodeType&&a.elem.parentNode&&(a.elem[a.prop]=a.now)}},r.easing={linear:function(a){return a},swing:function(a){return.5-Math.cos(a*Math.PI)/2},_default:"swing"},r.fx=Ya.prototype.init,r.fx.s
 tep={};var Za,$a,_a=/^(?:toggle|show|hide)$/,ab=/queueHooks$/;function bb(){$a&&(a.requestAnimationFrame(bb),r.fx.tick())}function cb(){return a.setTimeout(function(){Za=void 0}),Za=r.now()}function db(a,b){var c,d=0,e={height:a};for(b=b?1:0;d<4;d+=2-b)c=ba[d],e["margin"+c]=e["padding"+c]=a;return b&&(e.opacity=e.width=a),e}function eb(a,b,c){for(var d,e=(hb.tweeners[b]||[]).concat(hb.tweeners["*"]),f=0,g=e.length;f<g;f++)if(d=e[f].call(c,b,a))return d}function fb(a,b,c){var d,e,f,g,h,i,j,k,l="width"in b||"height"in b,m=this,n={},o=a.style,p=a.nodeType&&ca(a),q=V.get(a,"fxshow");c.queue||(g=r._queueHooks(a,"fx"),null==g.unqueued&&(g.unqueued=0,h=g.empty.fire,g.empty.fire=function(){g.unqueued||h()}),g.unqueued++,m.always(function(){m.always(function(){g.unqueued--,r.queue(a,"fx").length||g.empty.fire()})}));for(d in b)if(e=b[d],_a.test(e)){if(delete b[d],f=f||"toggle"===e,e===(p?"hide":"show")){if("show"!==e||!q||void 0===q[d])continue;p=!0}n[d]=q&&q[d]||r.style(a,d)}if(i=!r.isEmpty
 Object(b),i||!r.isEmptyObject(n)){l&&1===a.nodeType&&(c.overflow=[o.overflow,o.overflowX,o.overflowY],j=q&&q.display,null==j&&(j=V.get(a,"display")),k=r.css(a,"display"),"none"===k&&(j?k=j:(ha([a],!0),j=a.style.display||j,k=r.css(a,"display"),ha([a]))),("inline"===k||"inline-block"===k&&null!=j)&&"none"===r.css(a,"float")&&(i||(m.done(function(){o.display=j}),null==j&&(k=o.display,j="none"===k?"":k)),o.display="inline-block")),c.overflow&&(o.overflow="hidden",m.always(function(){o.overflow=c.overflow[0],o.overflowX=c.overflow[1],o.overflowY=c.overflow[2]})),i=!1;for(d in n)i||(q?"hidden"in q&&(p=q.hidden):q=V.access(a,"fxshow",{display:j}),f&&(q.hidden=!p),p&&ha([a],!0),m.done(function(){p||ha([a]),V.remove(a,"fxshow");for(d in n)r.style(a,d,n[d])})),i=eb(p?q[d]:0,d,m),d in q||(q[d]=i.start,p&&(i.end=i.start,i.start=0))}}function gb(a,b){var c,d,e,f,g;for(c in a)if(d=r.camelCase(c),e=b[d],f=a[c],r.isArray(f)&&(e=f[1],f=a[c]=f[0]),c!==d&&(a[d]=f,delete a[c]),g=r.cssHooks[d],g&&"expan
 d"in g){f=g.expand(f),delete a[d];for(c in f)c in a||(a[c]=f[c],b[c]=e)}else b[d]=e}function hb(a,b,c){var d,e,f=0,g=hb.prefilters.length,h=r.Deferred().always(function(){delete i.elem}),i=function(){if(e)return!1;for(var b=Za||cb(),c=Math.max(0,j.startTime+j.duration-b),d=c/j.duration||0,f=1-d,g=0,i=j.tweens.length;g<i;g++)j.tweens[g].run(f);return h.notifyWith(a,[j,f,c]),f<1&&i?c:(h.resolveWith(a,[j]),!1)},j=h.promise({elem:a,props:r.extend({},b),opts:r.extend(!0,{specialEasing:{},easing:r.easing._default},c),originalProperties:b,originalOptions:c,startTime:Za||cb(),duration:c.duration,tweens:[],createTween:function(b,c){var d=r.Tween(a,j.opts,b,c,j.opts.specialEasing[b]||j.opts.easing);return j.tweens.push(d),d},stop:function(b){var c=0,d=b?j.tweens.length:0;if(e)return this;for(e=!0;c<d;c++)j.tweens[c].run(1);return b?(h.notifyWith(a,[j,1,0]),h.resolveWith(a,[j,b])):h.rejectWith(a,[j,b]),this}}),k=j.props;for(gb(k,j.opts.specialEasing);f<g;f++)if(d=hb.prefilters[f].call(j,a,k,j.
 opts))return r.isFunction(d.stop)&&(r._queueHooks(j.elem,j.opts.queue).stop=r.proxy(d.stop,d)),d;return r.map(k,eb,j),r.isFunction(j.opts.start)&&j.opts.start.call(a,j),r.fx.timer(r.extend(i,{elem:a,anim:j,queue:j.opts.queue})),j.progress(j.opts.progress).done(j.opts.done,j.opts.complete).fail(j.opts.fail).always(j.opts.always)}r.Animation=r.extend(hb,{tweeners:{"*":[function(a,b){var c=this.createTween(a,b);return ea(c.elem,a,aa.exec(b),c),c}]},tweener:function(a,b){r.isFunction(a)?(b=a,a=["*"]):a=a.match(K);for(var c,d=0,e=a.length;d<e;d++)c=a[d],hb.tweeners[c]=hb.tweeners[c]||[],hb.tweeners[c].unshift(b)},prefilters:[fb],prefilter:function(a,b){b?hb.prefilters.unshift(a):hb.prefilters.push(a)}}),r.speed=function(a,b,c){var e=a&&"object"==typeof a?r.extend({},a):{complete:c||!c&&b||r.isFunction(a)&&a,duration:a,easing:c&&b||b&&!r.isFunction(b)&&b};return r.fx.off||d.hidden?e.duration=0:"number"!=typeof e.duration&&(e.duration in r.fx.speeds?e.duration=r.fx.speeds[e.duration]:e.dur
 ation=r.fx.speeds._default),null!=e.queue&&e.queue!==!0||(e.queue="fx"),e.old=e.complete,e.complete=function(){r.isFunction(e.old)&&e.old.call(this),e.queue&&r.dequeue(this,e.queue)},e},r.fn.extend({fadeTo:function(a,b,c,d){return this.filter(ca).css("opacity",0).show().end().animate({opacity:b},a,c,d)},animate:function(a,b,c,d){var e=r.isEmptyObject(a),f=r.speed(b,c,d),g=function(){var b=hb(this,r.extend({},a),f);(e||V.get(this,"finish"))&&b.stop(!0)};return g.finish=g,e||f.queue===!1?this.each(g):this.queue(f.queue,g)},stop:function(a,b,c){var d=function(a){var b=a.stop;delete a.stop,b(c)};return"string"!=typeof a&&(c=b,b=a,a=void 0),b&&a!==!1&&this.queue(a||"fx",[]),this.each(function(){var b=!0,e=null!=a&&a+"queueHooks",f=r.timers,g=V.get(this);if(e)g[e]&&g[e].stop&&d(g[e]);else for(e in g)g[e]&&g[e].stop&&ab.test(e)&&d(g[e]);for(e=f.length;e--;)f[e].elem!==this||null!=a&&f[e].queue!==a||(f[e].anim.stop(c),b=!1,f.splice(e,1));!b&&c||r.dequeue(this,a)})},finish:function(a){return
  a!==!1&&(a=a||"fx"),this.each(function(){var b,c=V.get(this),d=c[a+"queue"],e=c[a+"queueHooks"],f=r.timers,g=d?d.length:0;for(c.finish=!0,r.queue(this,a,[]),e&&e.stop&&e.stop.call(this,!0),b=f.length;b--;)f[b].elem===this&&f[b].queue===a&&(f[b].anim.stop(!0),f.splice(b,1));for(b=0;b<g;b++)d[b]&&d[b].finish&&d[b].finish.call(this);delete c.finish})}}),r.each(["toggle","show","hide"],function(a,b){var c=r.fn[b];r.fn[b]=function(a,d,e){return null==a||"boolean"==typeof a?c.apply(this,arguments):this.animate(db(b,!0),a,d,e)}}),r.each({slideDown:db("show"),slideUp:db("hide"),slideToggle:db("toggle"),fadeIn:{opacity:"show"},fadeOut:{opacity:"hide"},fadeToggle:{opacity:"toggle"}},function(a,b){r.fn[a]=function(a,c,d){return this.animate(b,a,c,d)}}),r.timers=[],r.fx.tick=function(){var a,b=0,c=r.timers;for(Za=r.now();b<c.length;b++)a=c[b],a()||c[b]!==a||c.splice(b--,1);c.length||r.fx.stop(),Za=void 0},r.fx.timer=function(a){r.timers.push(a),a()?r.fx.start():r.timers.pop()},r.fx.interval=13
 ,r.fx.start=function(){$a||($a=a.requestAnimationFrame?a.requestAnimationFrame(bb):a.setInterval(r.fx.tick,r.fx.interval))},r.fx.stop=function(){a.cancelAnimationFrame?a.cancelAnimationFrame($a):a.clearInterval($a),$a=null},r.fx.speeds={slow:600,fast:200,_default:400},r.fn.delay=function(b,c){return b=r.fx?r.fx.speeds[b]||b:b,c=c||"fx",this.queue(c,function(c,d){var e=a.setTimeout(c,b);d.stop=function(){a.clearTimeout(e)}})},function(){var a=d.createElement("input"),b=d.createElement("select"),c=b.appendChild(d.createElement("option"));a.type="checkbox",o.checkOn=""!==a.value,o.optSelected=c.selected,a=d.createElement("input"),a.value="t",a.type="radio",o.radioValue="t"===a.value}();var ib,jb=r.expr.attrHandle;r.fn.extend({attr:function(a,b){return S(this,r.attr,a,b,arguments.length>1)},removeAttr:function(a){return this.each(function(){r.removeAttr(this,a)})}}),r.extend({attr:function(a,b,c){var d,e,f=a.nodeType;if(3!==f&&8!==f&&2!==f)return"undefined"==typeof a.getAttribute?r.prop
 (a,b,c):(1===f&&r.isXMLDoc(a)||(e=r.attrHooks[b.toLowerCase()]||(r.expr.match.bool.test(b)?ib:void 0)),
+void 0!==c?null===c?void r.removeAttr(a,b):e&&"set"in e&&void 0!==(d=e.set(a,c,b))?d:(a.setAttribute(b,c+""),c):e&&"get"in e&&null!==(d=e.get(a,b))?d:(d=r.find.attr(a,b),null==d?void 0:d))},attrHooks:{type:{set:function(a,b){if(!o.radioValue&&"radio"===b&&r.nodeName(a,"input")){var c=a.value;return a.setAttribute("type",b),c&&(a.value=c),b}}}},removeAttr:function(a,b){var c,d=0,e=b&&b.match(K);if(e&&1===a.nodeType)while(c=e[d++])a.removeAttribute(c)}}),ib={set:function(a,b,c){return b===!1?r.removeAttr(a,c):a.setAttribute(c,c),c}},r.each(r.expr.match.bool.source.match(/\w+/g),function(a,b){var c=jb[b]||r.find.attr;jb[b]=function(a,b,d){var e,f,g=b.toLowerCase();return d||(f=jb[g],jb[g]=e,e=null!=c(a,b,d)?g:null,jb[g]=f),e}});var kb=/^(?:input|select|textarea|button)$/i,lb=/^(?:a|area)$/i;r.fn.extend({prop:function(a,b){return S(this,r.prop,a,b,arguments.length>1)},removeProp:function(a){return this.each(function(){delete this[r.propFix[a]||a]})}}),r.extend({prop:function(a,b,c){var 
 d,e,f=a.nodeType;if(3!==f&&8!==f&&2!==f)return 1===f&&r.isXMLDoc(a)||(b=r.propFix[b]||b,e=r.propHooks[b]),void 0!==c?e&&"set"in e&&void 0!==(d=e.set(a,c,b))?d:a[b]=c:e&&"get"in e&&null!==(d=e.get(a,b))?d:a[b]},propHooks:{tabIndex:{get:function(a){var b=r.find.attr(a,"tabindex");return b?parseInt(b,10):kb.test(a.nodeName)||lb.test(a.nodeName)&&a.href?0:-1}}},propFix:{"for":"htmlFor","class":"className"}}),o.optSelected||(r.propHooks.selected={get:function(a){var b=a.parentNode;return b&&b.parentNode&&b.parentNode.selectedIndex,null},set:function(a){var b=a.parentNode;b&&(b.selectedIndex,b.parentNode&&b.parentNode.selectedIndex)}}),r.each(["tabIndex","readOnly","maxLength","cellSpacing","cellPadding","rowSpan","colSpan","useMap","frameBorder","contentEditable"],function(){r.propFix[this.toLowerCase()]=this});function mb(a){var b=a.match(K)||[];return b.join(" ")}function nb(a){return a.getAttribute&&a.getAttribute("class")||""}r.fn.extend({addClass:function(a){var b,c,d,e,f,g,h,i=0;if
 (r.isFunction(a))return this.each(function(b){r(this).addClass(a.call(this,b,nb(this)))});if("string"==typeof a&&a){b=a.match(K)||[];while(c=this[i++])if(e=nb(c),d=1===c.nodeType&&" "+mb(e)+" "){g=0;while(f=b[g++])d.indexOf(" "+f+" ")<0&&(d+=f+" ");h=mb(d),e!==h&&c.setAttribute("class",h)}}return this},removeClass:function(a){var b,c,d,e,f,g,h,i=0;if(r.isFunction(a))return this.each(function(b){r(this).removeClass(a.call(this,b,nb(this)))});if(!arguments.length)return this.attr("class","");if("string"==typeof a&&a){b=a.match(K)||[];while(c=this[i++])if(e=nb(c),d=1===c.nodeType&&" "+mb(e)+" "){g=0;while(f=b[g++])while(d.indexOf(" "+f+" ")>-1)d=d.replace(" "+f+" "," ");h=mb(d),e!==h&&c.setAttribute("class",h)}}return this},toggleClass:function(a,b){var c=typeof a;return"boolean"==typeof b&&"string"===c?b?this.addClass(a):this.removeClass(a):r.isFunction(a)?this.each(function(c){r(this).toggleClass(a.call(this,c,nb(this),b),b)}):this.each(function(){var b,d,e,f;if("string"===c){d=0,e=r
 (this),f=a.match(K)||[];while(b=f[d++])e.hasClass(b)?e.removeClass(b):e.addClass(b)}else void 0!==a&&"boolean"!==c||(b=nb(this),b&&V.set(this,"__className__",b),this.setAttribute&&this.setAttribute("class",b||a===!1?"":V.get(this,"__className__")||""))})},hasClass:function(a){var b,c,d=0;b=" "+a+" ";while(c=this[d++])if(1===c.nodeType&&(" "+mb(nb(c))+" ").indexOf(b)>-1)return!0;return!1}});var ob=/\r/g;r.fn.extend({val:function(a){var b,c,d,e=this[0];{if(arguments.length)return d=r.isFunction(a),this.each(function(c){var e;1===this.nodeType&&(e=d?a.call(this,c,r(this).val()):a,null==e?e="":"number"==typeof e?e+="":r.isArray(e)&&(e=r.map(e,function(a){return null==a?"":a+""})),b=r.valHooks[this.type]||r.valHooks[this.nodeName.toLowerCase()],b&&"set"in b&&void 0!==b.set(this,e,"value")||(this.value=e))});if(e)return b=r.valHooks[e.type]||r.valHooks[e.nodeName.toLowerCase()],b&&"get"in b&&void 0!==(c=b.get(e,"value"))?c:(c=e.value,"string"==typeof c?c.replace(ob,""):null==c?"":c)}}}),r
 .extend({valHooks:{option:{get:function(a){var b=r.find.attr(a,"value");return null!=b?b:mb(r.text(a))}},select:{get:function(a){var b,c,d,e=a.options,f=a.selectedIndex,g="select-one"===a.type,h=g?null:[],i=g?f+1:e.length;for(d=f<0?i:g?f:0;d<i;d++)if(c=e[d],(c.selected||d===f)&&!c.disabled&&(!c.parentNode.disabled||!r.nodeName(c.parentNode,"optgroup"))){if(b=r(c).val(),g)return b;h.push(b)}return h},set:function(a,b){var c,d,e=a.options,f=r.makeArray(b),g=e.length;while(g--)d=e[g],(d.selected=r.inArray(r.valHooks.option.get(d),f)>-1)&&(c=!0);return c||(a.selectedIndex=-1),f}}}}),r.each(["radio","checkbox"],function(){r.valHooks[this]={set:function(a,b){if(r.isArray(b))return a.checked=r.inArray(r(a).val(),b)>-1}},o.checkOn||(r.valHooks[this].get=function(a){return null===a.getAttribute("value")?"on":a.value})});var pb=/^(?:focusinfocus|focusoutblur)$/;r.extend(r.event,{trigger:function(b,c,e,f){var g,h,i,j,k,m,n,o=[e||d],p=l.call(b,"type")?b.type:b,q=l.call(b,"namespace")?b.namespac
 e.split("."):[];if(h=i=e=e||d,3!==e.nodeType&&8!==e.nodeType&&!pb.test(p+r.event.triggered)&&(p.indexOf(".")>-1&&(q=p.split("."),p=q.shift(),q.sort()),k=p.indexOf(":")<0&&"on"+p,b=b[r.expando]?b:new r.Event(p,"object"==typeof b&&b),b.isTrigger=f?2:3,b.namespace=q.join("."),b.rnamespace=b.namespace?new RegExp("(^|\\.)"+q.join("\\.(?:.*\\.|)")+"(\\.|$)"):null,b.result=void 0,b.target||(b.target=e),c=null==c?[b]:r.makeArray(c,[b]),n=r.event.special[p]||{},f||!n.trigger||n.trigger.apply(e,c)!==!1)){if(!f&&!n.noBubble&&!r.isWindow(e)){for(j=n.delegateType||p,pb.test(j+p)||(h=h.parentNode);h;h=h.parentNode)o.push(h),i=h;i===(e.ownerDocument||d)&&o.push(i.defaultView||i.parentWindow||a)}g=0;while((h=o[g++])&&!b.isPropagationStopped())b.type=g>1?j:n.bindType||p,m=(V.get(h,"events")||{})[b.type]&&V.get(h,"handle"),m&&m.apply(h,c),m=k&&h[k],m&&m.apply&&T(h)&&(b.result=m.apply(h,c),b.result===!1&&b.preventDefault());return b.type=p,f||b.isDefaultPrevented()||n._default&&n._default.apply(o.pop(
 ),c)!==!1||!T(e)||k&&r.isFunction(e[p])&&!r.isWindow(e)&&(i=e[k],i&&(e[k]=null),r.event.triggered=p,e[p](),r.event.triggered=void 0,i&&(e[k]=i)),b.result}},simulate:function(a,b,c){var d=r.extend(new r.Event,c,{type:a,isSimulated:!0});r.event.trigger(d,null,b)}}),r.fn.extend({trigger:function(a,b){return this.each(function(){r.event.trigger(a,b,this)})},triggerHandler:function(a,b){var c=this[0];if(c)return r.event.trigger(a,b,c,!0)}}),r.each("blur focus focusin focusout resize scroll click dblclick mousedown mouseup mousemove mouseover mouseout mouseenter mouseleave change select submit keydown keypress keyup contextmenu".split(" "),function(a,b){r.fn[b]=function(a,c){return arguments.length>0?this.on(b,null,a,c):this.trigger(b)}}),r.fn.extend({hover:function(a,b){return this.mouseenter(a).mouseleave(b||a)}}),o.focusin="onfocusin"in a,o.focusin||r.each({focus:"focusin",blur:"focusout"},function(a,b){var c=function(a){r.event.simulate(b,a.target,r.event.fix(a))};r.event.special[b]={
 setup:function(){var d=this.ownerDocument||this,e=V.access(d,b);e||d.addEventListener(a,c,!0),V.access(d,b,(e||0)+1)},teardown:function(){var d=this.ownerDocument||this,e=V.access(d,b)-1;e?V.access(d,b,e):(d.removeEventListener(a,c,!0),V.remove(d,b))}}});var qb=a.location,rb=r.now(),sb=/\?/;r.parseXML=function(b){var c;if(!b||"string"!=typeof b)return null;try{c=(new a.DOMParser).parseFromString(b,"text/xml")}catch(d){c=void 0}return c&&!c.getElementsByTagName("parsererror").length||r.error("Invalid XML: "+b),c};var tb=/\[\]$/,ub=/\r?\n/g,vb=/^(?:submit|button|image|reset|file)$/i,wb=/^(?:input|select|textarea|keygen)/i;function xb(a,b,c,d){var e;if(r.isArray(b))r.each(b,function(b,e){c||tb.test(a)?d(a,e):xb(a+"["+("object"==typeof e&&null!=e?b:"")+"]",e,c,d)});else if(c||"object"!==r.type(b))d(a,b);else for(e in b)xb(a+"["+e+"]",b[e],c,d)}r.param=function(a,b){var c,d=[],e=function(a,b){var c=r.isFunction(b)?b():b;d[d.length]=encodeURIComponent(a)+"="+encodeURIComponent(null==c?"":
 c)};if(r.isArray(a)||a.jquery&&!r.isPlainObject(a))r.each(a,function(){e(this.name,this.value)});else for(c in a)xb(c,a[c],b,e);return d.join("&")},r.fn.extend({serialize:function(){return r.param(this.serializeArray())},serializeArray:function(){return this.map(function(){var a=r.prop(this,"elements");return a?r.makeArray(a):this}).filter(function(){var a=this.type;return this.name&&!r(this).is(":disabled")&&wb.test(this.nodeName)&&!vb.test(a)&&(this.checked||!ia.test(a))}).map(function(a,b){var c=r(this).val();return null==c?null:r.isArray(c)?r.map(c,function(a){return{name:b.name,value:a.replace(ub,"\r\n")}}):{name:b.name,value:c.replace(ub,"\r\n")}}).get()}});var yb=/%20/g,zb=/#.*$/,Ab=/([?&])_=[^&]*/,Bb=/^(.*?):[ \t]*([^\r\n]*)$/gm,Cb=/^(?:about|app|app-storage|.+-extension|file|res|widget):$/,Db=/^(?:GET|HEAD)$/,Eb=/^\/\//,Fb={},Gb={},Hb="*/".concat("*"),Ib=d.createElement("a");Ib.href=qb.href;function Jb(a){return function(b,c){"string"!=typeof b&&(c=b,b="*");var d,e=0,f=b.to
 LowerCase().match(K)||[];if(r.isFunction(c))while(d=f[e++])"+"===d[0]?(d=d.slice(1)||"*",(a[d]=a[d]||[]).unshift(c)):(a[d]=a[d]||[]).push(c)}}function Kb(a,b,c,d){var e={},f=a===Gb;function g(h){var i;return e[h]=!0,r.each(a[h]||[],function(a,h){var j=h(b,c,d);return"string"!=typeof j||f||e[j]?f?!(i=j):void 0:(b.dataTypes.unshift(j),g(j),!1)}),i}return g(b.dataTypes[0])||!e["*"]&&g("*")}function Lb(a,b){var c,d,e=r.ajaxSettings.flatOptions||{};for(c in b)void 0!==b[c]&&((e[c]?a:d||(d={}))[c]=b[c]);return d&&r.extend(!0,a,d),a}function Mb(a,b,c){var d,e,f,g,h=a.contents,i=a.dataTypes;while("*"===i[0])i.shift(),void 0===d&&(d=a.mimeType||b.getResponseHeader("Content-Type"));if(d)for(e in h)if(h[e]&&h[e].test(d)){i.unshift(e);break}if(i[0]in c)f=i[0];else{for(e in c){if(!i[0]||a.converters[e+" "+i[0]]){f=e;break}g||(g=e)}f=f||g}if(f)return f!==i[0]&&i.unshift(f),c[f]}function Nb(a,b,c,d){var e,f,g,h,i,j={},k=a.dataTypes.slice();if(k[1])for(g in a.converters)j[g.toLowerCase()]=a.convert
 ers[g];f=k.shift();while(f)if(a.responseFields[f]&&(c[a.responseFields[f]]=b),!i&&d&&a.dataFilter&&(b=a.dataFilter(b,a.dataType)),i=f,f=k.shift())if("*"===f)f=i;else if("*"!==i&&i!==f){if(g=j[i+" "+f]||j["* "+f],!g)for(e in j)if(h=e.split(" "),h[1]===f&&(g=j[i+" "+h[0]]||j["* "+h[0]])){g===!0?g=j[e]:j[e]!==!0&&(f=h[0],k.unshift(h[1]));break}if(g!==!0)if(g&&a["throws"])b=g(b);else try{b=g(b)}catch(l){return{state:"parsererror",error:g?l:"No conversion from "+i+" to "+f}}}return{state:"success",data:b}}r.extend({active:0,lastModified:{},etag:{},ajaxSettings:{url:qb.href,type:"GET",isLocal:Cb.test(qb.protocol),global:!0,processData:!0,async:!0,contentType:"application/x-www-form-urlencoded; charset=UTF-8",accepts:{"*":Hb,text:"text/plain",html:"text/html",xml:"application/xml, text/xml",json:"application/json, text/javascript"},contents:{xml:/\bxml\b/,html:/\bhtml/,json:/\bjson\b/},responseFields:{xml:"responseXML",text:"responseText",json:"responseJSON"},converters:{"* text":String,"t
 ext html":!0,"text json":JSON.parse,"text xml":r.parseXML},flatOptions:{url:!0,context:!0}},ajaxSetup:function(a,b){return b?Lb(Lb(a,r.ajaxSettings),b):Lb(r.ajaxSettings,a)},ajaxPrefilter:Jb(Fb),ajaxTransport:Jb(Gb),ajax:function(b,c){"object"==typeof b&&(c=b,b=void 0),c=c||{};var e,f,g,h,i,j,k,l,m,n,o=r.ajaxSetup({},c),p=o.context||o,q=o.context&&(p.nodeType||p.jquery)?r(p):r.event,s=r.Deferred(),t=r.Callbacks("once memory"),u=o.statusCode||{},v={},w={},x="canceled",y={readyState:0,getResponseHeader:function(a){var b;if(k){if(!h){h={};while(b=Bb.exec(g))h[b[1].toLowerCase()]=b[2]}b=h[a.toLowerCase()]}return null==b?null:b},getAllResponseHeaders:function(){return k?g:null},setRequestHeader:function(a,b){return null==k&&(a=w[a.toLowerCase()]=w[a.toLowerCase()]||a,v[a]=b),this},overrideMimeType:function(a){return null==k&&(o.mimeType=a),this},statusCode:function(a){var b;if(a)if(k)y.always(a[y.status]);else for(b in a)u[b]=[u[b],a[b]];return this},abort:function(a){var b=a||x;return e
 &&e.abort(b),A(0,b),this}};if(s.promise(y),o.url=((b||o.url||qb.href)+"").replace(Eb,qb.protocol+"//"),o.type=c.method||c.type||o.method||o.type,o.dataTypes=(o.dataType||"*").toLowerCase().match(K)||[""],null==o.crossDomain){j=d.createElement("a");try{j.href=o.url,j.href=j.href,o.crossDomain=Ib.protocol+"//"+Ib.host!=j.protocol+"//"+j.host}catch(z){o.crossDomain=!0}}if(o.data&&o.processData&&"string"!=typeof o.data&&(o.data=r.param(o.data,o.traditional)),Kb(Fb,o,c,y),k)return y;l=r.event&&o.global,l&&0===r.active++&&r.event.trigger("ajaxStart"),o.type=o.type.toUpperCase(),o.hasContent=!Db.test(o.type),f=o.url.replace(zb,""),o.hasContent?o.data&&o.processData&&0===(o.contentType||"").indexOf("application/x-www-form-urlencoded")&&(o.data=o.data.replace(yb,"+")):(n=o.url.slice(f.length),o.data&&(f+=(sb.test(f)?"&":"?")+o.data,delete o.data),o.cache===!1&&(f=f.replace(Ab,"$1"),n=(sb.test(f)?"&":"?")+"_="+rb++ +n),o.url=f+n),o.ifModified&&(r.lastModified[f]&&y.setRequestHeader("If-Modifi
 ed-Since",r.lastModified[f]),r.etag[f]&&y.setRequestHeader("If-None-Match",r.etag[f])),(o.data&&o.hasContent&&o.contentType!==!1||c.contentType)&&y.setRequestHeader("Content-Type",o.contentType),y.setRequestHeader("Accept",o.dataTypes[0]&&o.accepts[o.dataTypes[0]]?o.accepts[o.dataTypes[0]]+("*"!==o.dataTypes[0]?", "+Hb+"; q=0.01":""):o.accepts["*"]);for(m in o.headers)y.setRequestHeader(m,o.headers[m]);if(o.beforeSend&&(o.beforeSend.call(p,y,o)===!1||k))return y.abort();if(x="abort",t.add(o.complete),y.done(o.success),y.fail(o.error),e=Kb(Gb,o,c,y)){if(y.readyState=1,l&&q.trigger("ajaxSend",[y,o]),k)return y;o.async&&o.timeout>0&&(i=a.setTimeout(function(){y.abort("timeout")},o.timeout));try{k=!1,e.send(v,A)}catch(z){if(k)throw z;A(-1,z)}}else A(-1,"No Transport");function A(b,c,d,h){var j,m,n,v,w,x=c;k||(k=!0,i&&a.clearTimeout(i),e=void 0,g=h||"",y.readyState=b>0?4:0,j=b>=200&&b<300||304===b,d&&(v=Mb(o,y,d)),v=Nb(o,v,y,j),j?(o.ifModified&&(w=y.getResponseHeader("Last-Modified"),w&&
 (r.lastModified[f]=w),w=y.getResponseHeader("etag"),w&&(r.etag[f]=w)),204===b||"HEAD"===o.type?x="nocontent":304===b?x="notmodified":(x=v.state,m=v.data,n=v.error,j=!n)):(n=x,!b&&x||(x="error",b<0&&(b=0))),y.status=b,y.statusText=(c||x)+"",j?s.resolveWith(p,[m,x,y]):s.rejectWith(p,[y,x,n]),y.statusCode(u),u=void 0,l&&q.trigger(j?"ajaxSuccess":"ajaxError",[y,o,j?m:n]),t.fireWith(p,[y,x]),l&&(q.trigger("ajaxComplete",[y,o]),--r.active||r.event.trigger("ajaxStop")))}return y},getJSON:function(a,b,c){return r.get(a,b,c,"json")},getScript:function(a,b){return r.get(a,void 0,b,"script")}}),r.each(["get","post"],function(a,b){r[b]=function(a,c,d,e){return r.isFunction(c)&&(e=e||d,d=c,c=void 0),r.ajax(r.extend({url:a,type:b,dataType:e,data:c,success:d},r.isPlainObject(a)&&a))}}),r._evalUrl=function(a){return r.ajax({url:a,type:"GET",dataType:"script",cache:!0,async:!1,global:!1,"throws":!0})},r.fn.extend({wrapAll:function(a){var b;return this[0]&&(r.isFunction(a)&&(a=a.call(this[0])),b=r(a,
 this[0].ownerDocument).eq(0).clone(!0),this[0].parentNode&&b.insertBefore(this[0]),b.map(function(){var a=this;while(a.firstElementChild)a=a.firstElementChild;return a}).append(this)),this},wrapInner:function(a){return r.isFunction(a)?this.each(function(b){r(this).wrapInner(a.call(this,b))}):this.each(function(){var b=r(this),c=b.contents();c.length?c.wrapAll(a):b.append(a)})},wrap:function(a){var b=r.isFunction(a);return this.each(function(c){r(this).wrapAll(b?a.call(this,c):a)})},unwrap:function(a){return this.parent(a).not("body").each(function(){r(this).replaceWith(this.childNodes)}),this}}),r.expr.pseudos.hidden=function(a){return!r.expr.pseudos.visible(a)},r.expr.pseudos.visible=function(a){return!!(a.offsetWidth||a.offsetHeight||a.getClientRects().length)},r.ajaxSettings.xhr=function(){try{return new a.XMLHttpRequest}catch(b){}};var Ob={0:200,1223:204},Pb=r.ajaxSettings.xhr();o.cors=!!Pb&&"withCredentials"in Pb,o.ajax=Pb=!!Pb,r.ajaxTransport(function(b){var c,d;if(o.cors||Pb&
 &!b.crossDomain)return{send:function(e,f){var g,h=b.xhr();if(h.open(b.type,b.url,b.async,b.username,b.password),b.xhrFields)for(g in b.xhrFields)h[g]=b.xhrFields[g];b.mimeType&&h.overrideMimeType&&h.overrideMimeType(b.mimeType),b.crossDomain||e["X-Requested-With"]||(e["X-Requested-With"]="XMLHttpRequest");for(g in e)h.setRequestHeader(g,e[g]);c=function(a){return function(){c&&(c=d=h.onload=h.onerror=h.onabort=h.onreadystatechange=null,"abort"===a?h.abort():"error"===a?"number"!=typeof h.status?f(0,"error"):f(h.status,h.statusText):f(Ob[h.status]||h.status,h.statusText,"text"!==(h.responseType||"text")||"string"!=typeof h.responseText?{binary:h.response}:{text:h.responseText},h.getAllResponseHeaders()))}},h.onload=c(),d=h.onerror=c("error"),void 0!==h.onabort?h.onabort=d:h.onreadystatechange=function(){4===h.readyState&&a.setTimeout(function(){c&&d()})},c=c("abort");try{h.send(b.hasContent&&b.data||null)}catch(i){if(c)throw i}},abort:function(){c&&c()}}}),r.ajaxPrefilter(function(a)
 {a.crossDomain&&(a.contents.script=!1)}),r.ajaxSetup({accepts:{script:"text/javascript, application/javascript, application/ecmascript, application/x-ecmascript"},contents:{script:/\b(?:java|ecma)script\b/},converters:{"text script":function(a){return r.globalEval(a),a}}}),r.ajaxPrefilter("script",function(a){void 0===a.cache&&(a.cache=!1),a.crossDomain&&(a.type="GET")}),r.ajaxTransport("script",function(a){if(a.crossDomain){var b,c;return{send:function(e,f){b=r("<script>").prop({charset:a.scriptCharset,src:a.url}).on("load error",c=function(a){b.remove(),c=null,a&&f("error"===a.type?404:200,a.type)}),d.head.appendChild(b[0])},abort:function(){c&&c()}}}});var Qb=[],Rb=/(=)\?(?=&|$)|\?\?/;r.ajaxSetup({jsonp:"callback",jsonpCallback:function(){var a=Qb.pop()||r.expando+"_"+rb++;return this[a]=!0,a}}),r.ajaxPrefilter("json jsonp",function(b,c,d){var e,f,g,h=b.jsonp!==!1&&(Rb.test(b.url)?"url":"string"==typeof b.data&&0===(b.contentType||"").indexOf("application/x-www-form-urlencoded")&
 &Rb.test(b.data)&&"data");if(h||"jsonp"===b.dataTypes[0])return e=b.jsonpCallback=r.isFunction(b.jsonpCallback)?b.jsonpCallback():b.jsonpCallback,h?b[h]=b[h].replace(Rb,"$1"+e):b.jsonp!==!1&&(b.url+=(sb.test(b.url)?"&":"?")+b.jsonp+"="+e),b.converters["script json"]=function(){return g||r.error(e+" was not called"),g[0]},b.dataTypes[0]="json",f=a[e],a[e]=function(){g=arguments},d.always(function(){void 0===f?r(a).removeProp(e):a[e]=f,b[e]&&(b.jsonpCallback=c.jsonpCallback,Qb.push(e)),g&&r.isFunction(f)&&f(g[0]),g=f=void 0}),"script"}),o.createHTMLDocument=function(){var a=d.implementation.createHTMLDocument("").body;return a.innerHTML="<form></form><form></form>",2===a.childNodes.length}(),r.parseHTML=function(a,b,c){if("string"!=typeof a)return[];"boolean"==typeof b&&(c=b,b=!1);var e,f,g;return b||(o.createHTMLDocument?(b=d.implementation.createHTMLDocument(""),e=b.createElement("base"),e.href=d.location.href,b.head.appendChild(e)):b=d),f=B.exec(a),g=!c&&[],f?[b.createElement(f[1])
 ]:(f=pa([a],b,g),g&&g.length&&r(g).remove(),r.merge([],f.childNodes))},r.fn.load=function(a,b,c){var d,e,f,g=this,h=a.indexOf(" ");return h>-1&&(d=mb(a.slice(h)),a=a.slice(0,h)),r.isFunction(b)?(c=b,b=void 0):b&&"object"==typeof b&&(e="POST"),g.length>0&&r.ajax({url:a,type:e||"GET",dataType:"html",data:b}).done(function(a){f=arguments,g.html(d?r("<div>").append(r.parseHTML(a)).find(d):a)}).always(c&&function(a,b){g.each(function(){c.apply(this,f||[a.responseText,b,a])})}),this},r.each(["ajaxStart","ajaxStop","ajaxComplete","ajaxError","ajaxSuccess","ajaxSend"],function(a,b){r.fn[b]=function(a){return this.on(b,a)}}),r.expr.pseudos.animated=function(a){return r.grep(r.timers,function(b){return a===b.elem}).length};function Sb(a){return r.isWindow(a)?a:9===a.nodeType&&a.defaultView}r.offset={setOffset:function(a,b,c){var d,e,f,g,h,i,j,k=r.css(a,"position"),l=r(a),m={};"static"===k&&(a.style.position="relative"),h=l.offset(),f=r.css(a,"top"),i=r.css(a,"left"),j=("absolute"===k||"fixed"
 ===k)&&(f+i).indexOf("auto")>-1,j?(d=l.position(),g=d.top,e=d.left):(g=parseFloat(f)||0,e=parseFloat(i)||0),r.isFunction(b)&&(b=b.call(a,c,r.extend({},h))),null!=b.top&&(m.top=b.top-h.top+g),null!=b.left&&(m.left=b.left-h.left+e),"using"in b?b.using.call(a,m):l.css(m)}},r.fn.extend({offset:function(a){if(arguments.length)return void 0===a?this:this.each(function(b){r.offset.setOffset(this,a,b)});var b,c,d,e,f=this[0];if(f)return f.getClientRects().length?(d=f.getBoundingClientRect(),d.width||d.height?(e=f.ownerDocument,c=Sb(e),b=e.documentElement,{top:d.top+c.pageYOffset-b.clientTop,left:d.left+c.pageXOffset-b.clientLeft}):d):{top:0,left:0}},position:function(){if(this[0]){var a,b,c=this[0],d={top:0,left:0};return"fixed"===r.css(c,"position")?b=c.getBoundingClientRect():(a=this.offsetParent(),b=this.offset(),r.nodeName(a[0],"html")||(d=a.offset()),d={top:d.top+r.css(a[0],"borderTopWidth",!0),left:d.left+r.css(a[0],"borderLeftWidth",!0)}),{top:b.top-d.top-r.css(c,"marginTop",!0),left
 :b.left-d.left-r.css(c,"marginLeft",!0)}}},offsetParent:function(){return this.map(function(){var a=this.offsetParent;while(a&&"static"===r.css(a,"position"))a=a.offsetParent;return a||qa})}}),r.each({scrollLeft:"pageXOffset",scrollTop:"pageYOffset"},function(a,b){var c="pageYOffset"===b;r.fn[a]=function(d){return S(this,function(a,d,e){var f=Sb(a);return void 0===e?f?f[b]:a[d]:void(f?f.scrollTo(c?f.pageXOffset:e,c?e:f.pageYOffset):a[d]=e)},a,d,arguments.length)}}),r.each(["top","left"],function(a,b){r.cssHooks[b]=Oa(o.pixelPosition,function(a,c){if(c)return c=Na(a,b),La.test(c)?r(a).position()[b]+"px":c})}),r.each({Height:"height",Width:"width"},function(a,b){r.each({padding:"inner"+a,content:b,"":"outer"+a},function(c,d){r.fn[d]=function(e,f){var g=arguments.length&&(c||"boolean"!=typeof e),h=c||(e===!0||f===!0?"margin":"border");return S(this,function(b,c,e){var f;return r.isWindow(b)?0===d.indexOf("outer")?b["inner"+a]:b.document.documentElement["client"+a]:9===b.nodeType?(f=b.d
 ocumentElement,Math.max(b.body["scroll"+a],f["scroll"+a],b.body["offset"+a],f["offset"+a],f["client"+a])):void 0===e?r.css(b,c,h):r.style(b,c,e,h)},b,g?e:void 0,g)}})}),r.fn.extend({bind:function(a,b,c){return this.on(a,null,b,c)},unbind:function(a,b){return this.off(a,null,b)},delegate:function(a,b,c,d){return this.on(b,a,c,d)},undelegate:function(a,b,c){return 1===arguments.length?this.off(a,"**"):this.off(b,a||"**",c)}}),r.parseJSON=JSON.parse,"function"==typeof define&&define.amd&&define("jquery",[],function(){return r});var Tb=a.jQuery,Ub=a.$;return r.noConflict=function(b){return a.$===r&&(a.$=Ub),b&&a.jQuery===r&&(a.jQuery=Tb),r},b||(a.jQuery=a.$=r),r});


[25/51] [partial] nifi-registry git commit: NIFIREG-201 Refactoring project structure to better isolate extensions

Posted by kd...@apache.org.
http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/6f26290d/nifi-registry-core/nifi-registry-properties/src/main/java/org/apache/nifi/registry/properties/NiFiRegistryProperties.java
----------------------------------------------------------------------
diff --git a/nifi-registry-core/nifi-registry-properties/src/main/java/org/apache/nifi/registry/properties/NiFiRegistryProperties.java b/nifi-registry-core/nifi-registry-properties/src/main/java/org/apache/nifi/registry/properties/NiFiRegistryProperties.java
new file mode 100644
index 0000000..1dcb0f7
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-properties/src/main/java/org/apache/nifi/registry/properties/NiFiRegistryProperties.java
@@ -0,0 +1,305 @@
+/*
+ * 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.properties;
+
+import java.io.File;
+import java.util.Enumeration;
+import java.util.HashSet;
+import java.util.Properties;
+import java.util.Set;
+
+import org.apache.commons.lang3.StringUtils;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+public class NiFiRegistryProperties extends Properties {
+
+    private static final Logger logger = LoggerFactory.getLogger(NiFiRegistryProperties.class);
+
+    // Keys
+    public static final String WEB_WAR_DIR = "nifi.registry.web.war.directory";
+    public static final String WEB_HTTP_PORT = "nifi.registry.web.http.port";
+    public static final String WEB_HTTP_HOST = "nifi.registry.web.http.host";
+    public static final String WEB_HTTPS_PORT = "nifi.registry.web.https.port";
+    public static final String WEB_HTTPS_HOST = "nifi.registry.web.https.host";
+    public static final String WEB_WORKING_DIR = "nifi.registry.web.jetty.working.directory";
+    public static final String WEB_THREADS = "nifi.registry.web.jetty.threads";
+
+    public static final String SECURITY_KEYSTORE = "nifi.registry.security.keystore";
+    public static final String SECURITY_KEYSTORE_TYPE = "nifi.registry.security.keystoreType";
+    public static final String SECURITY_KEYSTORE_PASSWD = "nifi.registry.security.keystorePasswd";
+    public static final String SECURITY_KEY_PASSWD = "nifi.registry.security.keyPasswd";
+    public static final String SECURITY_TRUSTSTORE = "nifi.registry.security.truststore";
+    public static final String SECURITY_TRUSTSTORE_TYPE = "nifi.registry.security.truststoreType";
+    public static final String SECURITY_TRUSTSTORE_PASSWD = "nifi.registry.security.truststorePasswd";
+    public static final String SECURITY_NEED_CLIENT_AUTH = "nifi.registry.security.needClientAuth";
+    public static final String SECURITY_AUTHORIZERS_CONFIGURATION_FILE = "nifi.registry.security.authorizers.configuration.file";
+    public static final String SECURITY_AUTHORIZER = "nifi.registry.security.authorizer";
+    public static final String SECURITY_IDENTITY_PROVIDERS_CONFIGURATION_FILE = "nifi.registry.security.identity.providers.configuration.file";
+    public static final String SECURITY_IDENTITY_PROVIDER = "nifi.registry.security.identity.provider";
+    public static final String SECURITY_IDENTITY_MAPPING_PATTERN_PREFIX = "nifi.registry.security.identity.mapping.pattern.";
+    public static final String SECURITY_IDENTITY_MAPPING_VALUE_PREFIX = "nifi.registry.security.identity.mapping.value.";
+
+    public static final String EXTENSION_DIR_PREFIX = "nifi.registry.extension.dir.";
+
+    public static final String PROVIDERS_CONFIGURATION_FILE = "nifi.registry.providers.configuration.file";
+
+    // Original DB properties
+    public static final String DATABASE_DIRECTORY = "nifi.registry.db.directory";
+    public static final String DATABASE_URL_APPEND = "nifi.registry.db.url.append";
+
+    // New style DB properties
+    public static final String DATABASE_URL = "nifi.registry.db.url";
+    public static final String DATABASE_DRIVER_CLASS_NAME = "nifi.registry.db.driver.class";
+    public static final String DATABASE_DRIVER_DIR = "nifi.registry.db.driver.directory";
+    public static final String DATABASE_USERNAME = "nifi.registry.db.username";
+    public static final String DATABASE_PASSWORD = "nifi.registry.db.password";
+    public static final String DATABASE_MAX_CONNECTIONS = "nifi.registry.db.maxConnections";
+    public static final String DATABASE_SQL_DEBUG = "nifi.registry.db.sql.debug";
+
+    // Kerberos properties
+    public static final String KERBEROS_KRB5_FILE = "nifi.registry.kerberos.krb5.file";
+    public static final String KERBEROS_SPNEGO_PRINCIPAL = "nifi.registry.kerberos.spnego.principal";
+    public static final String KERBEROS_SPNEGO_KEYTAB_LOCATION = "nifi.registry.kerberos.spnego.keytab.location";
+    public static final String KERBEROS_SPNEGO_AUTHENTICATION_EXPIRATION = "nifi.registry.kerberos.spnego.authentication.expiration";
+    public static final String KERBEROS_SERVICE_PRINCIPAL = "nifi.registry.kerberos.service.principal";
+    public static final String KERBEROS_SERVICE_KEYTAB_LOCATION = "nifi.registry.kerberos.service.keytab.location";
+
+    // Defaults
+    public static final String DEFAULT_WEB_WORKING_DIR = "./work/jetty";
+    public static final String DEFAULT_WAR_DIR = "./lib";
+    public static final String DEFAULT_PROVIDERS_CONFIGURATION_FILE = "./conf/providers.xml";
+    public static final String DEFAULT_SECURITY_AUTHORIZERS_CONFIGURATION_FILE = "./conf/authorizers.xml";
+    public static final String DEFAULT_SECURITY_IDENTITY_PROVIDER_CONFIGURATION_FILE = "./conf/identity-providers.xml";
+    public static final String DEFAULT_AUTHENTICATION_EXPIRATION = "12 hours";
+
+    public int getWebThreads() {
+        int webThreads = 200;
+        try {
+            webThreads = Integer.parseInt(getProperty(WEB_THREADS));
+        } catch (final NumberFormatException nfe) {
+            logger.warn(String.format("%s must be an integer value. Defaulting to %s", WEB_THREADS, webThreads));
+        }
+        return webThreads;
+    }
+
+    public Integer getPort() {
+        return getPropertyAsInteger(WEB_HTTP_PORT);
+    }
+
+    public String getHttpHost() {
+        return getProperty(WEB_HTTP_HOST);
+    }
+
+    public Integer getSslPort() {
+        return getPropertyAsInteger(WEB_HTTPS_PORT);
+    }
+
+    public String getHttpsHost() {
+        return getProperty(WEB_HTTPS_HOST);
+    }
+
+    public boolean getNeedClientAuth() {
+        boolean needClientAuth = true;
+        String rawNeedClientAuth = getProperty(SECURITY_NEED_CLIENT_AUTH);
+        if ("false".equalsIgnoreCase(rawNeedClientAuth)) {
+            needClientAuth = false;
+        }
+        return needClientAuth;
+    }
+
+    public String getKeyStorePath() {
+        return getProperty(SECURITY_KEYSTORE);
+    }
+
+    public String getKeyStoreType() {
+        return getProperty(SECURITY_KEYSTORE_TYPE);
+    }
+
+    public String getKeyStorePassword() {
+        return getProperty(SECURITY_KEYSTORE_PASSWD);
+    }
+
+    public String getKeyPassword() {
+        return getProperty(SECURITY_KEY_PASSWD);
+    }
+
+    public String getTrustStorePath() {
+        return getProperty(SECURITY_TRUSTSTORE);
+    }
+
+    public String getTrustStoreType() {
+        return getProperty(SECURITY_TRUSTSTORE_TYPE);
+    }
+
+    public String getTrustStorePassword() {
+        return getProperty(SECURITY_TRUSTSTORE_PASSWD);
+    }
+
+    public File getWarLibDirectory() {
+        return new File(getProperty(WEB_WAR_DIR, DEFAULT_WAR_DIR));
+    }
+
+    public File getWebWorkingDirectory() {
+        return new File(getProperty(WEB_WORKING_DIR, DEFAULT_WEB_WORKING_DIR));
+    }
+
+    public File getProvidersConfigurationFile() {
+        return getPropertyAsFile(PROVIDERS_CONFIGURATION_FILE, DEFAULT_PROVIDERS_CONFIGURATION_FILE);
+    }
+
+    public String getLegacyDatabaseDirectory() {
+        return getProperty(DATABASE_DIRECTORY);
+    }
+
+    public String getLegacyDatabaseUrlAppend() {
+        return getProperty(DATABASE_URL_APPEND);
+    }
+
+    public String getDatabaseUrl() {
+        return getProperty(DATABASE_URL);
+    }
+
+    public String getDatabaseDriverClassName() {
+        return getProperty(DATABASE_DRIVER_CLASS_NAME);
+    }
+
+    public String getDatabaseDriverDirectory() {
+        return getProperty(DATABASE_DRIVER_DIR);
+    }
+
+    public String getDatabaseUsername() {
+        return getProperty(DATABASE_USERNAME);
+    }
+
+    public String getDatabasePassword() {
+        return getProperty(DATABASE_PASSWORD);
+    }
+
+    public Integer getDatabaseMaxConnections() {
+        return getPropertyAsInteger(DATABASE_MAX_CONNECTIONS);
+    }
+
+    public boolean getDatabaseSqlDebug() {
+        final String value = getProperty(DATABASE_SQL_DEBUG);
+
+        if (StringUtils.isBlank(value)) {
+            return false;
+        }
+
+        return "true".equalsIgnoreCase(value.trim());
+    }
+
+    public File getAuthorizersConfigurationFile() {
+        return getPropertyAsFile(SECURITY_AUTHORIZERS_CONFIGURATION_FILE, DEFAULT_SECURITY_AUTHORIZERS_CONFIGURATION_FILE);
+    }
+
+    public File getIdentityProviderConfigurationFile() {
+        return getPropertyAsFile(SECURITY_IDENTITY_PROVIDERS_CONFIGURATION_FILE, DEFAULT_SECURITY_IDENTITY_PROVIDER_CONFIGURATION_FILE);
+    }
+
+    public File getKerberosConfigurationFile() {
+        return getPropertyAsFile(KERBEROS_KRB5_FILE);
+    }
+
+    public String getKerberosSpnegoAuthenticationExpiration() {
+        return getProperty(KERBEROS_SPNEGO_AUTHENTICATION_EXPIRATION, DEFAULT_AUTHENTICATION_EXPIRATION);
+    }
+
+    public String getKerberosSpnegoPrincipal() {
+        return getPropertyAsTrimmedString(KERBEROS_SPNEGO_PRINCIPAL);
+    }
+
+    public String getKerberosSpnegoKeytabLocation() {
+        return getPropertyAsTrimmedString(KERBEROS_SPNEGO_KEYTAB_LOCATION);
+    }
+
+    public boolean isKerberosSpnegoSupportEnabled() {
+        return !StringUtils.isBlank(getKerberosSpnegoPrincipal()) && !StringUtils.isBlank(getKerberosSpnegoKeytabLocation());
+    }
+
+    public String getKerberosServicePrincipal() {
+        return getPropertyAsTrimmedString(KERBEROS_SERVICE_PRINCIPAL);
+    }
+
+    public String getKerberosServiceKeytabLocation() {
+        return getPropertyAsTrimmedString(KERBEROS_SERVICE_KEYTAB_LOCATION);
+    }
+
+    public Set<String> getExtensionsDirs() {
+        final Set<String> extensionDirs = new HashSet<>();
+        stringPropertyNames().stream().filter(key -> key.startsWith(EXTENSION_DIR_PREFIX)).forEach(key -> extensionDirs.add(getProperty(key)));
+        return extensionDirs;
+    }
+
+    /**
+     * Retrieves all known property keys.
+     *
+     * @return all known property keys
+     */
+    public Set<String> getPropertyKeys() {
+        Set<String> propertyNames = new HashSet<>();
+        Enumeration e = this.propertyNames();
+        for (; e.hasMoreElements(); ){
+            propertyNames.add((String) e.nextElement());
+        }
+
+        return propertyNames;
+    }
+
+    // Helper functions for common ways of interpreting property values
+
+    private String getPropertyAsTrimmedString(String key) {
+        final String value = getProperty(key);
+        if (!StringUtils.isBlank(value)) {
+            return value.trim();
+        } else {
+            return null;
+        }
+    }
+
+    private Integer getPropertyAsInteger(String key) {
+        final String value = getProperty(key);
+        if (StringUtils.isBlank(value)) {
+            return null;
+        }
+        try {
+            return Integer.parseInt(value);
+        } catch (final NumberFormatException nfe) {
+            throw new IllegalStateException(String.format("%s must be an integer value.", key));
+        }
+    }
+
+    private File getPropertyAsFile(String key) {
+        final String filePath = getProperty(key);
+        if (filePath != null && filePath.trim().length() > 0) {
+            return new File(filePath.trim());
+        } else {
+            return null;
+        }
+    }
+
+    private File getPropertyAsFile(String propertyKey, String defaultFileLocation) {
+        final String value = getProperty(propertyKey);
+        if (StringUtils.isBlank(value)) {
+            return new File(defaultFileLocation);
+        } else {
+            return new File(value);
+        }
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/6f26290d/nifi-registry-core/nifi-registry-properties/src/main/java/org/apache/nifi/registry/properties/NiFiRegistryPropertiesLoader.java
----------------------------------------------------------------------
diff --git a/nifi-registry-core/nifi-registry-properties/src/main/java/org/apache/nifi/registry/properties/NiFiRegistryPropertiesLoader.java b/nifi-registry-core/nifi-registry-properties/src/main/java/org/apache/nifi/registry/properties/NiFiRegistryPropertiesLoader.java
new file mode 100644
index 0000000..5ceffd1
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-properties/src/main/java/org/apache/nifi/registry/properties/NiFiRegistryPropertiesLoader.java
@@ -0,0 +1,148 @@
+/*
+ * 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.properties;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import javax.crypto.Cipher;
+import java.io.File;
+import java.io.FileReader;
+import java.io.IOException;
+import java.security.NoSuchAlgorithmException;
+
+public class NiFiRegistryPropertiesLoader {
+
+    private static final Logger logger = LoggerFactory.getLogger(NiFiRegistryPropertiesLoader.class);
+
+    private static final String RELATIVE_PATH = "conf/nifi-registry.properties";
+
+    private String keyHex;
+
+    // Future enhancement: allow for external registration of new providers
+    private static SensitivePropertyProviderFactory sensitivePropertyProviderFactory;
+
+    /**
+     * Returns an instance of the loader configured with the key.
+     * <p>
+     * <p>
+     * NOTE: This method is used reflectively by the process which starts NiFi
+     * so changes to it must be made in conjunction with that mechanism.</p>
+     *
+     * @param keyHex the key used to encrypt any sensitive properties
+     * @return the configured loader
+     */
+    public static NiFiRegistryPropertiesLoader withKey(String keyHex) {
+        NiFiRegistryPropertiesLoader loader = new NiFiRegistryPropertiesLoader();
+        loader.setKeyHex(keyHex);
+        return loader;
+    }
+
+    /**
+     * Sets the hexadecimal key used to unprotect properties encrypted with
+     * {@link AESSensitivePropertyProvider}. If the key has already been set,
+     * calling this method will throw a {@link RuntimeException}.
+     *
+     * @param keyHex the key in hexadecimal format
+     */
+    public void setKeyHex(String keyHex) {
+        if (this.keyHex == null || this.keyHex.trim().isEmpty()) {
+            this.keyHex = keyHex;
+        } else {
+            throw new RuntimeException("Cannot overwrite an existing key");
+        }
+    }
+
+    private static String getDefaultProviderKey() {
+        try {
+            return "aes/gcm/" + (Cipher.getMaxAllowedKeyLength("AES") > 128 ? "256" : "128");
+        } catch (NoSuchAlgorithmException e) {
+            return "aes/gcm/128";
+        }
+    }
+
+    private void initializeSensitivePropertyProviderFactory() {
+        sensitivePropertyProviderFactory = new AESSensitivePropertyProviderFactory(keyHex);
+    }
+
+    private SensitivePropertyProvider getSensitivePropertyProvider() {
+        initializeSensitivePropertyProviderFactory();
+        return sensitivePropertyProviderFactory.getProvider();
+    }
+
+    /**
+     * Returns a {@link ProtectedNiFiRegistryProperties} instance loaded from the
+     * serialized form in the file. Responsible for actually reading from disk
+     * and deserializing the properties. Returns a protected instance to allow
+     * for decryption operations.
+     *
+     * @param file the file containing serialized properties
+     * @return the ProtectedNiFiProperties instance
+     */
+    ProtectedNiFiRegistryProperties readProtectedPropertiesFromDisk(File file) {
+        if (file == null || !file.exists() || !file.canRead()) {
+            String path = (file == null ? "missing file" : file.getAbsolutePath());
+            logger.error("Cannot read from '{}' -- file is missing or not readable", path);
+            throw new IllegalArgumentException("NiFi Registry properties file missing or unreadable");
+        }
+
+        final NiFiRegistryProperties rawProperties = new NiFiRegistryProperties();
+        try (final FileReader reader = new FileReader(file)) {
+            rawProperties.load(reader);
+            logger.info("Loaded {} properties from {}", rawProperties.size(), file.getAbsolutePath());
+            ProtectedNiFiRegistryProperties protectedNiFiRegistryProperties = new ProtectedNiFiRegistryProperties(rawProperties);
+            return protectedNiFiRegistryProperties;
+        } catch (final IOException ioe) {
+            logger.error("Cannot load properties file due to " + ioe.getLocalizedMessage());
+            throw new RuntimeException("Cannot load properties file due to " + ioe.getLocalizedMessage(), ioe);
+        }
+    }
+
+    /**
+     * Returns an instance of {@link NiFiRegistryProperties} loaded from the provided
+     * {@link File}. If any properties are protected, will attempt to use the appropriate
+     * {@link SensitivePropertyProvider} to unprotect them transparently.
+     *
+     * @param file the File containing the serialized properties
+     * @return the NiFiProperties instance
+     */
+    public NiFiRegistryProperties load(File file) {
+        ProtectedNiFiRegistryProperties protectedNiFiRegistryProperties = readProtectedPropertiesFromDisk(file);
+        if (protectedNiFiRegistryProperties.hasProtectedKeys()) {
+            protectedNiFiRegistryProperties.addSensitivePropertyProvider(getSensitivePropertyProvider());
+        }
+
+        return protectedNiFiRegistryProperties.getUnprotectedProperties();
+    }
+
+    /**
+     * Returns an instance of {@link NiFiRegistryProperties}. The path must not be empty.
+     *
+     * @param path the path of the serialized properties file
+     * @return the NiFiRegistryProperties instance
+     * @see NiFiRegistryPropertiesLoader#load(File)
+     */
+    public NiFiRegistryProperties load(String path) {
+        if (path != null && !path.trim().isEmpty()) {
+            return load(new File(path));
+        } else {
+            logger.error("Cannot read from '{}' -- path is null or empty", path);
+            throw new IllegalArgumentException("NiFi Registry properties file path empty or null");
+        }
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/6f26290d/nifi-registry-core/nifi-registry-properties/src/main/java/org/apache/nifi/registry/properties/ProtectedNiFiRegistryProperties.java
----------------------------------------------------------------------
diff --git a/nifi-registry-core/nifi-registry-properties/src/main/java/org/apache/nifi/registry/properties/ProtectedNiFiRegistryProperties.java b/nifi-registry-core/nifi-registry-properties/src/main/java/org/apache/nifi/registry/properties/ProtectedNiFiRegistryProperties.java
new file mode 100644
index 0000000..5debc4a
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-properties/src/main/java/org/apache/nifi/registry/properties/ProtectedNiFiRegistryProperties.java
@@ -0,0 +1,528 @@
+/*
+ * 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.properties;
+
+import org.apache.commons.lang3.StringUtils;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.stream.Collectors;
+
+import static java.util.Arrays.asList;
+
+/**
+ * Wrapper class of {@link NiFiRegistryProperties} for intermediate phase when
+ * {@link NiFiRegistryPropertiesLoader} loads the raw properties file and performs
+ * unprotection activities before returning an instance of {@link NiFiRegistryProperties}.
+ */
+class ProtectedNiFiRegistryProperties {
+    private static final Logger logger = LoggerFactory.getLogger(ProtectedNiFiRegistryProperties.class);
+
+    private NiFiRegistryProperties properties;
+
+    private Map<String, SensitivePropertyProvider> localProviderCache = new HashMap<>();
+
+    // Additional "sensitive" property key
+    public static final String ADDITIONAL_SENSITIVE_PROPERTIES_KEY = "nifi.registry.sensitive.props.additional.keys";
+
+    // Default list of "sensitive" property keys
+    public static final List<String> DEFAULT_SENSITIVE_PROPERTIES = new ArrayList<>(asList(
+            NiFiRegistryProperties.SECURITY_KEY_PASSWD,
+            NiFiRegistryProperties.SECURITY_KEYSTORE_PASSWD,
+            NiFiRegistryProperties.SECURITY_TRUSTSTORE_PASSWD));
+
+    public ProtectedNiFiRegistryProperties() {
+        this(null);
+    }
+
+    /**
+     * Creates an instance containing the provided {@link NiFiRegistryProperties}.
+     *
+     * @param props the NiFiProperties to contain
+     */
+    public ProtectedNiFiRegistryProperties(NiFiRegistryProperties props) {
+        if (props == null) {
+            props = new NiFiRegistryProperties();
+        }
+        this.properties = props;
+        logger.debug("Loaded {} properties (including {} protection schemes) into ProtectedNiFiProperties",
+                getPropertyKeysIncludingProtectionSchemes().size(), getProtectedPropertyKeys().size());
+    }
+
+    /**
+     * Retrieves the property value for the given property key.
+     *
+     * @param key the key of property value to lookup
+     * @return value of property at given key or null if not found
+     */
+    // @Override
+    public String getProperty(String key) {
+        return getInternalNiFiProperties().getProperty(key);
+    }
+
+    /**
+     * Returns the internal representation of the {@link NiFiRegistryProperties} -- protected
+     * or not as determined by the current state. No guarantee is made to the
+     * protection state of these properties. If the internal reference is null, a new
+     * {@link NiFiRegistryProperties} instance is created.
+     *
+     * @return the internal properties
+     */
+    NiFiRegistryProperties getInternalNiFiProperties() {
+        if (this.properties == null) {
+            this.properties = new NiFiRegistryProperties();
+        }
+
+        return this.properties;
+    }
+
+    /**
+     * Returns the number of properties in the NiFiRegistryProperties,
+     * excluding protection scheme properties.
+     *
+     * <p>
+     * Example:
+     * <p>
+     * key: E(value, key)
+     * key.protected: aes/gcm/256
+     * key2: value2
+     * <p>
+     * would return size 2
+     *
+     * @return the count of real properties
+     */
+    int size() {
+        return getPropertyKeysExcludingProtectionSchemes().size();
+    }
+
+    /**
+     * Returns the complete set of property keys in the NiFiRegistryProperties,
+     * including any protection keys (i.e. 'x.y.z.protected').
+     *
+     * @return the set of property keys
+     */
+    Set<String> getPropertyKeysIncludingProtectionSchemes() {
+        return getInternalNiFiProperties().getPropertyKeys();
+    }
+
+    /**
+     * Returns the set of property keys in the NiFiRegistryProperties,
+     * excluding any protection keys (i.e. 'x.y.z.protected').
+     *
+     * @return the set of property keys
+     */
+    Set<String> getPropertyKeysExcludingProtectionSchemes() {
+        Set<String> filteredKeys = getPropertyKeysIncludingProtectionSchemes();
+        filteredKeys.removeIf(p -> p.endsWith(".protected"));
+        return filteredKeys;
+    }
+
+    /**
+     * Splits a single string containing multiple property keys into a List.
+     *
+     * Delimited by ',' or ';' and ignores leading and trailing whitespace around delimiter.
+     *
+     * @param multipleProperties a single String containing multiple properties, i.e.
+     *                           "nifi.registry.property.1; nifi.registry.property.2, nifi.registry.property.3"
+     * @return a List containing the split and trimmed properties
+     */
+    private static List<String> splitMultipleProperties(String multipleProperties) {
+        if (multipleProperties == null || multipleProperties.trim().isEmpty()) {
+            return new ArrayList<>(0);
+        } else {
+            List<String> properties = new ArrayList<>(asList(multipleProperties.split("\\s*[,;]\\s*")));
+            for (int i = 0; i < properties.size(); i++) {
+                properties.set(i, properties.get(i).trim());
+            }
+            return properties;
+        }
+    }
+
+    /**
+     * Returns a list of the keys identifying "sensitive" properties.
+     *
+     * There is a default list, and additional keys can be provided in the
+     * {@code nifi.registry.sensitive.props.additional.keys} property in {@code nifi-registry.properties}.
+     *
+     * @return the list of sensitive property keys
+     */
+    public List<String> getSensitivePropertyKeys() {
+        String additionalPropertiesString = getProperty(ADDITIONAL_SENSITIVE_PROPERTIES_KEY);
+        if (additionalPropertiesString == null || additionalPropertiesString.trim().isEmpty()) {
+            return DEFAULT_SENSITIVE_PROPERTIES;
+        } else {
+            List<String> additionalProperties = splitMultipleProperties(additionalPropertiesString);
+            /* Remove this key if it was accidentally provided as a sensitive key
+             * because we cannot protect it and read from it
+            */
+            if (additionalProperties.contains(ADDITIONAL_SENSITIVE_PROPERTIES_KEY)) {
+                logger.warn("The key '{}' contains itself. This is poor practice and should be removed", ADDITIONAL_SENSITIVE_PROPERTIES_KEY);
+                additionalProperties.remove(ADDITIONAL_SENSITIVE_PROPERTIES_KEY);
+            }
+            additionalProperties.addAll(DEFAULT_SENSITIVE_PROPERTIES);
+            return additionalProperties;
+        }
+    }
+
+    /**
+     * Returns a list of the keys identifying "sensitive" properties. There is a default list,
+     * and additional keys can be provided in the {@code nifi.sensitive.props.additional.keys} property in {@code nifi.properties}.
+     *
+     * @return the list of sensitive property keys
+     */
+    public List<String> getPopulatedSensitivePropertyKeys() {
+        List<String> allSensitiveKeys = getSensitivePropertyKeys();
+        return allSensitiveKeys.stream().filter(k -> StringUtils.isNotBlank(getProperty(k))).collect(Collectors.toList());
+    }
+
+    /**
+     * Returns true if any sensitive keys are protected.
+     *
+     * @return true if any key is protected; false otherwise
+     */
+    public boolean hasProtectedKeys() {
+        List<String> sensitiveKeys = getSensitivePropertyKeys();
+        for (String k : sensitiveKeys) {
+            if (isPropertyProtected(k)) {
+                return true;
+            }
+        }
+        return false;
+    }
+
+    /**
+     * Returns a Map of the keys identifying "sensitive" properties that are currently protected and the "protection" key for each.
+     *
+     * This may or may not include all properties marked as sensitive.
+     *
+     * @return the Map of protected property keys and the protection identifier for each
+     */
+    public Map<String, String> getProtectedPropertyKeys() {
+        List<String> sensitiveKeys = getSensitivePropertyKeys();
+
+        Map<String, String> traditionalProtectedProperties = new HashMap<>();
+        for (String key : sensitiveKeys) {
+            String protection = getProperty(getProtectionKey(key));
+            if (StringUtils.isNotBlank(protection) && StringUtils.isNotBlank(getProperty(key))) {
+                traditionalProtectedProperties.put(key, protection);
+            }
+        }
+
+        return traditionalProtectedProperties;
+    }
+
+    /**
+     * Returns the unique set of all protection schemes currently in use for this instance.
+     *
+     * @return the set of protection schemes
+     */
+    public Set<String> getProtectionSchemes() {
+        return new HashSet<>(getProtectedPropertyKeys().values());
+    }
+
+    /**
+     * Returns a percentage of the total number of populated properties marked as sensitive that are currently protected.
+     *
+     * @return the percent of sensitive properties marked as protected
+     */
+    public int getPercentOfSensitivePropertiesProtected() {
+        return (int) Math.round(getProtectedPropertyKeys().size() / ((double) getPopulatedSensitivePropertyKeys().size()) * 100);
+    }
+
+    /**
+     * Returns true if the property identified by this key is considered sensitive in this instance of {@code NiFiProperties}.
+     * Some properties are sensitive by default, while others can be specified by
+     * {@link ProtectedNiFiRegistryProperties#ADDITIONAL_SENSITIVE_PROPERTIES_KEY}.
+     *
+     * @param key the key
+     * @return true if it is sensitive
+     * @see ProtectedNiFiRegistryProperties#getSensitivePropertyKeys()
+     */
+    public boolean isPropertySensitive(String key) {
+        // If the explicit check for ADDITIONAL_SENSITIVE_PROPERTIES_KEY is not here, this could loop infinitely
+        return key != null && !key.equals(ADDITIONAL_SENSITIVE_PROPERTIES_KEY) && getSensitivePropertyKeys().contains(key.trim());
+    }
+
+    /**
+     * Returns true if the property identified by this key is considered protected in this instance of {@code NiFiProperties}.
+     * The property value is protected if the key is sensitive and the sibling key of key.protected is present.
+     *
+     * @param key the key
+     * @return true if it is currently marked as protected
+     * @see ProtectedNiFiRegistryProperties#getSensitivePropertyKeys()
+     */
+    public boolean isPropertyProtected(String key) {
+        return key != null && isPropertySensitive(key) && !StringUtils.isBlank(getProperty(getProtectionKey(key)));
+    }
+
+    /**
+     * Returns the sibling property key which specifies the protection scheme for this key.
+     * <p>
+     * Example:
+     * <p>
+     * nifi.registry.sensitive.key=ABCXYZ
+     * nifi.registry.sensitive.key.protected=aes/gcm/256
+     * <p>
+     * nifi.registry.sensitive.key -> nifi.sensitive.key.protected
+     *
+     * @param key the key identifying the sensitive property
+     * @return the key identifying the protection scheme for the sensitive property
+     */
+    public static String getProtectionKey(String key) {
+        if (key == null || key.isEmpty()) {
+            throw new IllegalArgumentException("Cannot find protection key for null key");
+        }
+
+        return key + ".protected";
+    }
+
+    /**
+     * Returns the unprotected {@link NiFiRegistryProperties} instance. If none of the
+     * properties loaded are marked as protected, it will simply pass through the
+     * internal instance. If any are protected, it will drop the protection scheme keys
+     * and translate each protected value (encrypted, HSM-retrieved, etc.) into the raw
+     * value and store it under the original key.
+     * <p>
+     * If any property fails to unprotect, it will save that key and continue. After
+     * attempting all properties, it will throw an exception containing all failed
+     * properties. This is necessary because the order is not enforced, so all failed
+     * properties should be gathered together.
+     *
+     * @return the NiFiRegistryProperties instance with all raw values
+     * @throws SensitivePropertyProtectionException if there is a problem unprotecting one or more keys
+     */
+    public NiFiRegistryProperties getUnprotectedProperties() throws SensitivePropertyProtectionException {
+        if (hasProtectedKeys()) {
+            logger.debug("There are {} protected properties of {} sensitive properties ({}%)",
+                    getProtectedPropertyKeys().size(),
+                    getPopulatedSensitivePropertyKeys().size(),
+                    getPercentOfSensitivePropertiesProtected());
+
+            NiFiRegistryProperties unprotectedProperties = new NiFiRegistryProperties();
+
+            Set<String> failedKeys = new HashSet<>();
+
+            for (String key : getPropertyKeysExcludingProtectionSchemes()) {
+                /* Three kinds of keys
+                 * 1. protection schemes -- skip
+                 * 2. protected keys -- unprotect and copy
+                 * 3. normal keys -- copy over
+                 */
+                if (key.endsWith(".protected")) {
+                    // Do nothing
+                } else if (isPropertyProtected(key)) {
+                    try {
+                        unprotectedProperties.setProperty(key, unprotectValue(key, getProperty(key)));
+                    } catch (SensitivePropertyProtectionException e) {
+                        logger.warn("Failed to unprotect '{}'", key, e);
+                        failedKeys.add(key);
+                    }
+                } else {
+                    unprotectedProperties.setProperty(key, getProperty(key));
+                }
+            }
+
+            if (!failedKeys.isEmpty()) {
+                if (failedKeys.size() > 1) {
+                    logger.warn("Combining {} failed keys [{}] into single exception", failedKeys.size(), StringUtils.join(failedKeys, ", "));
+                    throw new MultipleSensitivePropertyProtectionException("Failed to unprotect keys", failedKeys);
+                } else {
+                    throw new SensitivePropertyProtectionException("Failed to unprotect key " + failedKeys.iterator().next());
+                }
+            }
+
+            return unprotectedProperties;
+        } else {
+            logger.debug("No protected properties");
+            return getInternalNiFiProperties();
+        }
+    }
+
+    /**
+     * Registers a new {@link SensitivePropertyProvider}. This method will throw a {@link UnsupportedOperationException} if a provider is already registered for the protection scheme.
+     *
+     * @param sensitivePropertyProvider the provider
+     */
+    void addSensitivePropertyProvider(SensitivePropertyProvider sensitivePropertyProvider) {
+        if (sensitivePropertyProvider == null) {
+            throw new IllegalArgumentException("Cannot add null SensitivePropertyProvider");
+        }
+
+        if (getSensitivePropertyProviders().containsKey(sensitivePropertyProvider.getIdentifierKey())) {
+            throw new UnsupportedOperationException("Cannot overwrite existing sensitive property provider registered for " + sensitivePropertyProvider.getIdentifierKey());
+        }
+
+        getSensitivePropertyProviders().put(sensitivePropertyProvider.getIdentifierKey(), sensitivePropertyProvider);
+    }
+
+    private String getDefaultProtectionScheme() {
+        if (!getSensitivePropertyProviders().isEmpty()) {
+            List<String> schemes = new ArrayList<>(getSensitivePropertyProviders().keySet());
+            Collections.sort(schemes);
+            return schemes.get(0);
+        } else {
+            throw new IllegalStateException("No registered protection schemes");
+        }
+    }
+
+    /**
+     * Returns a new instance of {@link NiFiRegistryProperties} with all populated sensitive values protected by the default protection scheme.
+     *
+     * Plain non-sensitive values are copied directly.
+     *
+     * @return the protected properties in a {@link NiFiRegistryProperties} object
+     * @throws IllegalStateException if no protection schemes are registered
+     */
+    NiFiRegistryProperties protectPlainProperties() {
+        try {
+            return protectPlainProperties(getDefaultProtectionScheme());
+        } catch (IllegalStateException e) {
+            final String msg = "Cannot protect properties with default scheme if no protection schemes are registered";
+            logger.warn(msg);
+            throw new IllegalStateException(msg, e);
+        }
+    }
+
+    /**
+     * Returns a new instance of {@link NiFiRegistryProperties} with all populated sensitive values protected by the provided protection scheme.
+     *
+     * Plain non-sensitive values are copied directly.
+     *
+     * @param protectionScheme the identifier key of the {@link SensitivePropertyProvider} to use
+     * @return the protected properties in a {@link NiFiRegistryProperties} object
+     */
+    NiFiRegistryProperties protectPlainProperties(String protectionScheme) {
+        SensitivePropertyProvider spp = getSensitivePropertyProvider(protectionScheme);
+
+        NiFiRegistryProperties protectedProperties = new NiFiRegistryProperties();
+
+        // Copy over the plain keys
+        Set<String> plainKeys = getPropertyKeysExcludingProtectionSchemes();
+        plainKeys.removeAll(getSensitivePropertyKeys());
+        for (String key : plainKeys) {
+            protectedProperties.setProperty(key, getInternalNiFiProperties().getProperty(key));
+        }
+
+        // Add the protected keys and the protection schemes
+        for (String key : getSensitivePropertyKeys()) {
+            final String plainValue = getProperty(key);
+            if (plainValue != null && !plainValue.trim().isEmpty()) {
+                final String protectedValue = spp.protect(plainValue);
+                protectedProperties.setProperty(key, protectedValue);
+                protectedProperties.setProperty(getProtectionKey(key), protectionScheme);
+            }
+        }
+
+        return protectedProperties;
+    }
+
+    /**
+     * Returns the number of properties that are marked as protected in the provided {@link NiFiRegistryProperties} instance
+     * without requiring external creation of a {@link ProtectedNiFiRegistryProperties} instance.
+     *
+     * @param plainProperties the instance to count protected properties
+     * @return the number of protected properties
+     */
+    public static int countProtectedProperties(NiFiRegistryProperties plainProperties) {
+        return new ProtectedNiFiRegistryProperties(plainProperties).getProtectedPropertyKeys().size();
+    }
+
+    /**
+     * Returns the number of properties that are marked as sensitive in the provided {@link NiFiRegistryProperties} instance
+     * without requiring external creation of a {@link ProtectedNiFiRegistryProperties} instance.
+     *
+     * @param plainProperties the instance to count sensitive properties
+     * @return the number of sensitive properties
+     */
+    public static int countSensitiveProperties(NiFiRegistryProperties plainProperties) {
+        return new ProtectedNiFiRegistryProperties(plainProperties).getSensitivePropertyKeys().size();
+    }
+
+    @Override
+    public String toString() {
+        final Set<String> providers = getSensitivePropertyProviders().keySet();
+        return new StringBuilder("ProtectedNiFiProperties instance with ")
+                .append(getPropertyKeysIncludingProtectionSchemes().size())
+                .append(" properties (")
+                .append(getProtectedPropertyKeys().size())
+                .append(" protected) and ")
+                .append(providers.size())
+                .append(" sensitive property providers: ")
+                .append(StringUtils.join(providers, ", "))
+                .toString();
+    }
+
+    /**
+     * Returns the local provider cache (null-safe) as a Map of protection schemes -> implementations.
+     *
+     * @return the map
+     */
+    private Map<String, SensitivePropertyProvider> getSensitivePropertyProviders() {
+        if (localProviderCache == null) {
+            localProviderCache = new HashMap<>();
+        }
+
+        return localProviderCache;
+    }
+
+    private SensitivePropertyProvider getSensitivePropertyProvider(String protectionScheme) {
+        if (isProviderAvailable(protectionScheme)) {
+            return getSensitivePropertyProviders().get(protectionScheme);
+        } else {
+            throw new SensitivePropertyProtectionException("No provider available for " + protectionScheme);
+        }
+    }
+
+    private boolean isProviderAvailable(String protectionScheme) {
+        return getSensitivePropertyProviders().containsKey(protectionScheme);
+    }
+
+    /**
+     * If the value is protected, unprotects it and returns it. If not, returns the original value.
+     *
+     * @param key            the retrieved property key
+     * @param retrievedValue the retrieved property value
+     * @return the unprotected value
+     */
+    private String unprotectValue(String key, String retrievedValue) {
+        // Checks if the key is sensitive and marked as protected
+        if (isPropertyProtected(key)) {
+            final String protectionScheme = getProperty(getProtectionKey(key));
+
+            // No provider registered for this scheme, so just return the value
+            if (!isProviderAvailable(protectionScheme)) {
+                logger.warn("No provider available for {} so passing the protected {} value back", protectionScheme, key);
+                return retrievedValue;
+            }
+
+            try {
+                SensitivePropertyProvider sensitivePropertyProvider = getSensitivePropertyProvider(protectionScheme);
+                return sensitivePropertyProvider.unprotect(retrievedValue);
+            } catch (SensitivePropertyProtectionException e) {
+                throw new SensitivePropertyProtectionException("Error unprotecting value for " + key, e.getCause());
+            }
+        }
+        return retrievedValue;
+    }
+}

http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/6f26290d/nifi-registry-core/nifi-registry-properties/src/main/java/org/apache/nifi/registry/properties/SensitivePropertyProtectionException.java
----------------------------------------------------------------------
diff --git a/nifi-registry-core/nifi-registry-properties/src/main/java/org/apache/nifi/registry/properties/SensitivePropertyProtectionException.java b/nifi-registry-core/nifi-registry-properties/src/main/java/org/apache/nifi/registry/properties/SensitivePropertyProtectionException.java
new file mode 100644
index 0000000..2ffa902
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-properties/src/main/java/org/apache/nifi/registry/properties/SensitivePropertyProtectionException.java
@@ -0,0 +1,89 @@
+/*
+ * 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.properties;
+
+public class SensitivePropertyProtectionException extends RuntimeException {
+    /**
+     * Constructs a new throwable with {@code null} as its detail message.
+     * The cause is not initialized, and may subsequently be initialized by a
+     * call to {@link #initCause}.
+     * <p>
+     * <p>The {@link #fillInStackTrace()} method is called to initialize
+     * the stack trace data in the newly created throwable.
+     */
+    public SensitivePropertyProtectionException() {
+    }
+
+    /**
+     * Constructs a new throwable with the specified detail message.  The
+     * cause is not initialized, and may subsequently be initialized by
+     * a call to {@link #initCause}.
+     * <p>
+     * <p>The {@link #fillInStackTrace()} method is called to initialize
+     * the stack trace data in the newly created throwable.
+     *
+     * @param message the detail message. The detail message is saved for
+     *                later retrieval by the {@link #getMessage()} method.
+     */
+    public SensitivePropertyProtectionException(String message) {
+        super(message);
+    }
+
+    /**
+     * Constructs a new throwable with the specified detail message and
+     * cause.  <p>Note that the detail message associated with
+     * {@code cause} is <i>not</i> automatically incorporated in
+     * this throwable's detail message.
+     * <p>
+     * <p>The {@link #fillInStackTrace()} method is called to initialize
+     * the stack trace data in the newly created throwable.
+     *
+     * @param message the detail message (which is saved for later retrieval
+     *                by the {@link #getMessage()} method).
+     * @param cause   the cause (which is saved for later retrieval by the
+     *                {@link #getCause()} method).  (A {@code null} value is
+     *                permitted, and indicates that the cause is nonexistent or
+     *                unknown.)
+     */
+    public SensitivePropertyProtectionException(String message, Throwable cause) {
+        super(message, cause);
+    }
+
+    /**
+     * Constructs a new throwable with the specified cause and a detail
+     * message of {@code (cause==null ? null : cause.toString())} (which
+     * typically contains the class and detail message of {@code cause}).
+     * This constructor is useful for throwables that are little more than
+     * wrappers for other throwables (for example, PrivilegedActionException).
+     * <p>
+     * <p>The {@link #fillInStackTrace()} method is called to initialize
+     * the stack trace data in the newly created throwable.
+     *
+     * @param cause the cause (which is saved for later retrieval by the
+     *              {@link #getCause()} method).  (A {@code null} value is
+     *              permitted, and indicates that the cause is nonexistent or
+     *              unknown.)
+     */
+    public SensitivePropertyProtectionException(Throwable cause) {
+        super(cause);
+    }
+
+    @Override
+    public String toString() {
+        return "SensitivePropertyProtectionException: " + getLocalizedMessage();
+    }
+}

http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/6f26290d/nifi-registry-core/nifi-registry-properties/src/main/java/org/apache/nifi/registry/properties/SensitivePropertyProvider.java
----------------------------------------------------------------------
diff --git a/nifi-registry-core/nifi-registry-properties/src/main/java/org/apache/nifi/registry/properties/SensitivePropertyProvider.java b/nifi-registry-core/nifi-registry-properties/src/main/java/org/apache/nifi/registry/properties/SensitivePropertyProvider.java
new file mode 100644
index 0000000..c0dd43c
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-properties/src/main/java/org/apache/nifi/registry/properties/SensitivePropertyProvider.java
@@ -0,0 +1,52 @@
+/*
+ * 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.properties;
+
+public interface SensitivePropertyProvider {
+
+    /**
+     * Returns the name of the underlying implementation.
+     *
+     * @return the name of this sensitive property provider
+     */
+    String getName();
+
+    /**
+     * Returns the key used to identify the provider implementation in {@code nifi.properties}.
+     *
+     * @return the key to persist in the sibling property
+     */
+    String getIdentifierKey();
+
+    /**
+     * Returns the "protected" form of this value. This is a form which can safely be persisted in the {@code nifi.properties} file without compromising the value.
+     * An encryption-based provider would return a cipher text, while a remote-lookup provider could return a unique ID to retrieve the secured value.
+     *
+     * @param unprotectedValue the sensitive value
+     * @return the value to persist in the {@code nifi.properties} file
+     */
+    String protect(String unprotectedValue) throws SensitivePropertyProtectionException;
+
+    /**
+     * Returns the "unprotected" form of this value. This is the raw sensitive value which is used by the application logic.
+     * An encryption-based provider would decrypt a cipher text and return the plaintext, while a remote-lookup provider could retrieve the secured value.
+     *
+     * @param protectedValue the protected value read from the {@code nifi.properties} file
+     * @return the raw value to be used by the application
+     */
+    String unprotect(String protectedValue) throws SensitivePropertyProtectionException;
+}

http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/6f26290d/nifi-registry-core/nifi-registry-properties/src/main/java/org/apache/nifi/registry/properties/SensitivePropertyProviderFactory.java
----------------------------------------------------------------------
diff --git a/nifi-registry-core/nifi-registry-properties/src/main/java/org/apache/nifi/registry/properties/SensitivePropertyProviderFactory.java b/nifi-registry-core/nifi-registry-properties/src/main/java/org/apache/nifi/registry/properties/SensitivePropertyProviderFactory.java
new file mode 100644
index 0000000..c9d4313
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-properties/src/main/java/org/apache/nifi/registry/properties/SensitivePropertyProviderFactory.java
@@ -0,0 +1,23 @@
+/*
+ * 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.properties;
+
+public interface SensitivePropertyProviderFactory {
+
+    SensitivePropertyProvider getProvider();
+
+}

http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/6f26290d/nifi-registry-core/nifi-registry-properties/src/main/java/org/apache/nifi/registry/properties/util/IdentityMapping.java
----------------------------------------------------------------------
diff --git a/nifi-registry-core/nifi-registry-properties/src/main/java/org/apache/nifi/registry/properties/util/IdentityMapping.java b/nifi-registry-core/nifi-registry-properties/src/main/java/org/apache/nifi/registry/properties/util/IdentityMapping.java
new file mode 100644
index 0000000..df3bbe6
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-properties/src/main/java/org/apache/nifi/registry/properties/util/IdentityMapping.java
@@ -0,0 +1,48 @@
+/*
+ * 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.properties.util;
+
+import java.util.regex.Pattern;
+
+/**
+ * Holder to pass around the key, pattern, and replacement from an identity mapping in NiFiProperties.
+ */
+public class IdentityMapping {
+
+    private final String key;
+    private final Pattern pattern;
+    private final String replacementValue;
+
+    public IdentityMapping(String key, Pattern pattern, String replacementValue) {
+        this.key = key;
+        this.pattern = pattern;
+        this.replacementValue = replacementValue;
+    }
+
+    public String getKey() {
+        return key;
+    }
+
+    public Pattern getPattern() {
+        return pattern;
+    }
+
+    public String getReplacementValue() {
+        return replacementValue;
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/6f26290d/nifi-registry-core/nifi-registry-properties/src/main/java/org/apache/nifi/registry/properties/util/IdentityMappingUtil.java
----------------------------------------------------------------------
diff --git a/nifi-registry-core/nifi-registry-properties/src/main/java/org/apache/nifi/registry/properties/util/IdentityMappingUtil.java b/nifi-registry-core/nifi-registry-properties/src/main/java/org/apache/nifi/registry/properties/util/IdentityMappingUtil.java
new file mode 100644
index 0000000..3c9208c
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-properties/src/main/java/org/apache/nifi/registry/properties/util/IdentityMappingUtil.java
@@ -0,0 +1,145 @@
+/*
+ * 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.properties.util;
+
+import org.apache.commons.lang3.StringUtils;
+import org.apache.nifi.registry.properties.NiFiRegistryProperties;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.List;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+public class IdentityMappingUtil {
+
+    private static final Logger LOGGER = LoggerFactory.getLogger(IdentityMappingUtil.class);
+    private static final Pattern backReferencePattern = Pattern.compile("\\$(\\d+)");
+
+    /**
+     * Builds the identity mappings from NiFiRegistryProperties.
+     *
+     * @param properties the NiFiRegistryProperties instance
+     * @return a list of identity mappings
+     */
+    public static List<IdentityMapping> getIdentityMappings(final NiFiRegistryProperties properties) {
+        final List<IdentityMapping> mappings = new ArrayList<>();
+
+        // go through each property
+        for (String propertyName : properties.getPropertyKeys()) {
+            if (StringUtils.startsWith(propertyName, NiFiRegistryProperties.SECURITY_IDENTITY_MAPPING_PATTERN_PREFIX)) {
+                final String key = StringUtils.substringAfter(propertyName, NiFiRegistryProperties.SECURITY_IDENTITY_MAPPING_PATTERN_PREFIX);
+                final String identityPattern = properties.getProperty(propertyName);
+
+                if (StringUtils.isBlank(identityPattern)) {
+                    LOGGER.warn("Identity Mapping property {} was found, but was empty", new Object[]{propertyName});
+                    continue;
+                }
+
+                final String identityValueProperty = NiFiRegistryProperties.SECURITY_IDENTITY_MAPPING_VALUE_PREFIX + key;
+                final String identityValue = properties.getProperty(identityValueProperty);
+
+                if (StringUtils.isBlank(identityValue)) {
+                    LOGGER.warn("Identity Mapping property {} was found, but corresponding value {} was not found",
+                            new Object[]{propertyName, identityValueProperty});
+                    continue;
+                }
+
+                final IdentityMapping identityMapping = new IdentityMapping(key, Pattern.compile(identityPattern), identityValue);
+                mappings.add(identityMapping);
+
+                LOGGER.debug("Found Identity Mapping with key = {}, pattern = {}, value = {}",
+                        new Object[] {key, identityPattern, identityValue});
+            }
+        }
+
+        // sort the list by the key so users can control the ordering in nifi-registry.properties
+        Collections.sort(mappings, new Comparator<IdentityMapping>() {
+            @Override
+            public int compare(IdentityMapping m1, IdentityMapping m2) {
+                return m1.getKey().compareTo(m2.getKey());
+            }
+        });
+
+        return mappings;
+    }
+
+    /**
+     * Checks the given identity against each provided mapping and performs the mapping using the first one that matches.
+     * If none match then the identity is returned as is.
+     *
+     * @param identity the identity to map
+     * @param mappings the mappings
+     * @return the mapped identity, or the same identity if no mappings matched
+     */
+    public static String mapIdentity(final String identity, List<IdentityMapping> mappings) {
+        for (IdentityMapping mapping : mappings) {
+            Matcher m = mapping.getPattern().matcher(identity);
+            if (m.matches()) {
+                final String pattern = mapping.getPattern().pattern();
+                final String replacementValue = escapeLiteralBackReferences(mapping.getReplacementValue(), m.groupCount());
+                return identity.replaceAll(pattern, replacementValue);
+            }
+        }
+
+        return identity;
+    }
+
+    // If we find a back reference that is not valid, then we will treat it as a literal string. For example, if we have 3 capturing
+    // groups and the Replacement Value has the value is "I owe $8 to him", then we want to treat the $8 as a literal "$8", rather
+    // than attempting to use it as a back reference.
+    private static String escapeLiteralBackReferences(final String unescaped, final int numCapturingGroups) {
+        if (numCapturingGroups == 0) {
+            return unescaped;
+        }
+
+        String value = unescaped;
+        final Matcher backRefMatcher = backReferencePattern.matcher(value);
+        while (backRefMatcher.find()) {
+            final String backRefNum = backRefMatcher.group(1);
+            if (backRefNum.startsWith("0")) {
+                continue;
+            }
+            final int originalBackRefIndex = Integer.parseInt(backRefNum);
+            int backRefIndex = originalBackRefIndex;
+
+            // if we have a replacement value like $123, and we have less than 123 capturing groups, then
+            // we want to truncate the 3 and use capturing group 12; if we have less than 12 capturing groups,
+            // then we want to truncate the 2 and use capturing group 1; if we don't have a capturing group then
+            // we want to truncate the 1 and get 0.
+            while (backRefIndex > numCapturingGroups && backRefIndex >= 10) {
+                backRefIndex /= 10;
+            }
+
+            if (backRefIndex > numCapturingGroups) {
+                final StringBuilder sb = new StringBuilder(value.length() + 1);
+                final int groupStart = backRefMatcher.start(1);
+
+                sb.append(value.substring(0, groupStart - 1));
+                sb.append("\\");
+                sb.append(value.substring(groupStart - 1));
+                value = sb.toString();
+            }
+        }
+
+        return value;
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/6f26290d/nifi-registry-core/nifi-registry-properties/src/main/java/org/apache/nifi/registry/security/crypto/BootstrapFileCryptoKeyProvider.java
----------------------------------------------------------------------
diff --git a/nifi-registry-core/nifi-registry-properties/src/main/java/org/apache/nifi/registry/security/crypto/BootstrapFileCryptoKeyProvider.java b/nifi-registry-core/nifi-registry-properties/src/main/java/org/apache/nifi/registry/security/crypto/BootstrapFileCryptoKeyProvider.java
new file mode 100644
index 0000000..191b5e2
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-properties/src/main/java/org/apache/nifi/registry/security/crypto/BootstrapFileCryptoKeyProvider.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.security.crypto;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.IOException;
+
+/**
+ * An implementation of {@link CryptoKeyProvider} that loads the key from disk every time it is needed.
+ *
+ * The persistence-backing of the key is in the bootstrap.conf file, which must be provided to the
+ * constructor of this class.
+ *
+ * As key access for sensitive value decryption is only used a few times during server initialization,
+ * this implementation trades efficiency for security by only keeping the key in memory with an
+ * in-scope reference for a brief period of time (assuming callers do not maintain an in-scope reference).
+ *
+ * @see CryptoKeyProvider
+ */
+public class BootstrapFileCryptoKeyProvider implements CryptoKeyProvider {
+
+    private static final Logger logger = LoggerFactory.getLogger(BootstrapFileCryptoKeyProvider.class);
+
+    private final String bootstrapFile;
+
+    /**
+     * Construct a new instance backed by the contents of a bootstrap.conf file.
+     *
+     * @param bootstrapFilePath The path to the bootstrap.conf file for this instance of NiFi Registry.
+     *                          Must not be null.
+     */
+    public BootstrapFileCryptoKeyProvider(final String bootstrapFilePath) {
+        if (bootstrapFilePath == null) {
+            throw new IllegalArgumentException(BootstrapFileCryptoKeyProvider.class.getSimpleName() + " cannot be initialized with null bootstrap file path.");
+        }
+        this.bootstrapFile = bootstrapFilePath;
+    }
+
+    /**
+     * @return The bootstrap file path that backs this provider instance.
+     */
+    public String getBootstrapFile() {
+        return bootstrapFile;
+    }
+
+    @Override
+    public String getKey() throws MissingCryptoKeyException {
+        try {
+            return CryptoKeyLoader.extractKeyFromBootstrapFile(this.bootstrapFile);
+        } catch (IOException ioe) {
+            final String errMsg = "Loading the master crypto key from bootstrap file '" + bootstrapFile + "' failed due to IOException.";
+            logger.warn(errMsg);
+            throw new MissingCryptoKeyException(errMsg, ioe);
+        }
+
+    }
+
+    @Override
+    public String toString() {
+        return "BootstrapFileCryptoKeyProvider{" +
+                "bootstrapFile='" + bootstrapFile + '\'' +
+                '}';
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/6f26290d/nifi-registry-core/nifi-registry-properties/src/main/java/org/apache/nifi/registry/security/crypto/CryptoKeyLoader.java
----------------------------------------------------------------------
diff --git a/nifi-registry-core/nifi-registry-properties/src/main/java/org/apache/nifi/registry/security/crypto/CryptoKeyLoader.java b/nifi-registry-core/nifi-registry-properties/src/main/java/org/apache/nifi/registry/security/crypto/CryptoKeyLoader.java
new file mode 100644
index 0000000..d828773
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-properties/src/main/java/org/apache/nifi/registry/security/crypto/CryptoKeyLoader.java
@@ -0,0 +1,87 @@
+/*
+ * 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.security.crypto;
+
+import org.apache.commons.lang3.StringUtils;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.File;
+import java.io.IOException;
+import java.nio.file.Files;
+import java.nio.file.Paths;
+import java.util.Optional;
+import java.util.stream.Stream;
+
+public class CryptoKeyLoader {
+
+    private static final Logger logger = LoggerFactory.getLogger(CryptoKeyLoader.class);
+
+    private static final String BOOTSTRAP_KEY_PREFIX = "nifi.registry.bootstrap.sensitive.key=";
+
+    /**
+     * Returns the key (if any) used to encrypt sensitive properties.
+     * The key extracted from the bootstrap.conf file at the specified location.
+     *
+     * @param bootstrapPath the path to the bootstrap file
+     * @return the key in hexadecimal format, or {@link CryptoKeyProvider#EMPTY_KEY} if the key is null or empty
+     * @throws IOException if the file is not readable
+     */
+    public static String extractKeyFromBootstrapFile(String bootstrapPath) throws IOException {
+        File bootstrapFile;
+        if (StringUtils.isBlank(bootstrapPath)) {
+            logger.error("Cannot read from bootstrap.conf file to extract encryption key; location not specified");
+            throw new IOException("Cannot read from bootstrap.conf without file location");
+        } else {
+            bootstrapFile = new File(bootstrapPath);
+        }
+
+        String keyValue;
+        if (bootstrapFile.exists() && bootstrapFile.canRead()) {
+            try (Stream<String> stream = Files.lines(Paths.get(bootstrapFile.getAbsolutePath()))) {
+                Optional<String> keyLine = stream.filter(l -> l.startsWith(BOOTSTRAP_KEY_PREFIX)).findFirst();
+                if (keyLine.isPresent()) {
+                    keyValue = keyLine.get().split("=", 2)[1];
+                    keyValue = checkHexKey(keyValue);
+                } else {
+                    keyValue = CryptoKeyProvider.EMPTY_KEY;
+                }
+            } catch (IOException e) {
+                logger.error("Cannot read from bootstrap.conf file at {} to extract encryption key", bootstrapFile.getAbsolutePath());
+                throw new IOException("Cannot read from bootstrap.conf", e);
+            }
+        } else {
+            logger.error("Cannot read from bootstrap.conf file at {} to extract encryption key -- file is missing or permissions are incorrect", bootstrapFile.getAbsolutePath());
+            throw new IOException("Cannot read from bootstrap.conf");
+        }
+
+        if (CryptoKeyProvider.EMPTY_KEY.equals(keyValue)) {
+            logger.info("No encryption key present in the bootstrap.conf file at {}", bootstrapFile.getAbsolutePath());
+        }
+
+        return keyValue;
+    }
+
+    private static String checkHexKey(String input) {
+        if (input == null || input.trim().isEmpty()) {
+            logger.debug("Checking the hex key value that was loaded determined the key is empty.");
+            return CryptoKeyProvider.EMPTY_KEY;
+        }
+        return input;
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/6f26290d/nifi-registry-core/nifi-registry-properties/src/main/java/org/apache/nifi/registry/security/crypto/CryptoKeyProvider.java
----------------------------------------------------------------------
diff --git a/nifi-registry-core/nifi-registry-properties/src/main/java/org/apache/nifi/registry/security/crypto/CryptoKeyProvider.java b/nifi-registry-core/nifi-registry-properties/src/main/java/org/apache/nifi/registry/security/crypto/CryptoKeyProvider.java
new file mode 100644
index 0000000..bab8d7c
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-properties/src/main/java/org/apache/nifi/registry/security/crypto/CryptoKeyProvider.java
@@ -0,0 +1,68 @@
+/*
+ * 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.security.crypto;
+
+/**
+ * A simple interface that wraps a key that can be used for encryption and decryption.
+ * This allows for more flexibility with the lifecycle of keys and how other classes
+ * can declare dependencies for keys, by depending on a CryptoKeyProvider that will provided
+ * at runtime.
+ */
+public interface CryptoKeyProvider {
+
+    /**
+     * A string literal that indicates the contents of a key are empty.
+     * Can also be used in contexts that a null key is undesirable.
+     */
+    String EMPTY_KEY = "";
+
+    /**
+     * @return The crypto key known to this CryptoKeyProvider instance in hexadecimal format, or
+     *         {@link #EMPTY_KEY} if the key is empty.
+     * @throws MissingCryptoKeyException if the key cannot be provided or determined for any reason.
+     *         If the key is known to be empty, {@link #EMPTY_KEY} will be returned and a
+     *         CryptoKeyMissingException will not be thrown
+     */
+    String getKey() throws MissingCryptoKeyException;
+
+    /**
+     * @return A boolean indicating if the key value held by this CryptoKeyProvider is empty,
+     *         such as 'null' or empty string.
+     */
+    default boolean isEmpty() {
+        String key;
+        try {
+            key = getKey();
+        } catch (MissingCryptoKeyException e) {
+            return true;
+        }
+        return EMPTY_KEY.equals(key);
+    }
+
+    /**
+     * A string representation of this CryptoKeyProvider instance.
+     * <p>
+     * <p>
+     * Note: Implementations of this interface should take care not to leak sensitive
+     * key material in any strings they emmit, including in the toString implementation.
+     *
+     * @return A string representation of this CryptoKeyProvider instance.
+     */
+    @Override
+    public String toString();
+
+}

http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/6f26290d/nifi-registry-core/nifi-registry-properties/src/main/java/org/apache/nifi/registry/security/crypto/MissingCryptoKeyException.java
----------------------------------------------------------------------
diff --git a/nifi-registry-core/nifi-registry-properties/src/main/java/org/apache/nifi/registry/security/crypto/MissingCryptoKeyException.java b/nifi-registry-core/nifi-registry-properties/src/main/java/org/apache/nifi/registry/security/crypto/MissingCryptoKeyException.java
new file mode 100644
index 0000000..dbc3752
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-properties/src/main/java/org/apache/nifi/registry/security/crypto/MissingCryptoKeyException.java
@@ -0,0 +1,47 @@
+/*
+ * 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.security.crypto;
+
+/**
+ * An exception type used by a {@link CryptoKeyProvider} when a request for the key
+ * cannot be fulfilled for any reason.
+ *
+ * @see CryptoKeyProvider
+ */
+public class MissingCryptoKeyException extends Exception {
+
+    public MissingCryptoKeyException() {
+        super();
+    }
+
+    public MissingCryptoKeyException(String message) {
+        super(message);
+    }
+
+    public MissingCryptoKeyException(String message, Throwable cause) {
+        super(message, cause);
+    }
+
+    public MissingCryptoKeyException(Throwable cause) {
+        super(cause);
+    }
+
+    protected MissingCryptoKeyException(String message, Throwable cause, boolean enableSuppression, boolean writableStackTrace) {
+        super(message, cause, enableSuppression, writableStackTrace);
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/6f26290d/nifi-registry-core/nifi-registry-properties/src/test/groovy/org/apache/nifi/registry/properties/AESSensitivePropertyProviderFactoryTest.groovy
----------------------------------------------------------------------
diff --git a/nifi-registry-core/nifi-registry-properties/src/test/groovy/org/apache/nifi/registry/properties/AESSensitivePropertyProviderFactoryTest.groovy b/nifi-registry-core/nifi-registry-properties/src/test/groovy/org/apache/nifi/registry/properties/AESSensitivePropertyProviderFactoryTest.groovy
new file mode 100644
index 0000000..0d1d5e2
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-properties/src/test/groovy/org/apache/nifi/registry/properties/AESSensitivePropertyProviderFactoryTest.groovy
@@ -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.properties
+
+import org.bouncycastle.jce.provider.BouncyCastleProvider
+import org.junit.*
+import org.junit.runner.RunWith
+import org.junit.runners.JUnit4
+import org.slf4j.Logger
+import org.slf4j.LoggerFactory
+
+import javax.crypto.Cipher
+import java.security.Security
+
+@RunWith(JUnit4.class)
+class AESSensitivePropertyProviderFactoryTest extends GroovyTestCase {
+    private static final Logger logger = LoggerFactory.getLogger(AESSensitivePropertyProviderFactoryTest.class)
+
+    private static final String KEY_HEX_128 = "0123456789ABCDEFFEDCBA9876543210"
+    private static final String KEY_HEX_256 = KEY_HEX_128 * 2
+
+    @BeforeClass
+    public static void setUpOnce() throws Exception {
+        Security.addProvider(new BouncyCastleProvider())
+
+        logger.metaClass.methodMissing = { String name, args ->
+            logger.info("[${name?.toUpperCase()}] ${(args as List).join(" ")}")
+        }
+    }
+
+    @Before
+    public void setUp() throws Exception {
+    }
+
+    @After
+    public void tearDown() throws Exception {
+    }
+
+    @Test
+    public void testShouldGetProviderWithKey() throws Exception {
+        // Arrange
+        SensitivePropertyProviderFactory factory = new AESSensitivePropertyProviderFactory(KEY_HEX_128)
+
+        // Act
+        SensitivePropertyProvider provider = factory.getProvider()
+
+        // Assert
+        assert provider instanceof AESSensitivePropertyProvider
+        assert provider.@key
+        assert provider.@cipher
+    }
+
+    @Test
+    public void testShouldGetProviderWith256BitKey() throws Exception {
+        // Arrange
+        Assume.assumeTrue("JCE unlimited strength crypto policy must be installed for this test", Cipher.getMaxAllowedKeyLength("AES") > 128)
+        SensitivePropertyProviderFactory factory = new AESSensitivePropertyProviderFactory(KEY_HEX_256)
+
+        // Act
+        SensitivePropertyProvider provider = factory.getProvider()
+
+        // Assert
+        assert provider instanceof AESSensitivePropertyProvider
+        assert provider.@key
+        assert provider.@cipher
+    }
+}
\ No newline at end of file


[19/51] [partial] nifi-registry git commit: NIFIREG-201 Refactoring project structure to better isolate extensions

Posted by kd...@apache.org.
http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/6f26290d/nifi-registry-core/nifi-registry-security-utils/src/main/java/org/apache/nifi/registry/security/util/CertificateUtils.java
----------------------------------------------------------------------
diff --git a/nifi-registry-core/nifi-registry-security-utils/src/main/java/org/apache/nifi/registry/security/util/CertificateUtils.java b/nifi-registry-core/nifi-registry-security-utils/src/main/java/org/apache/nifi/registry/security/util/CertificateUtils.java
new file mode 100644
index 0000000..b14cb61
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-security-utils/src/main/java/org/apache/nifi/registry/security/util/CertificateUtils.java
@@ -0,0 +1,670 @@
+/*
+ * 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.security.util;
+
+import org.apache.commons.lang3.StringUtils;
+import org.apache.nifi.registry.security.util.KeyStoreUtils;
+import org.apache.nifi.registry.security.util.KeystoreType;
+import org.bouncycastle.asn1.ASN1Encodable;
+import org.bouncycastle.asn1.ASN1ObjectIdentifier;
+import org.bouncycastle.asn1.ASN1Set;
+import org.bouncycastle.asn1.DERSequence;
+import org.bouncycastle.asn1.pkcs.Attribute;
+import org.bouncycastle.asn1.pkcs.PKCSObjectIdentifiers;
+import org.bouncycastle.asn1.x500.AttributeTypeAndValue;
+import org.bouncycastle.asn1.x500.RDN;
+import org.bouncycastle.asn1.x500.X500Name;
+import org.bouncycastle.asn1.x500.style.BCStyle;
+import org.bouncycastle.asn1.x509.BasicConstraints;
+import org.bouncycastle.asn1.x509.ExtendedKeyUsage;
+import org.bouncycastle.asn1.x509.Extension;
+import org.bouncycastle.asn1.x509.Extensions;
+import org.bouncycastle.asn1.x509.KeyPurposeId;
+import org.bouncycastle.asn1.x509.KeyUsage;
+import org.bouncycastle.asn1.x509.SubjectPublicKeyInfo;
+import org.bouncycastle.cert.CertIOException;
+import org.bouncycastle.cert.X509CertificateHolder;
+import org.bouncycastle.cert.X509v3CertificateBuilder;
+import org.bouncycastle.cert.jcajce.JcaX509CertificateConverter;
+import org.bouncycastle.cert.jcajce.JcaX509ExtensionUtils;
+import org.bouncycastle.jce.provider.BouncyCastleProvider;
+import org.bouncycastle.operator.ContentSigner;
+import org.bouncycastle.operator.OperatorCreationException;
+import org.bouncycastle.operator.jcajce.JcaContentSignerBuilder;
+import org.bouncycastle.pkcs.jcajce.JcaPKCS10CertificationRequest;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import javax.naming.InvalidNameException;
+import javax.naming.ldap.LdapName;
+import javax.naming.ldap.Rdn;
+import javax.net.ssl.SSLPeerUnverifiedException;
+import javax.net.ssl.SSLSocket;
+import java.io.BufferedInputStream;
+import java.io.ByteArrayInputStream;
+import java.io.IOException;
+import java.math.BigInteger;
+import java.net.Socket;
+import java.net.URL;
+import java.security.KeyPair;
+import java.security.KeyStore;
+import java.security.NoSuchAlgorithmException;
+import java.security.PublicKey;
+import java.security.Security;
+import java.security.cert.Certificate;
+import java.security.cert.CertificateException;
+import java.security.cert.CertificateFactory;
+import java.security.cert.CertificateParsingException;
+import java.security.cert.X509Certificate;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.Date;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.TimeUnit;
+
+public final class CertificateUtils {
+    private static final Logger logger = LoggerFactory.getLogger(CertificateUtils.class);
+    private static final String PEER_NOT_AUTHENTICATED_MSG = "peer not authenticated";
+    private static final Map<ASN1ObjectIdentifier, Integer> dnOrderMap = createDnOrderMap();
+
+    static {
+        Security.addProvider(new BouncyCastleProvider());
+    }
+
+    /**
+     * The time in milliseconds that the last unique serial number was generated
+     */
+    private static long lastSerialNumberMillis = 0L;
+
+    /**
+     * An incrementor to add uniqueness to serial numbers generated in the same millisecond
+     */
+    private static int serialNumberIncrementor = 0;
+
+    /**
+     * BigInteger value to use for the base of the unique serial number
+     */
+    private static BigInteger millisecondBigInteger;
+
+    private static Map<ASN1ObjectIdentifier, Integer> createDnOrderMap() {
+        Map<ASN1ObjectIdentifier, Integer> orderMap = new HashMap<>();
+        int count = 0;
+        orderMap.put(BCStyle.CN, count++);
+        orderMap.put(BCStyle.L, count++);
+        orderMap.put(BCStyle.ST, count++);
+        orderMap.put(BCStyle.O, count++);
+        orderMap.put(BCStyle.OU, count++);
+        orderMap.put(BCStyle.C, count++);
+        orderMap.put(BCStyle.STREET, count++);
+        orderMap.put(BCStyle.DC, count++);
+        orderMap.put(BCStyle.UID, count++);
+        return Collections.unmodifiableMap(orderMap);
+    }
+
+    public enum ClientAuth {
+        NONE(0, "none"),
+        WANT(1, "want"),
+        NEED(2, "need");
+
+        private int value;
+        private String description;
+
+        ClientAuth(int value, String description) {
+            this.value = value;
+            this.description = description;
+        }
+
+        @Override
+        public String toString() {
+            return "Client Auth: " + this.description + " (" + this.value + ")";
+        }
+    }
+
+    /**
+     * Returns true if the given keystore can be loaded using the given keystore type and password. Returns false otherwise.
+     *
+     * @param keystore     the keystore to validate
+     * @param keystoreType the type of the keystore
+     * @param password     the password to access the keystore
+     * @return true if valid; false otherwise
+     */
+    public static boolean isStoreValid(final URL keystore, final KeystoreType keystoreType, final char[] password) {
+
+        if (keystore == null) {
+            throw new IllegalArgumentException("keystore may not be null");
+        } else if (keystoreType == null) {
+            throw new IllegalArgumentException("keystore type may not be null");
+        } else if (password == null) {
+            throw new IllegalArgumentException("password may not be null");
+        }
+
+        BufferedInputStream bis = null;
+        final KeyStore ks;
+        try {
+
+            // load the keystore
+            bis = new BufferedInputStream(keystore.openStream());
+            ks = KeyStoreUtils.getKeyStore(keystoreType.name());
+            ks.load(bis, password);
+
+            return true;
+
+        } catch (Exception e) {
+            return false;
+        } finally {
+            if (bis != null) {
+                try {
+                    bis.close();
+                } catch (final IOException ioe) {
+                    logger.warn("Failed to close input stream", ioe);
+                }
+            }
+        }
+    }
+
+    /**
+     * Extracts the username from the specified DN. If the username cannot be extracted because the CN is in an unrecognized format, the entire CN is returned. If the CN cannot be extracted because
+     * the DN is in an unrecognized format, the entire DN is returned.
+     *
+     * @param dn the dn to extract the username from
+     * @return the exatracted username
+     */
+    public static String extractUsername(String dn) {
+        String username = dn;
+
+        // ensure the dn is specified
+        if (StringUtils.isNotBlank(dn)) {
+            // determine the separate
+            final String separator = StringUtils.indexOfIgnoreCase(dn, "/cn=") > 0 ? "/" : ",";
+
+            // attempt to locate the cd
+            final String cnPattern = "cn=";
+            final int cnIndex = StringUtils.indexOfIgnoreCase(dn, cnPattern);
+            if (cnIndex >= 0) {
+                int separatorIndex = StringUtils.indexOf(dn, separator, cnIndex);
+                if (separatorIndex > 0) {
+                    username = StringUtils.substring(dn, cnIndex + cnPattern.length(), separatorIndex);
+                } else {
+                    username = StringUtils.substring(dn, cnIndex + cnPattern.length());
+                }
+            }
+        }
+
+        return username;
+    }
+
+    /**
+     * Returns a list of subject alternative names. Any name that is represented as a String by X509Certificate.getSubjectAlternativeNames() is converted to lowercase and returned.
+     *
+     * @param certificate a certificate
+     * @return a list of subject alternative names; list is never null
+     * @throws CertificateParsingException if parsing the certificate failed
+     */
+    public static List<String> getSubjectAlternativeNames(final X509Certificate certificate) throws CertificateParsingException {
+
+        final Collection<List<?>> altNames = certificate.getSubjectAlternativeNames();
+        if (altNames == null) {
+            return new ArrayList<>();
+        }
+
+        final List<String> result = new ArrayList<>();
+        for (final List<?> generalName : altNames) {
+            /**
+             * generalName has the name type as the first element a String or byte array for the second element. We return any general names that are String types.
+             *
+             * We don't inspect the numeric name type because some certificates incorrectly put IPs and DNS names under the wrong name types.
+             */
+            final Object value = generalName.get(1);
+            if (value instanceof String) {
+                result.add(((String) value).toLowerCase());
+            }
+
+        }
+
+        return result;
+    }
+
+    /**
+     * Returns the DN extracted from the peer certificate (the server DN if run on the client; the client DN (if available) if run on the server).
+     *
+     * If the client auth setting is WANT or NONE and a client certificate is not present, this method will return {@code null}.
+     * If the client auth is NEED, it will throw a {@link CertificateException}.
+     *
+     * @param socket the SSL Socket
+     * @return the extracted DN
+     * @throws CertificateException if there is a problem parsing the certificate
+     */
+    public static String extractPeerDNFromSSLSocket(Socket socket) throws CertificateException {
+        String dn = null;
+        if (socket instanceof SSLSocket) {
+            final SSLSocket sslSocket = (SSLSocket) socket;
+
+            boolean clientMode = sslSocket.getUseClientMode();
+            logger.debug("SSL Socket in {} mode", clientMode ? "client" : "server");
+            ClientAuth clientAuth = getClientAuthStatus(sslSocket);
+            logger.debug("SSL Socket client auth status: {}", clientAuth);
+
+            if (clientMode) {
+                logger.debug("This socket is in client mode, so attempting to extract certificate from remote 'server' socket");
+               dn = extractPeerDNFromServerSSLSocket(sslSocket);
+            } else {
+                logger.debug("This socket is in server mode, so attempting to extract certificate from remote 'client' socket");
+               dn = extractPeerDNFromClientSSLSocket(sslSocket);
+            }
+        }
+
+        return dn;
+    }
+
+    /**
+     * Returns the DN extracted from the client certificate.
+     *
+     * If the client auth setting is WANT or NONE and a certificate is not present (and {@code respectClientAuth} is {@code true}), this method will return {@code null}.
+     * If the client auth is NEED, it will throw a {@link CertificateException}.
+     *
+     * @param sslSocket the SSL Socket
+     * @return the extracted DN
+     * @throws CertificateException if there is a problem parsing the certificate
+     */
+    private static String extractPeerDNFromClientSSLSocket(SSLSocket sslSocket) throws CertificateException {
+        String dn = null;
+
+            /** The clientAuth value can be "need", "want", or "none"
+             * A client must send client certificates for need, should for want, and will not for none.
+             * This method should throw an exception if none are provided for need, return null if none are provided for want, and return null (without checking) for none.
+             */
+
+            ClientAuth clientAuth = getClientAuthStatus(sslSocket);
+            logger.debug("SSL Socket client auth status: {}", clientAuth);
+
+            if (clientAuth != ClientAuth.NONE) {
+                try {
+                    final Certificate[] certChains = sslSocket.getSession().getPeerCertificates();
+                    if (certChains != null && certChains.length > 0) {
+                        X509Certificate x509Certificate = convertAbstractX509Certificate(certChains[0]);
+                        dn = x509Certificate.getSubjectDN().getName().trim();
+                        logger.debug("Extracted DN={} from client certificate", dn);
+                    }
+                } catch (SSLPeerUnverifiedException e) {
+                    if (e.getMessage().equals(PEER_NOT_AUTHENTICATED_MSG)) {
+                        logger.error("The incoming request did not contain client certificates and thus the DN cannot" +
+                                " be extracted. Check that the other endpoint is providing a complete client certificate chain");
+                    }
+                    if (clientAuth == ClientAuth.WANT) {
+                        logger.warn("Suppressing missing client certificate exception because client auth is set to 'want'");
+                        return dn;
+                    }
+                    throw new CertificateException(e);
+                }
+            }
+        return dn;
+    }
+
+    /**
+     * Returns the DN extracted from the server certificate.
+     *
+     * @param socket the SSL Socket
+     * @return the extracted DN
+     * @throws CertificateException if there is a problem parsing the certificate
+     */
+    private static String extractPeerDNFromServerSSLSocket(Socket socket) throws CertificateException {
+        String dn = null;
+        if (socket instanceof SSLSocket) {
+            final SSLSocket sslSocket = (SSLSocket) socket;
+                try {
+                    final Certificate[] certChains = sslSocket.getSession().getPeerCertificates();
+                    if (certChains != null && certChains.length > 0) {
+                        X509Certificate x509Certificate = convertAbstractX509Certificate(certChains[0]);
+                        dn = x509Certificate.getSubjectDN().getName().trim();
+                        logger.debug("Extracted DN={} from server certificate", dn);
+                    }
+                } catch (SSLPeerUnverifiedException e) {
+                    if (e.getMessage().equals(PEER_NOT_AUTHENTICATED_MSG)) {
+                        logger.error("The server did not present a certificate and thus the DN cannot" +
+                                " be extracted. Check that the other endpoint is providing a complete certificate chain");
+                    }
+                    throw new CertificateException(e);
+                }
+        }
+        return dn;
+    }
+
+    private static ClientAuth getClientAuthStatus(SSLSocket sslSocket) {
+        return sslSocket.getNeedClientAuth() ? ClientAuth.NEED : sslSocket.getWantClientAuth() ? ClientAuth.WANT : ClientAuth.NONE;
+    }
+
+    /**
+     * Accepts a legacy {@link javax.security.cert.X509Certificate} and returns an {@link X509Certificate}. The {@code javax.*} package certificate classes are for legacy compatibility and should
+     * not be used for new development.
+     *
+     * @param legacyCertificate the {@code javax.security.cert.X509Certificate}
+     * @return a new {@code java.security.cert.X509Certificate}
+     * @throws CertificateException if there is an error generating the new certificate
+     */
+    public static X509Certificate convertLegacyX509Certificate(javax.security.cert.X509Certificate legacyCertificate) throws CertificateException {
+        if (legacyCertificate == null) {
+            throw new IllegalArgumentException("The X.509 certificate cannot be null");
+        }
+
+        try {
+            return formX509Certificate(legacyCertificate.getEncoded());
+        } catch (javax.security.cert.CertificateEncodingException e) {
+            throw new CertificateException(e);
+        }
+    }
+
+    /**
+     * Accepts an abstract {@link Certificate} and returns an {@link X509Certificate}. Because {@code sslSocket.getSession().getPeerCertificates()} returns an array of the
+     * abstract certificates, they must be translated to X.509 to replace the functionality of {@code sslSocket.getSession().getPeerCertificateChain()}.
+     *
+     * @param abstractCertificate the {@code java.security.cert.Certificate}
+     * @return a new {@code java.security.cert.X509Certificate}
+     * @throws CertificateException if there is an error generating the new certificate
+     */
+    public static X509Certificate convertAbstractX509Certificate(Certificate abstractCertificate) throws CertificateException {
+        if (abstractCertificate == null || !(abstractCertificate instanceof X509Certificate)) {
+            throw new IllegalArgumentException("The certificate cannot be null and must be an X.509 certificate");
+        }
+
+        try {
+            return formX509Certificate(abstractCertificate.getEncoded());
+        } catch (java.security.cert.CertificateEncodingException e) {
+            throw new CertificateException(e);
+        }
+    }
+
+    private static X509Certificate formX509Certificate(byte[] encodedCertificate) throws CertificateException {
+        try {
+            CertificateFactory cf = CertificateFactory.getInstance("X.509");
+            ByteArrayInputStream bais = new ByteArrayInputStream(encodedCertificate);
+            return (X509Certificate) cf.generateCertificate(bais);
+        } catch (CertificateException e) {
+            logger.error("Error converting the certificate", e);
+            throw e;
+        }
+    }
+
+    /**
+     * Reorders DN to the order the elements appear in the RFC 2253 table
+     *
+     * https://www.ietf.org/rfc/rfc2253.txt
+     *
+     * String  X.500 AttributeType
+     * ------------------------------
+     * CN      commonName
+     * L       localityName
+     * ST      stateOrProvinceName
+     * O       organizationName
+     * OU      organizationalUnitName
+     * C       countryName
+     * STREET  streetAddress
+     * DC      domainComponent
+     * UID     userid
+     *
+     * @param dn a possibly unordered DN
+     * @return the ordered dn
+     */
+    public static String reorderDn(String dn) {
+        RDN[] rdNs = new X500Name(dn).getRDNs();
+        Arrays.sort(rdNs, new Comparator<RDN>() {
+            @Override
+            public int compare(RDN o1, RDN o2) {
+                AttributeTypeAndValue o1First = o1.getFirst();
+                AttributeTypeAndValue o2First = o2.getFirst();
+
+                ASN1ObjectIdentifier o1Type = o1First.getType();
+                ASN1ObjectIdentifier o2Type = o2First.getType();
+
+                Integer o1Rank = dnOrderMap.get(o1Type);
+                Integer o2Rank = dnOrderMap.get(o2Type);
+                if (o1Rank == null) {
+                    if (o2Rank == null) {
+                        int idComparison = o1Type.getId().compareTo(o2Type.getId());
+                        if (idComparison != 0) {
+                            return idComparison;
+                        }
+                        return String.valueOf(o1Type).compareTo(String.valueOf(o2Type));
+                    }
+                    return 1;
+                } else if (o2Rank == null) {
+                    return -1;
+                }
+                return o1Rank - o2Rank;
+            }
+        });
+        return new X500Name(rdNs).toString();
+    }
+
+    /**
+     * Reverses the X500Name in order make the certificate be in the right order
+     * [see http://stackoverflow.com/questions/7567837/attributes-reversed-in-certificate-subject-and-issuer/12645265]
+     *
+     * @param x500Name the X500Name created with the intended order
+     * @return the X500Name reversed
+     */
+    private static X500Name reverseX500Name(X500Name x500Name) {
+        List<RDN> rdns = Arrays.asList(x500Name.getRDNs());
+        Collections.reverse(rdns);
+        return new X500Name(rdns.toArray(new RDN[rdns.size()]));
+    }
+
+    /**
+     * Generates a unique serial number by using the current time in milliseconds left shifted 32 bits (to make room for incrementor) with an incrementor added
+     *
+     * @return a unique serial number (technically unique to this classloader)
+     */
+    protected static synchronized BigInteger getUniqueSerialNumber() {
+        final long currentTimeMillis = System.currentTimeMillis();
+        final int incrementorValue;
+
+        if (lastSerialNumberMillis != currentTimeMillis) {
+            // We can only get into this block once per millisecond
+            millisecondBigInteger = BigInteger.valueOf(currentTimeMillis).shiftLeft(32);
+            lastSerialNumberMillis = currentTimeMillis;
+            incrementorValue = 0;
+            serialNumberIncrementor = 1;
+        } else {
+            // Already created at least one serial number this millisecond
+            incrementorValue = serialNumberIncrementor++;
+        }
+
+        return millisecondBigInteger.add(BigInteger.valueOf(incrementorValue));
+    }
+
+    /**
+     * Generates a self-signed {@link X509Certificate} suitable for use as a Certificate Authority.
+     *
+     * @param keyPair                 the {@link KeyPair} to generate the {@link X509Certificate} for
+     * @param dn                      the distinguished name to user for the {@link X509Certificate}
+     * @param signingAlgorithm        the signing algorithm to use for the {@link X509Certificate}
+     * @param certificateDurationDays the duration in days for which the {@link X509Certificate} should be valid
+     * @return a self-signed {@link X509Certificate} suitable for use as a Certificate Authority
+     * @throws CertificateException      if there is an generating the new certificate
+     */
+    public static X509Certificate generateSelfSignedX509Certificate(KeyPair keyPair, String dn, String signingAlgorithm, int certificateDurationDays)
+            throws CertificateException {
+        try {
+            ContentSigner sigGen = new JcaContentSignerBuilder(signingAlgorithm).setProvider(BouncyCastleProvider.PROVIDER_NAME).build(keyPair.getPrivate());
+            SubjectPublicKeyInfo subPubKeyInfo = SubjectPublicKeyInfo.getInstance(keyPair.getPublic().getEncoded());
+            Date startDate = new Date();
+            Date endDate = new Date(startDate.getTime() + TimeUnit.DAYS.toMillis(certificateDurationDays));
+
+            X509v3CertificateBuilder certBuilder = new X509v3CertificateBuilder(
+                    reverseX500Name(new X500Name(dn)),
+                    getUniqueSerialNumber(),
+                    startDate, endDate,
+                    reverseX500Name(new X500Name(dn)),
+                    subPubKeyInfo);
+
+            // Set certificate extensions
+            // (1) digitalSignature extension
+            certBuilder.addExtension(Extension.keyUsage, true, new KeyUsage(KeyUsage.digitalSignature | KeyUsage.keyEncipherment | KeyUsage.dataEncipherment
+                    | KeyUsage.keyAgreement | KeyUsage.nonRepudiation | KeyUsage.cRLSign | KeyUsage.keyCertSign));
+
+            certBuilder.addExtension(Extension.basicConstraints, false, new BasicConstraints(true));
+
+            certBuilder.addExtension(Extension.subjectKeyIdentifier, false, new JcaX509ExtensionUtils().createSubjectKeyIdentifier(keyPair.getPublic()));
+
+            certBuilder.addExtension(Extension.authorityKeyIdentifier, false, new JcaX509ExtensionUtils().createAuthorityKeyIdentifier(keyPair.getPublic()));
+
+            // (2) extendedKeyUsage extension
+            certBuilder.addExtension(Extension.extendedKeyUsage, false, new ExtendedKeyUsage(new KeyPurposeId[]{KeyPurposeId.id_kp_clientAuth, KeyPurposeId.id_kp_serverAuth}));
+
+            // Sign the certificate
+            X509CertificateHolder certificateHolder = certBuilder.build(sigGen);
+            return new JcaX509CertificateConverter().setProvider(BouncyCastleProvider.PROVIDER_NAME).getCertificate(certificateHolder);
+        } catch (CertIOException | NoSuchAlgorithmException | OperatorCreationException e) {
+            throw new CertificateException(e);
+        }
+    }
+
+    /**
+     * Generates an issued {@link X509Certificate} from the given issuer certificate and {@link KeyPair}
+     *
+     * @param dn the distinguished name to use
+     * @param publicKey the public key to issue the certificate to
+     * @param issuer the issuer's certificate
+     * @param issuerKeyPair the issuer's keypair
+     * @param signingAlgorithm the signing algorithm to use
+     * @param days the number of days it should be valid for
+     * @return an issued {@link X509Certificate} from the given issuer certificate and {@link KeyPair}
+     * @throws CertificateException if there is an error issuing the certificate
+     */
+    public static X509Certificate generateIssuedCertificate(String dn, PublicKey publicKey, X509Certificate issuer, KeyPair issuerKeyPair, String signingAlgorithm, int days)
+            throws CertificateException {
+        return generateIssuedCertificate(dn, publicKey, null, issuer, issuerKeyPair, signingAlgorithm, days);
+    }
+
+    /**
+     * Generates an issued {@link X509Certificate} from the given issuer certificate and {@link KeyPair}
+     *
+     * @param dn the distinguished name to use
+     * @param publicKey the public key to issue the certificate to
+     * @param extensions extensions extracted from the CSR
+     * @param issuer the issuer's certificate
+     * @param issuerKeyPair the issuer's keypair
+     * @param signingAlgorithm the signing algorithm to use
+     * @param days the number of days it should be valid for
+     * @return an issued {@link X509Certificate} from the given issuer certificate and {@link KeyPair}
+     * @throws CertificateException if there is an error issuing the certificate
+     */
+    public static X509Certificate generateIssuedCertificate(String dn, PublicKey publicKey, Extensions extensions, X509Certificate issuer, KeyPair issuerKeyPair, String signingAlgorithm, int days)
+            throws CertificateException {
+        try {
+            ContentSigner sigGen = new JcaContentSignerBuilder(signingAlgorithm).setProvider(BouncyCastleProvider.PROVIDER_NAME).build(issuerKeyPair.getPrivate());
+            SubjectPublicKeyInfo subPubKeyInfo = SubjectPublicKeyInfo.getInstance(publicKey.getEncoded());
+            Date startDate = new Date();
+            Date endDate = new Date(startDate.getTime() + TimeUnit.DAYS.toMillis(days));
+
+            X509v3CertificateBuilder certBuilder = new X509v3CertificateBuilder(
+                    reverseX500Name(new X500Name(issuer.getSubjectX500Principal().getName())),
+                    getUniqueSerialNumber(),
+                    startDate, endDate,
+                    reverseX500Name(new X500Name(dn)),
+                    subPubKeyInfo);
+
+            certBuilder.addExtension(Extension.subjectKeyIdentifier, false, new JcaX509ExtensionUtils().createSubjectKeyIdentifier(publicKey));
+
+            certBuilder.addExtension(Extension.authorityKeyIdentifier, false, new JcaX509ExtensionUtils().createAuthorityKeyIdentifier(issuerKeyPair.getPublic()));
+            // Set certificate extensions
+            // (1) digitalSignature extension
+            certBuilder.addExtension(Extension.keyUsage, true,
+                    new KeyUsage(KeyUsage.digitalSignature | KeyUsage.keyEncipherment | KeyUsage.dataEncipherment | KeyUsage.keyAgreement | KeyUsage.nonRepudiation));
+
+            certBuilder.addExtension(Extension.basicConstraints, false, new BasicConstraints(false));
+
+            // (2) extendedKeyUsage extension
+            certBuilder.addExtension(Extension.extendedKeyUsage, false, new ExtendedKeyUsage(new KeyPurposeId[]{KeyPurposeId.id_kp_clientAuth, KeyPurposeId.id_kp_serverAuth}));
+
+            // (3) subjectAlternativeName
+            if(extensions != null && extensions.getExtension(Extension.subjectAlternativeName) != null) {
+                certBuilder.addExtension(Extension.subjectAlternativeName, false, extensions.getExtensionParsedValue(Extension.subjectAlternativeName));
+            }
+
+            X509CertificateHolder certificateHolder = certBuilder.build(sigGen);
+            return new JcaX509CertificateConverter().setProvider(BouncyCastleProvider.PROVIDER_NAME).getCertificate(certificateHolder);
+        } catch (CertIOException | NoSuchAlgorithmException | OperatorCreationException e) {
+            throw new CertificateException(e);
+        }
+    }
+
+    /**
+     * Returns true if the two provided DNs are equivalent, regardless of the order of the elements. Returns false if one or both are invalid DNs.
+     *
+     * Example:
+     *
+     * CN=test1, O=testOrg, C=US compared to CN=test1, O=testOrg, C=US -> true
+     * CN=test1, O=testOrg, C=US compared to O=testOrg, CN=test1, C=US -> true
+     * CN=test1, O=testOrg, C=US compared to CN=test2, O=testOrg, C=US -> false
+     * CN=test1, O=testOrg, C=US compared to O=testOrg, CN=test2, C=US -> false
+     * CN=test1, O=testOrg, C=US compared to                           -> false
+     *                           compared to                           -> true
+     *
+     * @param dn1 the first DN to compare
+     * @param dn2 the second DN to compare
+     * @return true if the DNs are equivalent, false otherwise
+     */
+    public static boolean compareDNs(String dn1, String dn2) {
+        if (dn1 == null) {
+            dn1 = "";
+        }
+
+        if (dn2 == null) {
+            dn2 = "";
+        }
+
+        if (StringUtils.isEmpty(dn1) || StringUtils.isEmpty(dn2)) {
+            return dn1.equals(dn2);
+        }
+        try {
+            List<Rdn> rdn1 = new LdapName(dn1).getRdns();
+            List<Rdn> rdn2 = new LdapName(dn2).getRdns();
+
+            return rdn1.size() == rdn2.size() && rdn1.containsAll(rdn2);
+        } catch (InvalidNameException e) {
+            logger.warn("Cannot compare DNs: {} and {} because one or both is not a valid DN", dn1, dn2);
+            return false;
+        }
+    }
+
+    /**
+     * Extract extensions from CSR object
+     */
+    public static Extensions getExtensionsFromCSR(JcaPKCS10CertificationRequest csr) {
+        Attribute[] attributess = csr.getAttributes(PKCSObjectIdentifiers.pkcs_9_at_extensionRequest);
+        for (Attribute attribute : attributess) {
+            ASN1Set attValue = attribute.getAttrValues();
+            if (attValue != null) {
+                ASN1Encodable extension = attValue.getObjectAt(0);
+                if (extension instanceof Extensions) {
+                    return (Extensions) extension;
+                } else if (extension instanceof DERSequence) {
+                    return Extensions.getInstance(extension);
+                }
+            }
+        }
+        return null;
+    }
+
+    private CertificateUtils() {
+    }
+}

http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/6f26290d/nifi-registry-core/nifi-registry-security-utils/src/main/java/org/apache/nifi/registry/security/util/CryptoUtils.java
----------------------------------------------------------------------
diff --git a/nifi-registry-core/nifi-registry-security-utils/src/main/java/org/apache/nifi/registry/security/util/CryptoUtils.java b/nifi-registry-core/nifi-registry-security-utils/src/main/java/org/apache/nifi/registry/security/util/CryptoUtils.java
new file mode 100644
index 0000000..cd2d3e3
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-security-utils/src/main/java/org/apache/nifi/registry/security/util/CryptoUtils.java
@@ -0,0 +1,75 @@
+/*
+ * 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.security.util;
+
+import javax.crypto.Cipher;
+import java.security.NoSuchAlgorithmException;
+
+public class CryptoUtils {
+
+    /**
+     *  Required Cipher transformations according to Java SE 8 {@link Cipher} docs
+     */
+    private static final String[] standardCryptoTransformations = {
+        "AES/CBC/NoPadding",
+        "AES/CBC/PKCS5Padding",
+        "AES/ECB/NoPadding",
+        "AES/ECB/PKCS5Padding",
+        "DES/CBC/NoPadding",
+        "DES/CBC/PKCS5Padding",
+        "DES/ECB/NoPadding",
+        "DES/ECB/PKCS5Padding",
+        "DESede/CBC/NoPadding",
+        "DESede/CBC/PKCS5Padding",
+        "DESede/ECB/NoPadding",
+        "DESede/ECB/PKCS5Padding",
+        "RSA/ECB/PKCS1Padding",
+        "RSA/ECB/OAEPWithSHA-1AndMGF1Padding",
+        "RSA/ECB/OAEPWithSHA-256AndMGF1Padding"
+    };
+
+    /**
+     * Check if cryptographic strength available in this Java Runtime is restricted.
+     *
+     * Not every Java Platform supports "unlimited strength encryption",
+     * so this convenience method provides a way to check if strength of crypto
+     * functions (i.e., max key length) is unlimited or restricted in the
+     * current Java runtime environment.
+     *
+     * @return true if it can be determined that max key lengths are less than unlimited
+     *         false if key lengths are restricted
+     *         null if max key length cannot be determined for any known Cipher transformations */
+    public static Boolean isCryptoRestricted() {
+
+        Boolean isCryptoRestricted = null;
+
+        for (String transformation : standardCryptoTransformations) {
+            try {
+                return Cipher.getMaxAllowedKeyLength(transformation) < Integer.MAX_VALUE;
+            } catch (final NoSuchAlgorithmException e) {
+                // Unexpected as we are pulling from a list of transforms that every
+                // java platform is required to support, but try the next one
+            }
+        }
+
+        // Tried every standard Cipher transformation and none were available,
+        // so crypto strength restrictions cannot be determined.
+        return null;
+
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/6f26290d/nifi-registry-core/nifi-registry-security-utils/src/main/java/org/apache/nifi/registry/security/util/KeyStoreUtils.java
----------------------------------------------------------------------
diff --git a/nifi-registry-core/nifi-registry-security-utils/src/main/java/org/apache/nifi/registry/security/util/KeyStoreUtils.java b/nifi-registry-core/nifi-registry-security-utils/src/main/java/org/apache/nifi/registry/security/util/KeyStoreUtils.java
new file mode 100644
index 0000000..71f1ce0
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-security-utils/src/main/java/org/apache/nifi/registry/security/util/KeyStoreUtils.java
@@ -0,0 +1,82 @@
+/*
+ * 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.security.util;
+
+import org.apache.commons.lang3.StringUtils;
+import org.bouncycastle.jce.provider.BouncyCastleProvider;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.security.KeyStore;
+import java.security.KeyStoreException;
+import java.security.Security;
+
+public class KeyStoreUtils {
+    private static final Logger logger = LoggerFactory.getLogger(KeyStoreUtils.class);
+
+    static {
+        Security.addProvider(new BouncyCastleProvider());
+    }
+
+    /**
+     * Returns the provider that will be used for the given keyStoreType
+     *
+     * @param keyStoreType the keyStoreType
+     * @return the provider that will be used
+     */
+    public static String getKeyStoreProvider(String keyStoreType) {
+        if (KeystoreType.PKCS12.toString().equalsIgnoreCase(keyStoreType)) {
+            return BouncyCastleProvider.PROVIDER_NAME;
+        }
+        return null;
+    }
+
+    /**
+     * Returns an empty KeyStore backed by the appropriate provider
+     *
+     * @param keyStoreType the keyStoreType
+     * @return an empty KeyStore
+     * @throws KeyStoreException if a KeyStore of the given type cannot be instantiated
+     */
+    public static KeyStore getKeyStore(String keyStoreType) throws KeyStoreException {
+        String keyStoreProvider = getKeyStoreProvider(keyStoreType);
+        if (StringUtils.isNotEmpty(keyStoreProvider)) {
+            try {
+                return KeyStore.getInstance(keyStoreType, keyStoreProvider);
+            } catch (Exception e) {
+                logger.error("Unable to load " + keyStoreProvider + " " + keyStoreType
+                        + " keystore.  This may cause issues getting trusted CA certificates as well as Certificate Chains for use in TLS.", e);
+            }
+        }
+        return KeyStore.getInstance(keyStoreType);
+    }
+
+    /**
+     * Returns an empty KeyStore intended for use as a TrustStore backed by the appropriate provider
+     *
+     * @param trustStoreType the trustStoreType
+     * @return an empty KeyStore
+     * @throws KeyStoreException if a KeyStore of the given type cannot be instantiated
+     */
+    public static KeyStore getTrustStore(String trustStoreType) throws KeyStoreException {
+        if (KeystoreType.PKCS12.toString().equalsIgnoreCase(trustStoreType)) {
+            logger.warn(trustStoreType + " truststores are deprecated.  " + KeystoreType.JKS.toString() + " is preferred.");
+        }
+        return getKeyStore(trustStoreType);
+    }
+}

http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/6f26290d/nifi-registry-core/nifi-registry-security-utils/src/main/java/org/apache/nifi/registry/security/util/KeystoreType.java
----------------------------------------------------------------------
diff --git a/nifi-registry-core/nifi-registry-security-utils/src/main/java/org/apache/nifi/registry/security/util/KeystoreType.java b/nifi-registry-core/nifi-registry-security-utils/src/main/java/org/apache/nifi/registry/security/util/KeystoreType.java
new file mode 100644
index 0000000..f143e5a
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-security-utils/src/main/java/org/apache/nifi/registry/security/util/KeystoreType.java
@@ -0,0 +1,25 @@
+/*
+ * 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.security.util;
+
+/**
+ * Keystore types.
+ */
+public enum KeystoreType {
+    PKCS12,
+    JKS;
+}

http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/6f26290d/nifi-registry-core/nifi-registry-security-utils/src/main/java/org/apache/nifi/registry/security/util/ProxiedEntitiesUtils.java
----------------------------------------------------------------------
diff --git a/nifi-registry-core/nifi-registry-security-utils/src/main/java/org/apache/nifi/registry/security/util/ProxiedEntitiesUtils.java b/nifi-registry-core/nifi-registry-security-utils/src/main/java/org/apache/nifi/registry/security/util/ProxiedEntitiesUtils.java
new file mode 100644
index 0000000..f850341
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-security-utils/src/main/java/org/apache/nifi/registry/security/util/ProxiedEntitiesUtils.java
@@ -0,0 +1,127 @@
+/*
+ * 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.security.util;
+
+import org.apache.commons.lang3.StringUtils;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+import java.util.stream.Collectors;
+
+public class ProxiedEntitiesUtils {
+    private static final Logger logger = LoggerFactory.getLogger(ProxiedEntitiesUtils.class);
+
+    public static final String PROXY_ENTITIES_CHAIN = "X-ProxiedEntitiesChain";
+    public static final String PROXY_ENTITIES_ACCEPTED = "X-ProxiedEntitiesAccepted";
+    public static final String PROXY_ENTITIES_DETAILS = "X-ProxiedEntitiesDetails";
+
+    private static final String GT = ">";
+    private static final String ESCAPED_GT = "\\\\>";
+    private static final String LT = "<";
+    private static final String ESCAPED_LT = "\\\\<";
+
+    private static final String ANONYMOUS_CHAIN = "<>";
+
+    /**
+     * Formats the specified DN to be set as a HTTP header using well known conventions.
+     *
+     * @param dn raw dn
+     * @return the dn formatted as an HTTP header
+     */
+    public static String formatProxyDn(String dn) {
+        return LT + sanitizeDn(dn) + GT;
+    }
+
+    /**
+     * If a user provides a DN with the sequence '><', they could escape the tokenization process and impersonate another user.
+     * <p>
+     * Example:
+     * <p>
+     * Provided DN: {@code jdoe><alopresto} -> {@code <jdoe><alopresto><proxy...>} would allow the user to impersonate jdoe
+     *
+     * @param rawDn the unsanitized DN
+     * @return the sanitized DN
+     */
+    private static String sanitizeDn(String rawDn) {
+        if (StringUtils.isEmpty(rawDn)) {
+            return rawDn;
+        } else {
+            String sanitizedDn = rawDn.replaceAll(GT, ESCAPED_GT).replaceAll(LT, ESCAPED_LT);
+            if (!sanitizedDn.equals(rawDn)) {
+                logger.warn("The provided DN [" + rawDn + "] contained dangerous characters that were escaped to [" + sanitizedDn + "]");
+            }
+            return sanitizedDn;
+        }
+    }
+
+    /**
+     * Reconstitutes the original DN from the sanitized version passed in the proxy chain.
+     * <p>
+     * Example:
+     * <p>
+     * {@code alopresto\>\<proxy1} -> {@code alopresto><proxy1}
+     *
+     * @param sanitizedDn the sanitized DN
+     * @return the original DN
+     */
+    private static String unsanitizeDn(String sanitizedDn) {
+        if (StringUtils.isEmpty(sanitizedDn)) {
+            return sanitizedDn;
+        } else {
+            String unsanitizedDn = sanitizedDn.replaceAll(ESCAPED_GT, GT).replaceAll(ESCAPED_LT, LT);
+            if (!unsanitizedDn.equals(sanitizedDn)) {
+                logger.warn("The provided DN [" + sanitizedDn + "] had been escaped, and was reconstituted to the dangerous DN [" + unsanitizedDn + "]");
+            }
+            return unsanitizedDn;
+        }
+    }
+
+    /**
+     * Tokenizes the specified proxy chain.
+     *
+     * @param rawProxyChain raw chain
+     * @return tokenized proxy chain
+     */
+    public static List<String> tokenizeProxiedEntitiesChain(String rawProxyChain) {
+        final List<String> proxyChain = new ArrayList<>();
+        if (!StringUtils.isEmpty(rawProxyChain)) {
+            // Split the String on the >< token
+            List<String> elements = Arrays.asList(StringUtils.splitByWholeSeparatorPreserveAllTokens(rawProxyChain, "><"));
+
+            // Unsanitize each DN and collect back
+            elements = elements.stream().map(ProxiedEntitiesUtils::unsanitizeDn).collect(Collectors.toList());
+
+            // Remove the leading < from the first element
+            elements.set(0, elements.get(0).replaceFirst(LT, ""));
+
+            // Remove the trailing > from the last element
+            int last = elements.size() - 1;
+            String lastElement = elements.get(last);
+            if (lastElement.endsWith(GT)) {
+                elements.set(last, lastElement.substring(0, lastElement.length() - 1));
+            }
+
+            proxyChain.addAll(elements);
+        }
+
+        return proxyChain;
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/6f26290d/nifi-registry-core/nifi-registry-security-utils/src/main/java/org/apache/nifi/registry/security/util/SslContextFactory.java
----------------------------------------------------------------------
diff --git a/nifi-registry-core/nifi-registry-security-utils/src/main/java/org/apache/nifi/registry/security/util/SslContextFactory.java b/nifi-registry-core/nifi-registry-security-utils/src/main/java/org/apache/nifi/registry/security/util/SslContextFactory.java
new file mode 100644
index 0000000..9ed8ace
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-security-utils/src/main/java/org/apache/nifi/registry/security/util/SslContextFactory.java
@@ -0,0 +1,249 @@
+/*
+ * 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.security.util;
+
+import javax.net.ssl.KeyManager;
+import javax.net.ssl.KeyManagerFactory;
+import javax.net.ssl.SSLContext;
+import javax.net.ssl.TrustManager;
+import javax.net.ssl.TrustManagerFactory;
+import java.io.FileInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.security.KeyManagementException;
+import java.security.KeyStore;
+import java.security.KeyStoreException;
+import java.security.NoSuchAlgorithmException;
+import java.security.SecureRandom;
+import java.security.UnrecoverableKeyException;
+import java.security.cert.CertificateException;
+
+/**
+ * A factory for creating SSL contexts using the application's security
+ * properties.
+ *
+ */
+public final class SslContextFactory {
+
+    public static enum ClientAuth {
+
+        WANT,
+        REQUIRED,
+        NONE
+    }
+
+    /**
+     * Creates a SSLContext instance using the given information. The password for the key is assumed to be the same
+     * as the password for the keystore. If this is not the case, the {@link #createSslContext(String, char[], chart[], String, String, char[], String, ClientAuth, String)}
+     * method should be used instead
+     *
+     * @param keystore the full path to the keystore
+     * @param keystorePasswd the keystore password
+     * @param keystoreType the type of keystore (e.g., PKCS12, JKS)
+     * @param truststore the full path to the truststore
+     * @param truststorePasswd the truststore password
+     * @param truststoreType the type of truststore (e.g., PKCS12, JKS)
+     * @param clientAuth the type of client authentication
+     * @param protocol         the protocol to use for the SSL connection
+     *
+     * @return a SSLContext instance
+     * @throws KeyStoreException if any issues accessing the keystore
+     * @throws IOException for any problems loading the keystores
+     * @throws NoSuchAlgorithmException if an algorithm is found to be used but is unknown
+     * @throws CertificateException if there is an issue with the certificate
+     * @throws UnrecoverableKeyException if the key is insufficient
+     * @throws KeyManagementException if unable to manage the key
+     */
+    public static SSLContext createSslContext(
+            final String keystore, final char[] keystorePasswd, final String keystoreType,
+            final String truststore, final char[] truststorePasswd, final String truststoreType,
+            final ClientAuth clientAuth, final String protocol)
+            throws KeyStoreException, IOException, NoSuchAlgorithmException, CertificateException,
+            UnrecoverableKeyException, KeyManagementException {
+
+        // Pass the keystore password as both the keystore password and the key password.
+        return createSslContext(keystore, keystorePasswd, keystorePasswd, keystoreType, truststore, truststorePasswd, truststoreType, clientAuth, protocol);
+    }
+
+    /**
+     * Creates a SSLContext instance using the given information.
+     *
+     * @param keystore the full path to the keystore
+     * @param keystorePasswd the keystore password
+     * @param keystoreType the type of keystore (e.g., PKCS12, JKS)
+     * @param truststore the full path to the truststore
+     * @param truststorePasswd the truststore password
+     * @param truststoreType the type of truststore (e.g., PKCS12, JKS)
+     * @param clientAuth the type of client authentication
+     * @param protocol         the protocol to use for the SSL connection
+     *
+     * @return a SSLContext instance
+     * @throws KeyStoreException if any issues accessing the keystore
+     * @throws IOException for any problems loading the keystores
+     * @throws NoSuchAlgorithmException if an algorithm is found to be used but is unknown
+     * @throws CertificateException if there is an issue with the certificate
+     * @throws UnrecoverableKeyException if the key is insufficient
+     * @throws KeyManagementException if unable to manage the key
+     */
+    public static SSLContext createSslContext(
+            final String keystore, final char[] keystorePasswd, final char[] keyPasswd, final String keystoreType,
+            final String truststore, final char[] truststorePasswd, final String truststoreType,
+            final ClientAuth clientAuth, final String protocol)
+            throws KeyStoreException, IOException, NoSuchAlgorithmException, CertificateException,
+            UnrecoverableKeyException, KeyManagementException {
+
+        // prepare the keystore
+        final KeyStore keyStore = KeyStoreUtils.getKeyStore(keystoreType);
+        try (final InputStream keyStoreStream = new FileInputStream(keystore)) {
+            keyStore.load(keyStoreStream, keystorePasswd);
+        }
+        final KeyManagerFactory keyManagerFactory = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm());
+        if (keyPasswd == null) {
+            keyManagerFactory.init(keyStore, keystorePasswd);
+        } else {
+            keyManagerFactory.init(keyStore, keyPasswd);
+        }
+
+        // prepare the truststore
+        final KeyStore trustStore = KeyStoreUtils.getTrustStore(truststoreType);
+        try (final InputStream trustStoreStream = new FileInputStream(truststore)) {
+            trustStore.load(trustStoreStream, truststorePasswd);
+        }
+        final TrustManagerFactory trustManagerFactory = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());
+        trustManagerFactory.init(trustStore);
+
+        // initialize the ssl context
+        final SSLContext sslContext = SSLContext.getInstance(protocol);
+        sslContext.init(keyManagerFactory.getKeyManagers(), trustManagerFactory.getTrustManagers(), new SecureRandom());
+        if (ClientAuth.REQUIRED == clientAuth) {
+            sslContext.getDefaultSSLParameters().setNeedClientAuth(true);
+        } else if (ClientAuth.WANT == clientAuth) {
+            sslContext.getDefaultSSLParameters().setWantClientAuth(true);
+        } else {
+            sslContext.getDefaultSSLParameters().setWantClientAuth(false);
+        }
+
+        return sslContext;
+
+    }
+
+    /**
+     * Creates a SSLContext instance using the given information. This method assumes that the key password is
+     * the same as the keystore password. If this is not the case, use the {@link #createSslContext(String, char[], char[], String, String)}
+     * method instead.
+     *
+     * @param keystore the full path to the keystore
+     * @param keystorePasswd the keystore password
+     * @param keystoreType the type of keystore (e.g., PKCS12, JKS)
+     * @param protocol the protocol to use for the SSL connection
+     *
+     * @return a SSLContext instance
+     * @throws KeyStoreException if any issues accessing the keystore
+     * @throws IOException for any problems loading the keystores
+     * @throws NoSuchAlgorithmException if an algorithm is found to be used but is unknown
+     * @throws CertificateException if there is an issue with the certificate
+     * @throws UnrecoverableKeyException if the key is insufficient
+     * @throws KeyManagementException if unable to manage the key
+     */
+    public static SSLContext createSslContext(
+        final String keystore, final char[] keystorePasswd, final String keystoreType, final String protocol)
+        throws KeyStoreException, IOException, NoSuchAlgorithmException, CertificateException,
+        UnrecoverableKeyException, KeyManagementException {
+
+        // create SSL Context passing keystore password as the key password
+        return createSslContext(keystore, keystorePasswd, keystorePasswd, keystoreType, protocol);
+    }
+
+    /**
+     * Creates a SSLContext instance using the given information.
+     *
+     * @param keystore the full path to the keystore
+     * @param keystorePasswd the keystore password
+     * @param keystoreType the type of keystore (e.g., PKCS12, JKS)
+     * @param protocol the protocol to use for the SSL connection
+     *
+     * @return a SSLContext instance
+     * @throws KeyStoreException if any issues accessing the keystore
+     * @throws IOException for any problems loading the keystores
+     * @throws NoSuchAlgorithmException if an algorithm is found to be used but is unknown
+     * @throws CertificateException if there is an issue with the certificate
+     * @throws UnrecoverableKeyException if the key is insufficient
+     * @throws KeyManagementException if unable to manage the key
+     */
+    public static SSLContext createSslContext(
+        final String keystore, final char[] keystorePasswd, final char[] keyPasswd, final String keystoreType, final String protocol)
+            throws KeyStoreException, IOException, NoSuchAlgorithmException, CertificateException,
+            UnrecoverableKeyException, KeyManagementException {
+
+        // prepare the keystore
+        final KeyStore keyStore = KeyStoreUtils.getKeyStore(keystoreType);
+        try (final InputStream keyStoreStream = new FileInputStream(keystore)) {
+            keyStore.load(keyStoreStream, keystorePasswd);
+        }
+        final KeyManagerFactory keyManagerFactory = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm());
+        if (keyPasswd == null) {
+            keyManagerFactory.init(keyStore, keystorePasswd);
+        } else {
+            keyManagerFactory.init(keyStore, keyPasswd);
+        }
+
+        // initialize the ssl context
+        final SSLContext ctx = SSLContext.getInstance(protocol);
+        ctx.init(keyManagerFactory.getKeyManagers(), new TrustManager[0], new SecureRandom());
+
+        return ctx;
+
+    }
+
+    /**
+     * Creates a SSLContext instance using the given information.
+     *
+     * @param truststore the full path to the truststore
+     * @param truststorePasswd the truststore password
+     * @param truststoreType the type of truststore (e.g., PKCS12, JKS)
+     * @param protocol the protocol to use for the SSL connection
+     *
+     * @return a SSLContext instance
+     * @throws KeyStoreException if any issues accessing the keystore
+     * @throws IOException for any problems loading the keystores
+     * @throws NoSuchAlgorithmException if an algorithm is found to be used but is unknown
+     * @throws CertificateException if there is an issue with the certificate
+     * @throws UnrecoverableKeyException if the key is insufficient
+     * @throws KeyManagementException if unable to manage the key
+     */
+    public static SSLContext createTrustSslContext(
+            final String truststore, final char[] truststorePasswd, final String truststoreType, final String protocol)
+            throws KeyStoreException, IOException, NoSuchAlgorithmException, CertificateException,
+            UnrecoverableKeyException, KeyManagementException {
+
+        // prepare the truststore
+        final KeyStore trustStore = KeyStoreUtils.getTrustStore(truststoreType);
+        try (final InputStream trustStoreStream = new FileInputStream(truststore)) {
+            trustStore.load(trustStoreStream, truststorePasswd);
+        }
+        final TrustManagerFactory trustManagerFactory = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());
+        trustManagerFactory.init(trustStore);
+
+        // initialize the ssl context
+        final SSLContext ctx = SSLContext.getInstance(protocol);
+        ctx.init(new KeyManager[0], trustManagerFactory.getTrustManagers(), new SecureRandom());
+
+        return ctx;
+
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/6f26290d/nifi-registry-core/nifi-registry-utils/pom.xml
----------------------------------------------------------------------
diff --git a/nifi-registry-core/nifi-registry-utils/pom.xml b/nifi-registry-core/nifi-registry-utils/pom.xml
new file mode 100644
index 0000000..99f196d
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-utils/pom.xml
@@ -0,0 +1,26 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 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. -->
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+    <modelVersion>4.0.0</modelVersion>
+    <parent>
+        <groupId>org.apache.nifi.registry</groupId>
+        <artifactId>nifi-registry-core</artifactId>
+        <version>0.3.0-SNAPSHOT</version>
+    </parent>
+    
+    <artifactId>nifi-registry-utils</artifactId>
+    <packaging>jar</packaging>
+    
+    <dependencies>
+    </dependencies>
+
+</project>

http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/6f26290d/nifi-registry-core/nifi-registry-utils/src/main/java/org/apache/nifi/registry/util/DataUnit.java
----------------------------------------------------------------------
diff --git a/nifi-registry-core/nifi-registry-utils/src/main/java/org/apache/nifi/registry/util/DataUnit.java b/nifi-registry-core/nifi-registry-utils/src/main/java/org/apache/nifi/registry/util/DataUnit.java
new file mode 100644
index 0000000..21aa9a7
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-utils/src/main/java/org/apache/nifi/registry/util/DataUnit.java
@@ -0,0 +1,245 @@
+/*
+ * 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.util;
+
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+public enum DataUnit {
+
+    /**
+     * Bytes
+     */
+    B {
+                @Override
+                public double toB(double value) {
+                    return value;
+                }
+
+                @Override
+                public double toKB(double value) {
+                    return value / POWERS[1];
+                }
+
+                @Override
+                public double toMB(double value) {
+                    return value / POWERS[2];
+                }
+
+                @Override
+                public double toGB(double value) {
+                    return value / POWERS[3];
+                }
+
+                @Override
+                public double toTB(double value) {
+                    return value / POWERS[4];
+                }
+
+                @Override
+                public double convert(double sourceSize, DataUnit sourceUnit) {
+                    return sourceUnit.toB(sourceSize);
+                }
+            },
+    /**
+     * Kilobytes
+     */
+    KB {
+                @Override
+                public double toB(double value) {
+                    return value * POWERS[1];
+                }
+
+                @Override
+                public double toKB(double value) {
+                    return value;
+                }
+
+                @Override
+                public double toMB(double value) {
+                    return value / POWERS[1];
+                }
+
+                @Override
+                public double toGB(double value) {
+                    return value / POWERS[2];
+                }
+
+                @Override
+                public double toTB(double value) {
+                    return value / POWERS[3];
+                }
+
+                @Override
+                public double convert(double sourceSize, DataUnit sourceUnit) {
+                    return sourceUnit.toKB(sourceSize);
+                }
+            },
+    /**
+     * Megabytes
+     */
+    MB {
+                @Override
+                public double toB(double value) {
+                    return value * POWERS[2];
+                }
+
+                @Override
+                public double toKB(double value) {
+                    return value * POWERS[1];
+                }
+
+                @Override
+                public double toMB(double value) {
+                    return value;
+                }
+
+                @Override
+                public double toGB(double value) {
+                    return value / POWERS[1];
+                }
+
+                @Override
+                public double toTB(double value) {
+                    return value / POWERS[2];
+                }
+
+                @Override
+                public double convert(double sourceSize, DataUnit sourceUnit) {
+                    return sourceUnit.toMB(sourceSize);
+                }
+            },
+    /**
+     * Gigabytes
+     */
+    GB {
+                @Override
+                public double toB(double value) {
+                    return value * POWERS[3];
+                }
+
+                @Override
+                public double toKB(double value) {
+                    return value * POWERS[2];
+                }
+
+                @Override
+                public double toMB(double value) {
+                    return value * POWERS[1];
+                }
+
+                @Override
+                public double toGB(double value) {
+                    return value;
+                }
+
+                @Override
+                public double toTB(double value) {
+                    return value / POWERS[1];
+                }
+
+                @Override
+                public double convert(double sourceSize, DataUnit sourceUnit) {
+                    return sourceUnit.toGB(sourceSize);
+                }
+            },
+    /**
+     * Terabytes
+     */
+    TB {
+                @Override
+                public double toB(double value) {
+                    return value * POWERS[4];
+                }
+
+                @Override
+                public double toKB(double value) {
+                    return value * POWERS[3];
+                }
+
+                @Override
+                public double toMB(double value) {
+                    return value * POWERS[2];
+                }
+
+                @Override
+                public double toGB(double value) {
+                    return value * POWERS[1];
+                }
+
+                @Override
+                public double toTB(double value) {
+                    return value;
+                }
+
+                @Override
+                public double convert(double sourceSize, DataUnit sourceUnit) {
+                    return sourceUnit.toTB(sourceSize);
+                }
+            };
+
+    public double convert(final double sourceSize, final DataUnit sourceUnit) {
+        throw new AbstractMethodError();
+    }
+
+    public double toB(double size) {
+        throw new AbstractMethodError();
+    }
+
+    public double toKB(double size) {
+        throw new AbstractMethodError();
+    }
+
+    public double toMB(double size) {
+        throw new AbstractMethodError();
+    }
+
+    public double toGB(double size) {
+        throw new AbstractMethodError();
+    }
+
+    public double toTB(double size) {
+        throw new AbstractMethodError();
+    }
+
+    public static final double[] POWERS = {1,
+        1024D,
+        1024 * 1024D,
+        1024 * 1024 * 1024D,
+        1024 * 1024 * 1024 * 1024D};
+
+    public static final String DATA_SIZE_REGEX = "(\\d+(?:\\.\\d+)?)\\s*(B|KB|MB|GB|TB)";
+    public static final Pattern DATA_SIZE_PATTERN = Pattern.compile(DATA_SIZE_REGEX);
+
+    public static Double parseDataSize(final String value, final DataUnit units) {
+        if (value == null) {
+            return null;
+        }
+
+        final Matcher matcher = DATA_SIZE_PATTERN.matcher(value.toUpperCase());
+        if (!matcher.find()) {
+            throw new IllegalArgumentException("Invalid data size: " + value);
+        }
+
+        final String sizeValue = matcher.group(1);
+        final String unitValue = matcher.group(2);
+
+        final DataUnit sourceUnit = DataUnit.valueOf(unitValue);
+        final double size = Double.parseDouble(sizeValue);
+        return units.convert(size, sourceUnit);
+    }
+}

http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/6f26290d/nifi-registry-core/nifi-registry-utils/src/main/java/org/apache/nifi/registry/util/EscapeUtils.java
----------------------------------------------------------------------
diff --git a/nifi-registry-core/nifi-registry-utils/src/main/java/org/apache/nifi/registry/util/EscapeUtils.java b/nifi-registry-core/nifi-registry-utils/src/main/java/org/apache/nifi/registry/util/EscapeUtils.java
new file mode 100644
index 0000000..b42538a
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-utils/src/main/java/org/apache/nifi/registry/util/EscapeUtils.java
@@ -0,0 +1,41 @@
+/*
+ * 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.util;
+
+public class EscapeUtils {
+
+    /**
+     * Escapes the specified html by replacing &amp;, &lt;, &gt;, &quot;, &#39;, &#x2f; with their corresponding html entity. If html is null, null is returned.
+     *
+     * @param html to escape
+     * @return escaped html
+     */
+    public static String escapeHtml(String html) {
+        if (html == null) {
+            return null;
+        }
+
+        html = html.replace("&", "&amp;");
+        html = html.replace("<", "&lt;");
+        html = html.replace(">", "&gt;");
+        html = html.replace("\"", "&quot;");
+        html = html.replace("'", "&#39;");
+        html = html.replace("/", "&#x2f;");
+
+        return html;
+    }
+}


[34/51] [partial] nifi-registry git commit: NIFIREG-201 Refactoring project structure to better isolate extensions

Posted by kd...@apache.org.
http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/6f26290d/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/security/authorization/file/FileUserGroupProvider.java
----------------------------------------------------------------------
diff --git a/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/security/authorization/file/FileUserGroupProvider.java b/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/security/authorization/file/FileUserGroupProvider.java
new file mode 100644
index 0000000..4736fe2
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/security/authorization/file/FileUserGroupProvider.java
@@ -0,0 +1,746 @@
+/*
+ * 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.security.authorization.file;
+
+import org.apache.commons.lang3.StringUtils;
+import org.apache.nifi.registry.properties.NiFiRegistryProperties;
+import org.apache.nifi.registry.properties.util.IdentityMapping;
+import org.apache.nifi.registry.properties.util.IdentityMappingUtil;
+import org.apache.nifi.registry.security.authorization.AuthorizerConfigurationContext;
+import org.apache.nifi.registry.security.authorization.ConfigurableUserGroupProvider;
+import org.apache.nifi.registry.security.authorization.Group;
+import org.apache.nifi.registry.security.authorization.User;
+import org.apache.nifi.registry.security.authorization.UserAndGroups;
+import org.apache.nifi.registry.security.authorization.UserGroupProviderInitializationContext;
+import org.apache.nifi.registry.security.authorization.annotation.AuthorizerContext;
+import org.apache.nifi.registry.security.authorization.exception.AuthorizationAccessException;
+import org.apache.nifi.registry.security.authorization.exception.UninheritableAuthorizationsException;
+import org.apache.nifi.registry.security.authorization.file.tenants.generated.Groups;
+import org.apache.nifi.registry.security.authorization.file.tenants.generated.Tenants;
+import org.apache.nifi.registry.security.authorization.file.tenants.generated.Users;
+import org.apache.nifi.registry.security.exception.SecurityProviderCreationException;
+import org.apache.nifi.registry.security.exception.SecurityProviderDestructionException;
+import org.apache.nifi.registry.util.FileUtils;
+import org.apache.nifi.registry.util.PropertyValue;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.w3c.dom.Document;
+import org.w3c.dom.Element;
+import org.w3c.dom.Node;
+import org.w3c.dom.NodeList;
+import org.xml.sax.SAXException;
+
+import javax.xml.XMLConstants;
+import javax.xml.bind.JAXBContext;
+import javax.xml.bind.JAXBElement;
+import javax.xml.bind.JAXBException;
+import javax.xml.bind.Marshaller;
+import javax.xml.bind.Unmarshaller;
+import javax.xml.parsers.DocumentBuilder;
+import javax.xml.parsers.DocumentBuilderFactory;
+import javax.xml.parsers.ParserConfigurationException;
+import javax.xml.stream.XMLOutputFactory;
+import javax.xml.stream.XMLStreamException;
+import javax.xml.stream.XMLStreamWriter;
+import javax.xml.transform.stream.StreamSource;
+import javax.xml.validation.Schema;
+import javax.xml.validation.SchemaFactory;
+import java.io.ByteArrayInputStream;
+import java.io.File;
+import java.io.IOException;
+import java.io.StringWriter;
+import java.nio.charset.StandardCharsets;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.Date;
+import java.util.HashSet;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.concurrent.atomic.AtomicReference;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+public class FileUserGroupProvider implements ConfigurableUserGroupProvider {
+
+    private static final Logger logger = LoggerFactory.getLogger(FileUserGroupProvider.class);
+
+    private static final String TENANTS_XSD = "/tenants.xsd";
+    private static final String JAXB_TENANTS_PATH = "org.apache.nifi.registry.security.authorization.file.tenants.generated";
+
+    private static final JAXBContext JAXB_TENANTS_CONTEXT = initializeJaxbContext(JAXB_TENANTS_PATH);
+
+    /**
+     * Load the JAXBContext.
+     */
+    private static JAXBContext initializeJaxbContext(final String contextPath) {
+        try {
+            return JAXBContext.newInstance(contextPath, FileAuthorizer.class.getClassLoader());
+            //return JAXBContext.newInstance(contextPath);
+        } catch (JAXBException e) {
+            throw new RuntimeException("Unable to create JAXBContext: " + e);
+        }
+    }
+
+    private static final DocumentBuilderFactory DOCUMENT_BUILDER_FACTORY = DocumentBuilderFactory.newInstance();
+    private static final XMLOutputFactory XML_OUTPUT_FACTORY = XMLOutputFactory.newInstance();
+
+    private static final String USER_ELEMENT = "user";
+    private static final String GROUP_USER_ELEMENT = "groupUser";
+    private static final String GROUP_ELEMENT = "group";
+    private static final String IDENTIFIER_ATTR = "identifier";
+    private static final String IDENTITY_ATTR = "identity";
+    private static final String NAME_ATTR = "name";
+
+    static final String PROP_INITIAL_USER_IDENTITY_PREFIX = "Initial User Identity ";
+    static final String PROP_TENANTS_FILE = "Users File";
+    static final Pattern INITIAL_USER_IDENTITY_PATTERN = Pattern.compile(PROP_INITIAL_USER_IDENTITY_PREFIX + "\\S+");
+
+    private Schema usersSchema;
+    private Schema tenantsSchema;
+    private NiFiRegistryProperties properties;
+    private File tenantsFile;
+    private File restoreTenantsFile;
+    private Set<String> initialUserIdentities;
+    private List<IdentityMapping> identityMappings;
+
+    private final AtomicReference<UserGroupHolder> userGroupHolder = new AtomicReference<>();
+
+    @Override
+    public void initialize(UserGroupProviderInitializationContext initializationContext) throws SecurityProviderCreationException {
+        try {
+            final SchemaFactory schemaFactory = SchemaFactory.newInstance(XMLConstants.W3C_XML_SCHEMA_NS_URI);
+            tenantsSchema = schemaFactory.newSchema(FileAuthorizer.class.getResource(TENANTS_XSD));
+            //usersSchema = schemaFactory.newSchema(FileAuthorizer.class.getResource(USERS_XSD));
+        } catch (Exception e) {
+            throw new SecurityProviderCreationException(e);
+        }
+    }
+
+    @Override
+    public void onConfigured(AuthorizerConfigurationContext configurationContext) throws SecurityProviderCreationException {
+        try {
+            final PropertyValue tenantsPath = configurationContext.getProperty(PROP_TENANTS_FILE);
+            if (StringUtils.isBlank(tenantsPath.getValue())) {
+                throw new SecurityProviderCreationException("The users file must be specified.");
+            }
+
+            // get the tenants file and ensure it exists
+            tenantsFile = new File(tenantsPath.getValue());
+            if (!tenantsFile.exists()) {
+                logger.info("Creating new users file at {}", new Object[] {tenantsFile.getAbsolutePath()});
+                saveTenants(new Tenants());
+            }
+
+            final File tenantsFileDirectory = tenantsFile.getAbsoluteFile().getParentFile();
+
+            // extract the identity mappings from nifi-registry.properties if any are provided
+            identityMappings = Collections.unmodifiableList(IdentityMappingUtil.getIdentityMappings(properties));
+
+            // extract any nifi identities
+            initialUserIdentities = new HashSet<>();
+            for (Map.Entry<String,String> entry : configurationContext.getProperties().entrySet()) {
+                Matcher matcher = INITIAL_USER_IDENTITY_PATTERN.matcher(entry.getKey());
+                if (matcher.matches() && !StringUtils.isBlank(entry.getValue())) {
+                    initialUserIdentities.add(IdentityMappingUtil.mapIdentity(entry.getValue(), identityMappings));
+                }
+            }
+
+            load();
+
+            // if we've copied the authorizations file to a restore directory synchronize it
+            if (restoreTenantsFile != null) {
+                FileUtils.copyFile(tenantsFile, restoreTenantsFile, false, false, logger);
+            }
+
+            logger.info(String.format("Users/Groups file loaded at %s", new Date().toString()));
+        } catch (IOException | SecurityProviderCreationException | JAXBException | IllegalStateException | SAXException e) {
+            throw new SecurityProviderCreationException(e);
+        }
+    }
+
+    @Override
+    public Set<User> getUsers() throws AuthorizationAccessException {
+        return userGroupHolder.get().getAllUsers();
+    }
+
+    @Override
+    public synchronized User addUser(User user) throws AuthorizationAccessException {
+        if (user == null) {
+            throw new IllegalArgumentException("User cannot be null");
+        }
+
+        final org.apache.nifi.registry.security.authorization.file.tenants.generated.User jaxbUser = createJAXBUser(user);
+
+        final UserGroupHolder holder = userGroupHolder.get();
+        final Tenants tenants = holder.getTenants();
+        tenants.getUsers().getUser().add(jaxbUser);
+
+        saveAndRefreshHolder(tenants);
+
+        return userGroupHolder.get().getUsersById().get(user.getIdentifier());
+    }
+
+    @Override
+    public User getUser(String identifier) throws AuthorizationAccessException {
+        if (identifier == null) {
+            return null;
+        }
+
+        final UserGroupHolder holder = userGroupHolder.get();
+        return holder.getUsersById().get(identifier);
+    }
+
+    @Override
+    public synchronized User updateUser(User user) throws AuthorizationAccessException {
+        if (user == null) {
+            throw new IllegalArgumentException("User cannot be null");
+        }
+
+        final UserGroupHolder holder = userGroupHolder.get();
+        final Tenants tenants = holder.getTenants();
+
+        final List<org.apache.nifi.registry.security.authorization.file.tenants.generated.User> users = tenants.getUsers().getUser();
+
+        // fine the User that needs to be updated
+        org.apache.nifi.registry.security.authorization.file.tenants.generated.User updateUser = null;
+        for (org.apache.nifi.registry.security.authorization.file.tenants.generated.User jaxbUser : users) {
+            if (user.getIdentifier().equals(jaxbUser.getIdentifier())) {
+                updateUser = jaxbUser;
+                break;
+            }
+        }
+
+        // if user wasn't found return null, otherwise update the user and save changes
+        if (updateUser == null) {
+            return null;
+        } else {
+            updateUser.setIdentity(user.getIdentity());
+            saveAndRefreshHolder(tenants);
+
+            return userGroupHolder.get().getUsersById().get(user.getIdentifier());
+        }
+    }
+
+    @Override
+    public User getUserByIdentity(String identity) throws AuthorizationAccessException {
+        if (identity == null) {
+            return null;
+        }
+
+        final UserGroupHolder holder = userGroupHolder.get();
+        return holder.getUsersByIdentity().get(identity);
+    }
+
+    @Override
+    public synchronized User deleteUser(User user) throws AuthorizationAccessException {
+        if (user == null) {
+            throw new IllegalArgumentException("User cannot be null");
+        }
+
+        return deleteUser(user.getIdentifier());
+    }
+
+    @Override
+    public synchronized User deleteUser(String userIdentifier) throws AuthorizationAccessException {
+        if (userIdentifier == null) {
+            throw new IllegalArgumentException("User identifier cannot be null");
+        }
+
+        final UserGroupHolder holder = userGroupHolder.get();
+        final User deletedUser = holder.getUsersById().get(userIdentifier);
+        if (deletedUser == null) {
+            return null;
+        }
+
+        // for each group iterate over the user references and remove the user reference if it matches the user being deleted
+        final Tenants tenants = holder.getTenants();
+        for (org.apache.nifi.registry.security.authorization.file.tenants.generated.Group group : tenants.getGroups().getGroup()) {
+            Iterator<org.apache.nifi.registry.security.authorization.file.tenants.generated.Group.User> groupUserIter = group.getUser().iterator();
+            while (groupUserIter.hasNext()) {
+                org.apache.nifi.registry.security.authorization.file.tenants.generated.Group.User groupUser = groupUserIter.next();
+                if (groupUser.getIdentifier().equals(userIdentifier)) {
+                    groupUserIter.remove();
+                    break;
+                }
+            }
+        }
+
+        // remove the actual user
+        Iterator<org.apache.nifi.registry.security.authorization.file.tenants.generated.User> iter = tenants.getUsers().getUser().iterator();
+        while (iter.hasNext()) {
+            org.apache.nifi.registry.security.authorization.file.tenants.generated.User jaxbUser = iter.next();
+            if (userIdentifier.equals(jaxbUser.getIdentifier())) {
+                iter.remove();
+                break;
+            }
+        }
+
+        saveAndRefreshHolder(tenants);
+        return deletedUser;
+    }
+
+    @Override
+    public Set<Group> getGroups() throws AuthorizationAccessException {
+        return userGroupHolder.get().getAllGroups();
+    }
+
+    @Override
+    public synchronized Group addGroup(Group group) throws AuthorizationAccessException {
+        if (group == null) {
+            throw new IllegalArgumentException("Group cannot be null");
+        }
+
+        final UserGroupHolder holder = userGroupHolder.get();
+        final Tenants tenants = holder.getTenants();
+
+        // create a new JAXB Group based on the incoming Group
+        final org.apache.nifi.registry.security.authorization.file.tenants.generated.Group jaxbGroup =
+                new org.apache.nifi.registry.security.authorization.file.tenants.generated.Group();
+        jaxbGroup.setIdentifier(group.getIdentifier());
+        jaxbGroup.setName(group.getName());
+
+        // add each user to the group
+        for (String groupUser : group.getUsers()) {
+            org.apache.nifi.registry.security.authorization.file.tenants.generated.Group.User jaxbGroupUser =
+                    new org.apache.nifi.registry.security.authorization.file.tenants.generated.Group.User();
+            jaxbGroupUser.setIdentifier(groupUser);
+            jaxbGroup.getUser().add(jaxbGroupUser);
+        }
+
+        tenants.getGroups().getGroup().add(jaxbGroup);
+        saveAndRefreshHolder(tenants);
+
+        return userGroupHolder.get().getGroupsById().get(group.getIdentifier());
+    }
+
+    @Override
+    public Group getGroup(String identifier) throws AuthorizationAccessException {
+        if (identifier == null) {
+            return null;
+        }
+        return userGroupHolder.get().getGroupsById().get(identifier);
+    }
+
+    @Override
+    public UserAndGroups getUserAndGroups(final String identity) throws AuthorizationAccessException {
+        final UserGroupHolder holder = userGroupHolder.get();
+        final User user = holder.getUser(identity);
+        final Set<Group> groups = holder.getGroups(identity);
+
+        return new UserAndGroups() {
+            @Override
+            public User getUser() {
+                return user;
+            }
+
+            @Override
+            public Set<Group> getGroups() {
+                return groups;
+            }
+        };
+    }
+
+    @Override
+    public synchronized Group updateGroup(Group group) throws AuthorizationAccessException {
+        if (group == null) {
+            throw new IllegalArgumentException("Group cannot be null");
+        }
+
+        final UserGroupHolder holder = userGroupHolder.get();
+        final Tenants tenants = holder.getTenants();
+
+        // find the group that needs to be update
+        org.apache.nifi.registry.security.authorization.file.tenants.generated.Group updateGroup = null;
+        for (org.apache.nifi.registry.security.authorization.file.tenants.generated.Group jaxbGroup : tenants.getGroups().getGroup()) {
+            if (jaxbGroup.getIdentifier().equals(group.getIdentifier())) {
+                updateGroup = jaxbGroup;
+                break;
+            }
+        }
+
+        // if the group wasn't found return null, otherwise update the group and save changes
+        if (updateGroup == null) {
+            return null;
+        }
+
+        // reset the list of users and add each user to the group
+        updateGroup.getUser().clear();
+        for (String groupUser : group.getUsers()) {
+            org.apache.nifi.registry.security.authorization.file.tenants.generated.Group.User jaxbGroupUser =
+                    new org.apache.nifi.registry.security.authorization.file.tenants.generated.Group.User();
+            jaxbGroupUser.setIdentifier(groupUser);
+            updateGroup.getUser().add(jaxbGroupUser);
+        }
+
+        updateGroup.setName(group.getName());
+        saveAndRefreshHolder(tenants);
+
+        return userGroupHolder.get().getGroupsById().get(group.getIdentifier());
+    }
+
+    @Override
+    public synchronized Group deleteGroup(Group group) throws AuthorizationAccessException {
+        if (group == null) {
+            throw new IllegalArgumentException("Group cannot be null");
+        }
+
+        return deleteGroup(group.getIdentifier());
+    }
+
+    @Override
+    public synchronized Group deleteGroup(String groupIdentifier) throws AuthorizationAccessException {
+        if (groupIdentifier == null) {
+            throw new IllegalArgumentException("Group identifier cannot be null");
+        }
+
+        final UserGroupHolder holder = userGroupHolder.get();
+        final Group deletedGroup = holder.getGroupsById().get(groupIdentifier);
+        if (deletedGroup == null) {
+            return null;
+        }
+
+        // now remove the actual group from the top-level list of groups
+        final Tenants tenants = holder.getTenants();
+        Iterator<org.apache.nifi.registry.security.authorization.file.tenants.generated.Group> iter = tenants.getGroups().getGroup().iterator();
+        while (iter.hasNext()) {
+            org.apache.nifi.registry.security.authorization.file.tenants.generated.Group jaxbGroup = iter.next();
+            if (groupIdentifier.equals(jaxbGroup.getIdentifier())) {
+                iter.remove();
+                break;
+            }
+        }
+
+        saveAndRefreshHolder(tenants);
+        return deletedGroup;
+    }
+
+    UserGroupHolder getUserGroupHolder() {
+        return userGroupHolder.get();
+    }
+
+    @AuthorizerContext
+    public void setNiFiProperties(NiFiRegistryProperties properties) {
+        this.properties = properties;
+    }
+
+    @Override
+    public synchronized void inheritFingerprint(String fingerprint) throws AuthorizationAccessException {
+        final UsersAndGroups usersAndGroups = parseUsersAndGroups(fingerprint);
+        usersAndGroups.getUsers().forEach(user -> addUser(user));
+        usersAndGroups.getGroups().forEach(group -> addGroup(group));
+    }
+
+    @Override
+    public void checkInheritability(String proposedFingerprint) throws AuthorizationAccessException {
+        try {
+            // ensure we understand the proposed fingerprint
+            parseUsersAndGroups(proposedFingerprint);
+        } catch (final AuthorizationAccessException e) {
+            throw new UninheritableAuthorizationsException("Unable to parse the proposed fingerprint: " + e);
+        }
+
+        final UserGroupHolder usersAndGroups = userGroupHolder.get();
+
+        // ensure we are in a proper state to inherit the fingerprint
+        if (!usersAndGroups.getAllUsers().isEmpty() || !usersAndGroups.getAllGroups().isEmpty()) {
+            throw new UninheritableAuthorizationsException("Proposed fingerprint is not inheritable because the current users and groups is not empty.");
+        }
+    }
+
+    @Override
+    public String getFingerprint() throws AuthorizationAccessException {
+        final UserGroupHolder usersAndGroups = userGroupHolder.get();
+
+        final List<User> users = new ArrayList<>(usersAndGroups.getAllUsers());
+        Collections.sort(users, Comparator.comparing(User::getIdentifier));
+
+        final List<Group> groups = new ArrayList<>(usersAndGroups.getAllGroups());
+        Collections.sort(groups, Comparator.comparing(Group::getIdentifier));
+
+        XMLStreamWriter writer = null;
+        final StringWriter out = new StringWriter();
+        try {
+            writer = XML_OUTPUT_FACTORY.createXMLStreamWriter(out);
+            writer.writeStartDocument();
+            writer.writeStartElement("tenants");
+
+            for (User user : users) {
+                writeUser(writer, user);
+            }
+            for (Group group : groups) {
+                writeGroup(writer, group);
+            }
+
+            writer.writeEndElement();
+            writer.writeEndDocument();
+            writer.flush();
+        } catch (XMLStreamException e) {
+            throw new AuthorizationAccessException("Unable to generate fingerprint", e);
+        } finally {
+            if (writer != null) {
+                try {
+                    writer.close();
+                } catch (XMLStreamException e) {
+                    // nothing to do here
+                }
+            }
+        }
+
+        return out.toString();
+    }
+
+    private UsersAndGroups parseUsersAndGroups(final String fingerprint) {
+        final List<User> users = new ArrayList<>();
+        final List<Group> groups = new ArrayList<>();
+
+        final byte[] fingerprintBytes = fingerprint.getBytes(StandardCharsets.UTF_8);
+        try (final ByteArrayInputStream in = new ByteArrayInputStream(fingerprintBytes)) {
+            final DocumentBuilder docBuilder = DOCUMENT_BUILDER_FACTORY.newDocumentBuilder();
+            final Document document = docBuilder.parse(in);
+            final Element rootElement = document.getDocumentElement();
+
+            // parse all the users and add them to the current user group provider
+            NodeList userNodes = rootElement.getElementsByTagName(USER_ELEMENT);
+            for (int i=0; i < userNodes.getLength(); i++) {
+                Node userNode = userNodes.item(i);
+                users.add(parseUser((Element) userNode));
+            }
+
+            // parse all the groups and add them to the current user group provider
+            NodeList groupNodes = rootElement.getElementsByTagName(GROUP_ELEMENT);
+            for (int i=0; i < groupNodes.getLength(); i++) {
+                Node groupNode = groupNodes.item(i);
+                groups.add(parseGroup((Element) groupNode));
+            }
+        } catch (SAXException | ParserConfigurationException | IOException e) {
+            throw new AuthorizationAccessException("Unable to parse fingerprint", e);
+        }
+
+        return new UsersAndGroups(users, groups);
+    }
+
+    private User parseUser(final Element element) {
+        final User.Builder builder = new User.Builder()
+                .identifier(element.getAttribute(IDENTIFIER_ATTR))
+                .identity(element.getAttribute(IDENTITY_ATTR));
+
+        return builder.build();
+    }
+
+    private Group parseGroup(final Element element) {
+        final Group.Builder builder = new Group.Builder()
+                .identifier(element.getAttribute(IDENTIFIER_ATTR))
+                .name(element.getAttribute(NAME_ATTR));
+
+        NodeList groupUsers = element.getElementsByTagName(GROUP_USER_ELEMENT);
+        for (int i=0; i < groupUsers.getLength(); i++) {
+            Element groupUserNode = (Element) groupUsers.item(i);
+            builder.addUser(groupUserNode.getAttribute(IDENTIFIER_ATTR));
+        }
+
+        return builder.build();
+    }
+
+    private void writeUser(final XMLStreamWriter writer, final User user) throws XMLStreamException {
+        writer.writeStartElement(USER_ELEMENT);
+        writer.writeAttribute(IDENTIFIER_ATTR, user.getIdentifier());
+        writer.writeAttribute(IDENTITY_ATTR, user.getIdentity());
+        writer.writeEndElement();
+    }
+
+    private void writeGroup(final XMLStreamWriter writer, final Group group) throws XMLStreamException {
+        List<String> users = new ArrayList<>(group.getUsers());
+        Collections.sort(users);
+
+        writer.writeStartElement(GROUP_ELEMENT);
+        writer.writeAttribute(IDENTIFIER_ATTR, group.getIdentifier());
+        writer.writeAttribute(NAME_ATTR, group.getName());
+
+        for (String user : users) {
+            writer.writeStartElement(GROUP_USER_ELEMENT);
+            writer.writeAttribute(IDENTIFIER_ATTR, user);
+            writer.writeEndElement();
+        }
+
+        writer.writeEndElement();
+    }
+
+    private org.apache.nifi.registry.security.authorization.file.tenants.generated.User createJAXBUser(User user) {
+        final org.apache.nifi.registry.security.authorization.file.tenants.generated.User jaxbUser =
+                new org.apache.nifi.registry.security.authorization.file.tenants.generated.User();
+        jaxbUser.setIdentifier(user.getIdentifier());
+        jaxbUser.setIdentity(user.getIdentity());
+        return jaxbUser;
+    }
+
+    /**
+     * Loads the authorizations file and populates the AuthorizationsHolder, only called during start-up.
+     *
+     * @throws JAXBException            Unable to reload the authorized users file
+     * @throws IllegalStateException    Unable to sync file with restore
+     * @throws SAXException             Unable to unmarshall tenants
+     */
+    private synchronized void load() throws JAXBException, IllegalStateException, SAXException {
+        final Tenants tenants = unmarshallTenants();
+        if (tenants.getUsers() == null) {
+            tenants.setUsers(new Users());
+        }
+        if (tenants.getGroups() == null) {
+            tenants.setGroups(new Groups());
+        }
+
+        final UserGroupHolder userGroupHolder = new UserGroupHolder(tenants);
+        final boolean emptyTenants = userGroupHolder.getAllUsers().isEmpty() && userGroupHolder.getAllGroups().isEmpty();
+
+        if (emptyTenants) {
+
+            populateInitialUsers(tenants);
+
+            // save any changes that were made and repopulate the holder
+            saveAndRefreshHolder(tenants);
+        } else {
+            this.userGroupHolder.set(userGroupHolder);
+        }
+    }
+
+    private void saveTenants(final Tenants tenants) throws JAXBException {
+        final Marshaller marshaller = JAXB_TENANTS_CONTEXT.createMarshaller();
+        marshaller.setSchema(tenantsSchema);
+        marshaller.setProperty(Marshaller.JAXB_FORMATTED_OUTPUT, true);
+        marshaller.marshal(tenants, tenantsFile);
+    }
+
+    private Tenants unmarshallTenants() throws JAXBException {
+        final Unmarshaller unmarshaller = JAXB_TENANTS_CONTEXT.createUnmarshaller();
+        unmarshaller.setSchema(tenantsSchema);
+
+        final JAXBElement<Tenants> element = unmarshaller.unmarshal(new StreamSource(tenantsFile), Tenants.class);
+        return element.getValue();
+    }
+
+    private void populateInitialUsers(final Tenants tenants) {
+        for (String initialUserIdentity : initialUserIdentities) {
+            getOrCreateUser(tenants, initialUserIdentity);
+        }
+    }
+
+    /**
+     * Finds the User with the given identity, or creates a new one and adds it to the Tenants.
+     *
+     * @param tenants the Tenants reference
+     * @param userIdentity the user identity to find or create
+     * @return the User from Tenants with the given identity, or a new instance that was added to Tenants
+     */
+    private org.apache.nifi.registry.security.authorization.file.tenants.generated.User getOrCreateUser(final Tenants tenants, final String userIdentity) {
+        if (StringUtils.isBlank(userIdentity)) {
+            return null;
+        }
+
+        org.apache.nifi.registry.security.authorization.file.tenants.generated.User foundUser = null;
+        for (org.apache.nifi.registry.security.authorization.file.tenants.generated.User user : tenants.getUsers().getUser()) {
+            if (user.getIdentity().equals(userIdentity)) {
+                foundUser = user;
+                break;
+            }
+        }
+
+        if (foundUser == null) {
+            final String userIdentifier = IdentifierUtil.getIdentifier(userIdentity);
+            foundUser = new org.apache.nifi.registry.security.authorization.file.tenants.generated.User();
+            foundUser.setIdentifier(userIdentifier);
+            foundUser.setIdentity(userIdentity);
+            tenants.getUsers().getUser().add(foundUser);
+        }
+
+        return foundUser;
+    }
+
+    /**
+     * Finds the Group with the given name, or creates a new one and adds it to Tenants.
+     *
+     * @param tenants the Tenants reference
+     * @param groupName the name of the group to look for
+     * @return the Group from Tenants with the given name, or a new instance that was added to Tenants
+     */
+    private org.apache.nifi.registry.security.authorization.file.tenants.generated.Group getOrCreateGroup(final Tenants tenants, final String groupName) {
+        if (StringUtils.isBlank(groupName)) {
+            return null;
+        }
+
+        org.apache.nifi.registry.security.authorization.file.tenants.generated.Group foundGroup = null;
+        for (org.apache.nifi.registry.security.authorization.file.tenants.generated.Group group : tenants.getGroups().getGroup()) {
+            if (group.getName().equals(groupName)) {
+                foundGroup = group;
+                break;
+            }
+        }
+
+        if (foundGroup == null) {
+            final String newGroupIdentifier = IdentifierUtil.getIdentifier(groupName);
+            foundGroup = new org.apache.nifi.registry.security.authorization.file.tenants.generated.Group();
+            foundGroup.setIdentifier(newGroupIdentifier);
+            foundGroup.setName(groupName);
+            tenants.getGroups().getGroup().add(foundGroup);
+        }
+
+        return foundGroup;
+    }
+
+    /**
+     * Saves the Authorizations instance by marshalling to a file, then re-populates the
+     * in-memory data structures and sets the new holder.
+     *
+     * Synchronized to ensure only one thread writes the file at a time.
+     *
+     * @param tenants the tenants to save and populate from
+     * @throws AuthorizationAccessException if an error occurs saving the authorizations
+     */
+    private synchronized void saveAndRefreshHolder(final Tenants tenants) throws AuthorizationAccessException {
+        try {
+            saveTenants(tenants);
+
+            this.userGroupHolder.set(new UserGroupHolder(tenants));
+        } catch (JAXBException e) {
+            throw new AuthorizationAccessException("Unable to save Authorizations", e);
+        }
+    }
+
+    @Override
+    public void preDestruction() throws SecurityProviderDestructionException {
+    }
+
+    private static class UsersAndGroups {
+        final List<User> users;
+        final List<Group> groups;
+
+        public UsersAndGroups(List<User> users, List<Group> groups) {
+            this.users = users;
+            this.groups = groups;
+        }
+
+        public List<User> getUsers() {
+            return users;
+        }
+
+        public List<Group> getGroups() {
+            return groups;
+        }
+    }
+}

http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/6f26290d/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/security/authorization/file/IdentifierUtil.java
----------------------------------------------------------------------
diff --git a/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/security/authorization/file/IdentifierUtil.java b/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/security/authorization/file/IdentifierUtil.java
new file mode 100644
index 0000000..91e673f
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/security/authorization/file/IdentifierUtil.java
@@ -0,0 +1,35 @@
+/*
+ * 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.security.authorization.file;
+
+import org.apache.commons.lang3.StringUtils;
+
+import java.nio.charset.StandardCharsets;
+import java.util.UUID;
+
+public final class IdentifierUtil {
+
+    static String getIdentifier(final String seed) {
+        if (StringUtils.isBlank(seed)) {
+            return null;
+        }
+
+        return UUID.nameUUIDFromBytes(seed.getBytes(StandardCharsets.UTF_8)).toString();
+    }
+
+    private IdentifierUtil() {}
+}

http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/6f26290d/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/security/authorization/file/UserGroupHolder.java
----------------------------------------------------------------------
diff --git a/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/security/authorization/file/UserGroupHolder.java b/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/security/authorization/file/UserGroupHolder.java
new file mode 100644
index 0000000..9828a45
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/security/authorization/file/UserGroupHolder.java
@@ -0,0 +1,241 @@
+/*
+ * 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.security.authorization.file;
+
+
+import org.apache.nifi.registry.security.authorization.file.tenants.generated.Groups;
+import org.apache.nifi.registry.security.authorization.file.tenants.generated.Tenants;
+import org.apache.nifi.registry.security.authorization.file.tenants.generated.Users;
+import org.apache.nifi.registry.security.authorization.Group;
+import org.apache.nifi.registry.security.authorization.User;
+
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Map;
+import java.util.Set;
+
+/**
+ * A holder to provide atomic access to user group data structures.
+ */
+public class UserGroupHolder {
+
+    private final Tenants tenants;
+
+    private final Set<User> allUsers;
+    private final Map<String,User> usersById;
+    private final Map<String,User> usersByIdentity;
+
+    private final Set<Group> allGroups;
+    private final Map<String,Group> groupsById;
+    private final Map<String, Set<Group>> groupsByUserIdentity;
+
+    /**
+     * Creates a new holder and populates all convenience data structures.
+     *
+     * @param tenants the current tenants instance
+     */
+    public UserGroupHolder(final Tenants tenants) {
+        this.tenants = tenants;
+
+        // load all users
+        final Users users = tenants.getUsers();
+        final Set<User> allUsers = Collections.unmodifiableSet(createUsers(users));
+
+        // load all groups
+        final Groups groups = tenants.getGroups();
+        final Set<Group> allGroups = Collections.unmodifiableSet(createGroups(groups, users));
+
+        // create a convenience map to retrieve a user by id
+        final Map<String, User> userByIdMap = Collections.unmodifiableMap(createUserByIdMap(allUsers));
+
+        // create a convenience map to retrieve a user by identity
+        final Map<String, User> userByIdentityMap = Collections.unmodifiableMap(createUserByIdentityMap(allUsers));
+
+        // create a convenience map to retrieve a group by id
+        final Map<String, Group> groupByIdMap = Collections.unmodifiableMap(createGroupByIdMap(allGroups));
+
+        // create a convenience map to retrieve the groups for a user identity
+        final Map<String, Set<Group>> groupsByUserIdentityMap = Collections.unmodifiableMap(createGroupsByUserIdentityMap(allGroups, allUsers));
+
+        // set all the holders
+        this.allUsers = allUsers;
+        this.allGroups = allGroups;
+        this.usersById = userByIdMap;
+        this.usersByIdentity = userByIdentityMap;
+        this.groupsById = groupByIdMap;
+        this.groupsByUserIdentity = groupsByUserIdentityMap;
+    }
+
+    /**
+     * Creates a set of Users from the JAXB Users.
+     *
+     * @param users the JAXB Users
+     * @return a set of API Users matching the provided JAXB Users
+     */
+    private Set<User> createUsers(Users users) {
+        Set<User> allUsers = new HashSet<>();
+        if (users == null || users.getUser() == null) {
+            return allUsers;
+        }
+
+        for (org.apache.nifi.registry.security.authorization.file.tenants.generated.User user : users.getUser()) {
+            final User.Builder builder = new User.Builder()
+                    .identity(user.getIdentity())
+                    .identifier(user.getIdentifier());
+
+            allUsers.add(builder.build());
+        }
+
+        return allUsers;
+    }
+
+    /**
+     * Creates a set of Groups from the JAXB Groups.
+     *
+     * @param groups the JAXB Groups
+     * @return a set of API Groups matching the provided JAXB Groups
+     */
+    private Set<Group> createGroups(Groups groups,
+                                    Users users) {
+        Set<Group> allGroups = new HashSet<>();
+        if (groups == null || groups.getGroup() == null) {
+            return allGroups;
+        }
+
+        for (org.apache.nifi.registry.security.authorization.file.tenants.generated.Group group : groups.getGroup()) {
+            final Group.Builder builder = new Group.Builder()
+                    .identifier(group.getIdentifier())
+                    .name(group.getName());
+
+            for (org.apache.nifi.registry.security.authorization.file.tenants.generated.Group.User groupUser : group.getUser()) {
+                builder.addUser(groupUser.getIdentifier());
+            }
+
+            allGroups.add(builder.build());
+        }
+
+        return allGroups;
+    }
+
+    /**
+     * Creates a Map from user identifier to User.
+     *
+     * @param users the set of all users
+     * @return the Map from user identifier to User
+     */
+    private Map<String,User> createUserByIdMap(final Set<User> users) {
+        Map<String,User> usersMap = new HashMap<>();
+        for (User user : users) {
+            usersMap.put(user.getIdentifier(), user);
+        }
+        return usersMap;
+    }
+
+    /**
+     * Creates a Map from user identity to User.
+     *
+     * @param users the set of all users
+     * @return the Map from user identity to User
+     */
+    private Map<String,User> createUserByIdentityMap(final Set<User> users) {
+        Map<String,User> usersMap = new HashMap<>();
+        for (User user : users) {
+            usersMap.put(user.getIdentity(), user);
+        }
+        return usersMap;
+    }
+
+    /**
+     * Creates a Map from group identifier to Group.
+     *
+     * @param groups the set of all groups
+     * @return the Map from group identifier to Group
+     */
+    private Map<String,Group> createGroupByIdMap(final Set<Group> groups) {
+        Map<String,Group> groupsMap = new HashMap<>();
+        for (Group group : groups) {
+            groupsMap.put(group.getIdentifier(), group);
+        }
+        return groupsMap;
+    }
+
+    /**
+     * Creates a Map from user identity to the set of Groups for that identity.
+     *
+     * @param groups all groups
+     * @param users all users
+     * @return a Map from User identity to the set of Groups for that identity
+     */
+    private Map<String, Set<Group>> createGroupsByUserIdentityMap(final Set<Group> groups, final Set<User> users) {
+        Map<String, Set<Group>> groupsByUserIdentity = new HashMap<>();
+
+        for (User user : users) {
+            Set<Group> userGroups = new HashSet<>();
+            for (Group group : groups) {
+                for (String groupUser : group.getUsers()) {
+                    if (groupUser.equals(user.getIdentifier())) {
+                        userGroups.add(group);
+                    }
+                }
+            }
+
+            groupsByUserIdentity.put(user.getIdentity(), userGroups);
+        }
+
+        return groupsByUserIdentity;
+    }
+
+    public Tenants getTenants() {
+        return tenants;
+    }
+
+    public Set<User> getAllUsers() {
+        return allUsers;
+    }
+
+    public Map<String, User> getUsersById() {
+        return usersById;
+    }
+
+    public Map<String, User> getUsersByIdentity() {
+        return usersByIdentity;
+    }
+
+    public Set<Group> getAllGroups() {
+        return allGroups;
+    }
+
+    public Map<String, Group> getGroupsById() {
+        return groupsById;
+    }
+
+    public User getUser(String identity) {
+        if (identity == null) {
+            throw new IllegalArgumentException("Identity cannot be null");
+        }
+        return usersByIdentity.get(identity);
+    }
+
+    public Set<Group> getGroups(String userIdentity) {
+        if (userIdentity == null) {
+            throw new IllegalArgumentException("User Identity cannot be null");
+        }
+        return groupsByUserIdentity.get(userIdentity);
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/6f26290d/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/security/authorization/resource/Authorizable.java
----------------------------------------------------------------------
diff --git a/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/security/authorization/resource/Authorizable.java b/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/security/authorization/resource/Authorizable.java
new file mode 100644
index 0000000..d08467e
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/security/authorization/resource/Authorizable.java
@@ -0,0 +1,300 @@
+/*
+ * 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.security.authorization.resource;
+
+import org.apache.nifi.registry.security.authorization.AuthorizationResult.Result;
+import org.apache.nifi.registry.security.authorization.exception.AccessDeniedException;
+import org.apache.nifi.registry.security.authorization.user.NiFiUser;
+import org.apache.nifi.registry.security.authorization.AuthorizationAuditor;
+import org.apache.nifi.registry.security.authorization.AuthorizationRequest;
+import org.apache.nifi.registry.security.authorization.AuthorizationResult;
+import org.apache.nifi.registry.security.authorization.Authorizer;
+import org.apache.nifi.registry.security.authorization.RequestAction;
+import org.apache.nifi.registry.security.authorization.Resource;
+import org.apache.nifi.registry.security.authorization.UserContextKeys;
+
+import java.util.HashMap;
+import java.util.Map;
+
+public interface Authorizable {
+
+    /**
+     * The parent for this Authorizable. May be null.
+     *
+     * @return the parent authorizable or null
+     */
+    Authorizable getParentAuthorizable();
+
+    /**
+     * The Resource for this Authorizable.
+     *
+     * @return the resource
+     */
+    Resource getResource();
+
+    /**
+     * The originally requested resource for this Authorizable. Because policies are inherited, if a resource
+     * does not have a policy, this Authorizable may represent a parent resource and this method will return
+     * the originally requested resource.
+     *
+     * @return the originally requested resource
+     */
+    default Resource getRequestedResource() {
+        return getResource();
+    }
+
+    /**
+     * Returns whether the current user is authorized for the specified action on the specified resource. This
+     * method does not imply the user is directly attempting to access the specified resource. If the user is
+     * attempting a direct access use Authorizable.authorize().
+     *
+     * @param authorizer authorizer
+     * @param action action
+     * @return is authorized
+     */
+    default boolean isAuthorized(Authorizer authorizer, RequestAction action, NiFiUser user) {
+        return Result.Approved.equals(checkAuthorization(authorizer, action, user).getResult());
+    }
+
+    /**
+     * Returns the result of an authorization request for the specified user for the specified action on the specified
+     * resource. This method does not imply the user is directly attempting to access the specified resource. If the user is
+     * attempting a direct access use Authorizable.authorize().
+     *
+     * @param authorizer authorizer
+     * @param action action
+     * @param user user
+     * @return is authorized
+     */
+    default AuthorizationResult checkAuthorization(Authorizer authorizer, RequestAction action, NiFiUser user, Map<String, String> resourceContext) {
+        if (user == null) {
+            return AuthorizationResult.denied("Unknown user.");
+        }
+
+        final Map<String,String> userContext;
+        if (user.getClientAddress() != null && !user.getClientAddress().trim().isEmpty()) {
+            userContext = new HashMap<>();
+            userContext.put(UserContextKeys.CLIENT_ADDRESS.name(), user.getClientAddress());
+        } else {
+            userContext = null;
+        }
+
+        final Resource resource = getResource();
+        final Resource requestedResource = getRequestedResource();
+        final AuthorizationRequest request = new AuthorizationRequest.Builder()
+                .identity(user.getIdentity())
+                .groups(user.getGroups())
+                .anonymous(user.isAnonymous())
+                .accessAttempt(false)
+                .action(action)
+                .resource(resource)
+                .requestedResource(requestedResource)
+                .resourceContext(resourceContext)
+                .userContext(userContext)
+                .explanationSupplier(() -> {
+                    // build the safe explanation
+                    final StringBuilder safeDescription = new StringBuilder("Unable to ");
+
+                    if (RequestAction.READ.equals(action)) {
+                        safeDescription.append("view ");
+                    } else {
+                        safeDescription.append("modify "); // covers write or delete
+                    }
+                    safeDescription.append(resource.getSafeDescription()).append(".");
+
+                    return safeDescription.toString();
+                })
+                .build();
+
+        // perform the authorization
+        final AuthorizationResult result = authorizer.authorize(request);
+
+        // verify the results
+        if (Result.ResourceNotFound.equals(result.getResult())) {
+            final Authorizable parent = getParentAuthorizable();
+            if (parent == null) {
+                return AuthorizationResult.denied("No applicable policies could be found.");
+            } else {
+                // create a custom authorizable to override the safe description but still defer to the parent authorizable
+                final Authorizable parentProxy = new Authorizable() {
+                    @Override
+                    public Authorizable getParentAuthorizable() {
+                        return parent.getParentAuthorizable();
+                    }
+
+                    @Override
+                    public Resource getRequestedResource() {
+                        return requestedResource;
+                    }
+
+                    @Override
+                    public Resource getResource() {
+                        final Resource parentResource = parent.getResource();
+                        return new Resource() {
+                            @Override
+                            public String getIdentifier() {
+                                return parentResource.getIdentifier();
+                            }
+
+                            @Override
+                            public String getName() {
+                                return parentResource.getName();
+                            }
+
+                            @Override
+                            public String getSafeDescription() {
+                                return resource.getSafeDescription();
+                            }
+                        };
+                    }
+                };
+                return parentProxy.checkAuthorization(authorizer, action, user, resourceContext);
+            }
+        } else {
+            return result;
+        }
+    }
+
+    /**
+     * Returns the result of an authorization request for the specified user for the specified action on the specified
+     * resource. This method does not imply the user is directly attempting to access the specified resource. If the user is
+     * attempting a direct access use Authorizable.authorize().
+     *
+     * @param authorizer authorizer
+     * @param action action
+     * @param user user
+     * @return is authorized
+     */
+    default AuthorizationResult checkAuthorization(Authorizer authorizer, RequestAction action, NiFiUser user) {
+        return checkAuthorization(authorizer, action, user, null);
+    }
+
+    /**
+     * Authorizes the current user for the specified action on the specified resource. This method does imply the user is
+     * directly accessing the specified resource.
+     *
+     * @param authorizer authorizer
+     * @param action action
+     * @param user user
+     * @param resourceContext resource context
+     */
+    default void authorize(Authorizer authorizer, RequestAction action, NiFiUser user, Map<String, String> resourceContext) throws AccessDeniedException {
+        if (user == null) {
+            throw new AccessDeniedException("Unknown user.");
+        }
+
+        final Map<String,String> userContext;
+        if (user.getClientAddress() != null && !user.getClientAddress().trim().isEmpty()) {
+            userContext = new HashMap<>();
+            userContext.put(UserContextKeys.CLIENT_ADDRESS.name(), user.getClientAddress());
+        } else {
+            userContext = null;
+        }
+
+        final Resource resource = getResource();
+        final Resource requestedResource = getRequestedResource();
+        final AuthorizationRequest request = new AuthorizationRequest.Builder()
+                .identity(user.getIdentity())
+                .groups(user.getGroups())
+                .anonymous(user.isAnonymous())
+                .accessAttempt(true)
+                .action(action)
+                .resource(resource)
+                .requestedResource(requestedResource)
+                .resourceContext(resourceContext)
+                .userContext(userContext)
+                .explanationSupplier(() -> {
+                    // build the safe explanation
+                    final StringBuilder safeDescription = new StringBuilder("Unable to ");
+
+                    if (RequestAction.READ.equals(action)) {
+                        safeDescription.append("view ");
+                    } else {
+                        safeDescription.append("modify ");
+                    }
+                    safeDescription.append(resource.getSafeDescription()).append(".");
+
+                    return safeDescription.toString();
+                })
+                .build();
+
+        final AuthorizationResult result = authorizer.authorize(request);
+        if (Result.ResourceNotFound.equals(result.getResult())) {
+            final Authorizable parent = getParentAuthorizable();
+            if (parent == null) {
+                final AuthorizationResult failure = AuthorizationResult.denied("No applicable policies could be found.");
+
+                // audit authorization request
+                if (authorizer instanceof AuthorizationAuditor) {
+                    ((AuthorizationAuditor) authorizer).auditAccessAttempt(request, failure);
+                }
+
+                // denied
+                throw new AccessDeniedException(failure.getExplanation());
+            } else {
+                // create a custom authorizable to override the safe description but still defer to the parent authorizable
+                final Authorizable parentProxy = new Authorizable() {
+                    @Override
+                    public Authorizable getParentAuthorizable() {
+                        return parent.getParentAuthorizable();
+                    }
+
+                    @Override
+                    public Resource getRequestedResource() {
+                        return requestedResource;
+                    }
+
+                    @Override
+                    public Resource getResource() {
+                        final Resource parentResource = parent.getResource();
+                        return new Resource() {
+                            @Override
+                            public String getIdentifier() {
+                                return parentResource.getIdentifier();
+                            }
+
+                            @Override
+                            public String getName() {
+                                return parentResource.getName();
+                            }
+
+                            @Override
+                            public String getSafeDescription() {
+                                return resource.getSafeDescription();
+                            }
+                        };
+                    }
+                };
+                parentProxy.authorize(authorizer, action, user, resourceContext);
+            }
+        } else if (Result.Denied.equals(result.getResult())) {
+            throw new AccessDeniedException(result.getExplanation());
+        }
+    }
+
+    /**
+     * Authorizes the current user for the specified action on the specified resource. This method does imply the user is
+     * directly accessing the specified resource.
+     *
+     * @param authorizer authorizer
+     * @param action action
+     * @param user user
+     */
+    default void authorize(Authorizer authorizer, RequestAction action, NiFiUser user) throws AccessDeniedException {
+        authorize(authorizer, action, user, null);
+    }
+}

http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/6f26290d/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/security/authorization/resource/InheritingAuthorizable.java
----------------------------------------------------------------------
diff --git a/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/security/authorization/resource/InheritingAuthorizable.java b/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/security/authorization/resource/InheritingAuthorizable.java
new file mode 100644
index 0000000..b029229
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/security/authorization/resource/InheritingAuthorizable.java
@@ -0,0 +1,85 @@
+/*
+ * 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.security.authorization.resource;
+
+import org.apache.nifi.registry.security.authorization.AuthorizationResult;
+import org.apache.nifi.registry.security.authorization.AuthorizationResult.Result;
+import org.apache.nifi.registry.security.authorization.Authorizer;
+import org.apache.nifi.registry.security.authorization.RequestAction;
+import org.apache.nifi.registry.security.authorization.exception.AccessDeniedException;
+import org.apache.nifi.registry.security.authorization.user.NiFiUser;
+
+import java.util.Map;
+
+public interface InheritingAuthorizable extends Authorizable {
+
+    /**
+     * Returns the result of an authorization request for the specified user for the specified action on the specified
+     * resource. This method does not imply the user is directly attempting to access the specified resource. If the user is
+     * attempting a direct access use Authorizable.authorize().
+     *
+     * @param authorizer authorizer
+     * @param action action
+     * @param user user
+     * @return is authorized
+     */
+    default AuthorizationResult checkAuthorization(Authorizer authorizer, RequestAction action, NiFiUser user, Map<String, String> resourceContext) {
+        if (user == null) {
+            throw new AccessDeniedException("Unknown user.");
+        }
+
+        final AuthorizationResult resourceResult = Authorizable.super.checkAuthorization(authorizer, action, user, resourceContext);
+
+        // if we're denied from the resource try inheriting
+        if (Result.Denied.equals(resourceResult.getResult()) && getParentAuthorizable() != null) {
+            return getParentAuthorizable().checkAuthorization(authorizer, action, user, resourceContext);
+        } else {
+            return resourceResult;
+        }
+    }
+
+    /**
+     * Authorizes the current user for the specified action on the specified resource. If the current user is
+     * not in the access policy for the specified resource, the parent authorizable resource will be checked, recursively
+     *
+     * @param authorizer authorizer
+     * @param action action
+     * @param user user
+     * @param resourceContext resource context
+     */
+    default void authorize(Authorizer authorizer, RequestAction action, NiFiUser user, Map<String, String> resourceContext) throws AccessDeniedException {
+        if (user == null) {
+            throw new AccessDeniedException("Unknown user.");
+        }
+
+        try {
+            Authorizable.super.authorize(authorizer, action, user, resourceContext);
+        } catch (final AccessDeniedException resourceDenied) {
+            // if we're denied from the resource try inheriting
+            try {
+                if (getParentAuthorizable() != null) {
+                    getParentAuthorizable().authorize(authorizer, action, user, resourceContext);
+                } else {
+                    throw resourceDenied;
+                }
+            } catch (final AccessDeniedException policiesDenied) {
+                throw resourceDenied;
+            }
+        }
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/6f26290d/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/security/authorization/resource/ResourceFactory.java
----------------------------------------------------------------------
diff --git a/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/security/authorization/resource/ResourceFactory.java b/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/security/authorization/resource/ResourceFactory.java
new file mode 100644
index 0000000..c605d4a
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/security/authorization/resource/ResourceFactory.java
@@ -0,0 +1,235 @@
+/*
+ * 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.security.authorization.resource;
+
+import org.apache.nifi.registry.security.authorization.Resource;
+
+import java.util.Objects;
+
+public final class ResourceFactory {
+
+    private final static Resource BUCKETS_RESOURCE = new Resource() {
+        @Override
+        public String getIdentifier() {
+            return ResourceType.Bucket.getValue();
+        }
+
+        @Override
+        public String getName() {
+            return "Buckets";
+        }
+
+        @Override
+        public String getSafeDescription() {
+            return "buckets";
+        }
+    };
+
+    private final static Resource PROXY_RESOURCE = new Resource() {
+        @Override
+        public String getIdentifier() {
+            return ResourceType.Proxy.getValue();
+        }
+
+        @Override
+        public String getName() {
+            return "Proxy User Requests";
+        }
+
+        @Override
+        public String getSafeDescription() {
+            return "proxy requests on behalf of users";
+        }
+    };
+
+    private final static Resource TENANTS_RESOURCE = new Resource() {
+        @Override
+        public String getIdentifier() {
+            return ResourceType.Tenant.getValue();
+        }
+
+        @Override
+        public String getName() {
+            return "Tenants";
+        }
+
+        @Override
+        public String getSafeDescription() {
+            return "users/user groups";
+        }
+    };
+
+    private final static Resource POLICIES_RESOURCE = new Resource() {
+
+        @Override
+        public String getIdentifier() {
+            return ResourceType.Policy.getValue();
+        }
+
+        @Override
+        public String getName() {
+            return "Access Policies";
+        }
+
+        @Override
+        public String getSafeDescription() {
+            return "policies";
+        }
+    };
+
+    private final static Resource ACTUATOR_RESOURCE = new Resource() {
+        @Override
+        public String getIdentifier() {
+            return ResourceType.Actuator.getValue();
+        }
+
+        @Override
+        public String getName() {
+            return "Actuator";
+        }
+
+        @Override
+        public String getSafeDescription() {
+            return "actuator";
+        }
+    };
+
+    private final static Resource SWAGGER_RESOURCE = new Resource() {
+        @Override
+        public String getIdentifier() {
+            return ResourceType.Swagger.getValue();
+        }
+
+        @Override
+        public String getName() {
+            return "Swagger";
+        }
+
+        @Override
+        public String getSafeDescription() {
+            return "swagger";
+        }
+    };
+
+    /**
+     * Gets the Resource for actuator system management endpoints.
+     *
+     * @return  The resource for actuator system management endpoints.
+     */
+    public static Resource getActuatorResource() {
+        return ACTUATOR_RESOURCE;
+    }
+
+    /**
+     * Gets the Resource for swagger UI static resources.
+     *
+     * @return  The resource for swagger UI static resources.
+     */
+    public static Resource getSwaggerResource() {
+        return SWAGGER_RESOURCE;
+    }
+
+    /**
+     * Gets the Resource for proxying a user request.
+     *
+     * @return  The resource for proxying a user request
+     */
+    public static Resource getProxyResource() {
+        return PROXY_RESOURCE;
+    }
+
+    /**
+     * Gets the Resource for accessing Tenants which includes creating, modifying, and deleting Users and UserGroups.
+     *
+     * @return The Resource for accessing Tenants
+     */
+    public static Resource getTenantsResource() {
+        return TENANTS_RESOURCE;
+    }
+
+    /**
+     * Gets the {@link Resource} for accessing access policies.
+     * @return The policies resource
+     */
+    public static Resource getPoliciesResource() {
+        return POLICIES_RESOURCE;
+    }
+
+    /**
+     * Gets the {@link Resource} for accessing buckets.
+     * @return The buckets resource
+     */
+    public static Resource getBucketsResource() {
+        return BUCKETS_RESOURCE;
+    }
+
+    /**
+     * Gets the {@link Resource} for accessing buckets.
+     * @return The buckets resource
+     */
+    public static Resource getBucketResource(String bucketIdentifier, String bucketName) {
+        return getChildResource(ResourceType.Bucket, bucketIdentifier, bucketName);
+    }
+
+    /**
+     * Get a Resource object for any object that has a base type and an identifier, ie:
+     * /buckets/{uuid}
+     *
+     * @param parentResourceType - Required, the base resource type
+     * @param childIdentifier - Required, the identity of this sub resource
+     * @param name - Optional, the name of the subresource
+     * @return A resource for this object
+     */
+    private static Resource getChildResource(final ResourceType parentResourceType, final String childIdentifier, final String name) {
+        Objects.requireNonNull(parentResourceType, "The base resource type must be specified.");
+        Objects.requireNonNull(childIdentifier, "The child identifier identifier must be specified.");
+
+        return new Resource() {
+            @Override
+            public String getIdentifier() {
+                return String.format("%s/%s", parentResourceType.getValue(), childIdentifier);
+            }
+
+            @Override
+            public String getName() {
+                return name;
+            }
+
+            @Override
+            public String getSafeDescription() {
+                final StringBuilder safeDescription = new StringBuilder();
+                switch (parentResourceType) {
+                    case Bucket:
+                        safeDescription.append("Bucket");
+                        break;
+                    default:
+                        safeDescription.append("Unknown resource type");
+                        break;
+                }
+                safeDescription.append(" with ID ");
+                safeDescription.append(childIdentifier);
+                return safeDescription.toString();
+            }
+        };
+
+    }
+
+    /**
+     * Prevent outside instantiation.
+     */
+    private ResourceFactory() {}
+}

http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/6f26290d/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/security/authorization/resource/ResourceType.java
----------------------------------------------------------------------
diff --git a/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/security/authorization/resource/ResourceType.java b/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/security/authorization/resource/ResourceType.java
new file mode 100644
index 0000000..0b77cd2
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/security/authorization/resource/ResourceType.java
@@ -0,0 +1,87 @@
+/*
+ * 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.security.authorization.resource;
+
+public enum ResourceType {
+    Bucket("/buckets"),
+    Policy("/policies"),
+    Proxy("/proxy"),
+    Tenant("/tenants"),
+    Actuator("/actuator"),
+    Swagger("/swagger");
+
+    final String value;
+
+    private ResourceType(final String value) {
+        this.value = value;
+    }
+
+    public String getValue() {
+        return value;
+    }
+
+    public static ResourceType valueOfValue(final String rawValue) {
+        ResourceType type = null;
+
+        for (final ResourceType rt : values()) {
+            if (rt.getValue().equals(rawValue)) {
+                type = rt;
+                break;
+            }
+        }
+
+        if (type == null) {
+            throw new IllegalArgumentException("Unknown resource type value " + rawValue);
+        }
+
+        return type;
+    }
+
+    /**
+     * Map an arbitrary resource path to its base resource type. The base resource type is
+     * what the resource path starts with.
+     *
+     * The resourcePath arg is expected to be a string of the format:
+     *
+     * {ResourceTypeValue}/arbitrary/sub-resource/path
+     *
+     * For example:
+     *   /buckets -> ResourceType.Bucket
+     *   /buckets/bucketId -> ResourceType.Bucket
+     *   /policies/read/buckets -> ResourceType.Policy
+     *
+     * @param resourcePath the path component of a URI (not including the context path)
+     * @return the base resource type
+     */
+    public static ResourceType mapFullResourcePathToResourceType(final String resourcePath) {
+        if (resourcePath == null) {
+            throw new IllegalArgumentException("Resource path must not be null");
+        }
+
+        ResourceType type = null;
+
+        for (final ResourceType rt : values()) {
+            final String rtValue = rt.getValue();
+            if(resourcePath.equals(rtValue) || resourcePath.startsWith(rtValue + "/"))  {
+                type = rt;
+                break;
+            }
+        }
+
+        return type;
+    }
+}
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/6f26290d/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/security/authorization/user/NiFiUser.java
----------------------------------------------------------------------
diff --git a/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/security/authorization/user/NiFiUser.java b/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/security/authorization/user/NiFiUser.java
new file mode 100644
index 0000000..47127b6
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/security/authorization/user/NiFiUser.java
@@ -0,0 +1,52 @@
+/*
+ * 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.security.authorization.user;
+
+import java.util.Set;
+
+/**
+ * A representation of a NiFi user that has logged into the application
+ */
+public interface NiFiUser {
+
+    /**
+     * @return the unique identity of this user
+     */
+    String getIdentity();
+
+    /**
+     * @return the groups that this user belongs to if this nifi is configured to load user groups, null otherwise.
+     */
+    Set<String> getGroups();
+
+    /**
+     * @return the next user in the proxied entities chain, or <code>null</code> if no more users exist in the chain.
+     */
+    NiFiUser getChain();
+
+    /**
+     * @return <code>true</code> if the user is the unauthenticated Anonymous user
+     */
+    boolean isAnonymous();
+
+    /**
+     * @return the address of the client that made the request which created this user
+     */
+    String getClientAddress();
+
+}

http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/6f26290d/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/security/authorization/user/NiFiUserDetails.java
----------------------------------------------------------------------
diff --git a/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/security/authorization/user/NiFiUserDetails.java b/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/security/authorization/user/NiFiUserDetails.java
new file mode 100644
index 0000000..ca6ea2e
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/security/authorization/user/NiFiUserDetails.java
@@ -0,0 +1,91 @@
+/*
+ * 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.security.authorization.user;
+
+import org.apache.commons.lang3.StringUtils;
+import org.springframework.security.core.GrantedAuthority;
+import org.springframework.security.core.userdetails.UserDetails;
+
+import java.util.Collection;
+import java.util.Collections;
+
+/**
+ * User details for a NiFi user.
+ */
+public class NiFiUserDetails implements UserDetails {
+
+    private final NiFiUser user;
+
+    /**
+     * Creates a new NiFiUserDetails.
+     *
+     * @param user user
+     */
+    public NiFiUserDetails(NiFiUser user) {
+        this.user = user;
+    }
+
+    /**
+     * Get the user for this UserDetails.
+     *
+     * @return user
+     */
+    public NiFiUser getNiFiUser() {
+        return user;
+    }
+
+    /**
+     * Returns the authorities that this NiFi user has.
+     *
+     * @return authorities
+     */
+    @Override
+    public Collection<? extends GrantedAuthority> getAuthorities() {
+        return Collections.EMPTY_SET;
+    }
+
+    @Override
+    public String getPassword() {
+        return StringUtils.EMPTY;
+    }
+
+    @Override
+    public String getUsername() {
+        return user.getIdentity();
+    }
+
+    @Override
+    public boolean isAccountNonExpired() {
+        return true;
+    }
+
+    @Override
+    public boolean isAccountNonLocked() {
+        return true;
+    }
+
+    @Override
+    public boolean isCredentialsNonExpired() {
+        return true;
+    }
+
+    @Override
+    public boolean isEnabled() {
+        return true;
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/6f26290d/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/security/authorization/user/NiFiUserUtils.java
----------------------------------------------------------------------
diff --git a/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/security/authorization/user/NiFiUserUtils.java b/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/security/authorization/user/NiFiUserUtils.java
new file mode 100644
index 0000000..b5147ea
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/security/authorization/user/NiFiUserUtils.java
@@ -0,0 +1,91 @@
+/*
+ * 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.security.authorization.user;
+
+import org.apache.commons.lang3.StringUtils;
+import org.springframework.security.core.Authentication;
+import org.springframework.security.core.context.SecurityContext;
+import org.springframework.security.core.context.SecurityContextHolder;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Utility methods for retrieving information about the current application user.
+ *
+ */
+public final class NiFiUserUtils {
+
+    /**
+     * Returns the current NiFiUser or null if the current user is not a NiFiUser.
+     *
+     * @return user
+     */
+    public static NiFiUser getNiFiUser() {
+        NiFiUser user = null;
+
+        // obtain the principal in the current authentication
+        final SecurityContext context = SecurityContextHolder.getContext();
+        final Authentication authentication = context.getAuthentication();
+        if (authentication != null) {
+            Object principal = authentication.getPrincipal();
+            if (principal instanceof NiFiUserDetails) {
+                user = ((NiFiUserDetails) principal).getNiFiUser();
+            }
+        }
+
+        return user;
+    }
+
+    public static String getNiFiUserIdentity() {
+        // get the nifi user to extract the username
+        NiFiUser user = NiFiUserUtils.getNiFiUser();
+        if (user == null) {
+            return "unknown";
+        } else {
+            return user.getIdentity();
+        }
+    }
+
+    /**
+     * Builds the proxy chain for the specified user.
+     *
+     * @param user The current user
+     * @return The proxy chain for that user in List form
+     */
+    public static List<String> buildProxiedEntitiesChain(final NiFiUser user) {
+        // calculate the dn chain
+        final List<String> proxyChain = new ArrayList<>();
+
+        // build the dn chain
+        NiFiUser chainedUser = user;
+        while (chainedUser != null) {
+            // add the entry for this user
+            if (chainedUser.isAnonymous()) {
+                // use an empty string to represent an anonymous user in the proxy entities chain
+                proxyChain.add(StringUtils.EMPTY);
+            } else {
+                proxyChain.add(chainedUser.getIdentity());
+            }
+
+            // go to the next user in the chain
+            chainedUser = chainedUser.getChain();
+        }
+
+        return proxyChain;
+    }
+}


[33/51] [partial] nifi-registry git commit: NIFIREG-201 Refactoring project structure to better isolate extensions

Posted by kd...@apache.org.
http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/6f26290d/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/security/authorization/user/StandardNiFiUser.java
----------------------------------------------------------------------
diff --git a/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/security/authorization/user/StandardNiFiUser.java b/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/security/authorization/user/StandardNiFiUser.java
new file mode 100644
index 0000000..92c2274
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/security/authorization/user/StandardNiFiUser.java
@@ -0,0 +1,189 @@
+/*
+ * 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.security.authorization.user;
+
+import org.apache.commons.lang3.StringUtils;
+
+import java.util.Collections;
+import java.util.Objects;
+import java.util.Set;
+
+/**
+ * An implementation of NiFiUser.
+ */
+public class StandardNiFiUser implements NiFiUser {
+
+    public static final String ANONYMOUS_IDENTITY = "anonymous";
+    public static final StandardNiFiUser ANONYMOUS = new Builder().identity(ANONYMOUS_IDENTITY).anonymous(true).build();
+
+    private final String identity;
+    private final Set<String> groups;
+    private final NiFiUser chain;
+    private final String clientAddress;
+    private final boolean isAnonymous;
+
+    private StandardNiFiUser(final Builder builder) {
+        this.identity = builder.identity;
+        this.groups = builder.groups == null ? null : Collections.unmodifiableSet(builder.groups);
+        this.chain = builder.chain;
+        this.clientAddress = builder.clientAddress;
+        this.isAnonymous = builder.isAnonymous;
+    }
+
+    /**
+     * This static builder allows the chain and clientAddress to be populated without allowing calling code to provide a non-anonymous identity of the anonymous user.
+     *
+     * @param chain the proxied entities in {@see NiFiUser} form
+     * @param clientAddress the address the request originated from
+     * @return an anonymous user instance with the identity "anonymous"
+     */
+    public static StandardNiFiUser populateAnonymousUser(NiFiUser chain, String clientAddress) {
+        return new Builder().identity(ANONYMOUS_IDENTITY).chain(chain).clientAddress(clientAddress).anonymous(true).build();
+    }
+
+    @Override
+    public String getIdentity() {
+        return identity;
+    }
+
+    @Override
+    public Set<String> getGroups() {
+        return groups;
+    }
+
+    @Override
+    public NiFiUser getChain() {
+        return chain;
+    }
+
+    @Override
+    public boolean isAnonymous() {
+        return isAnonymous;
+    }
+
+    @Override
+    public String getClientAddress() {
+        return clientAddress;
+    }
+
+    @Override
+    public boolean equals(Object obj) {
+        if (obj == null) {
+            return false;
+        }
+
+        if (!(obj instanceof NiFiUser)) {
+            return false;
+        }
+
+        final NiFiUser other = (NiFiUser) obj;
+        return Objects.equals(this.identity, other.getIdentity());
+    }
+
+    @Override
+    public int hashCode() {
+        int hash = 7;
+        hash = 53 * hash + Objects.hashCode(this.identity);
+        return hash;
+    }
+
+    @Override
+    public String toString() {
+        final String formattedGroups;
+        if (groups == null) {
+            formattedGroups = "none";
+        } else {
+            formattedGroups = StringUtils.join(groups, ", ");
+        }
+
+        return String.format("identity[%s], groups[%s]", getIdentity(), formattedGroups);
+    }
+
+    /**
+     * Builder for a StandardNiFiUser
+     */
+    public static class Builder {
+
+        private String identity;
+        private Set<String> groups;
+        private NiFiUser chain;
+        private String clientAddress;
+        private boolean isAnonymous = false;
+
+        /**
+         * Sets the identity.
+         *
+         * @param identity the identity string for the user (i.e. "Andy" or "CN=alopresto, OU=Apache NiFi")
+         * @return the builder
+         */
+        public Builder identity(final String identity) {
+            this.identity = identity;
+            return this;
+        }
+
+        /**
+         * Sets the groups.
+         *
+         * @param groups the user groups
+         * @return the builder
+         */
+        public Builder groups(final Set<String> groups) {
+            this.groups = groups;
+            return this;
+        }
+
+        /**
+         * Sets the chain.
+         *
+         * @param chain the proxy chain that leads to this users
+         * @return the builder
+         */
+        public Builder chain(final NiFiUser chain) {
+            this.chain = chain;
+            return this;
+        }
+
+        /**
+         * Sets the client address.
+         *
+         * @param clientAddress the source address of the request
+         * @return the builder
+         */
+        public Builder clientAddress(final String clientAddress) {
+            this.clientAddress = clientAddress;
+            return this;
+        }
+
+        /**
+         * Sets whether this user is the canonical "anonymous" user
+         *
+         * @param isAnonymous true to represent the canonical "anonymous" user
+         * @return the builder
+         */
+        private Builder anonymous(final boolean isAnonymous) {
+            this.isAnonymous = isAnonymous;
+            return this;
+        }
+
+        /**
+         * @return builds a StandardNiFiUser from the current state of the builder
+         */
+        public StandardNiFiUser build() {
+            return new StandardNiFiUser(this);
+        }
+    }
+}

http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/6f26290d/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/security/crypto/SensitivePropertyProviderConfiguration.java
----------------------------------------------------------------------
diff --git a/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/security/crypto/SensitivePropertyProviderConfiguration.java b/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/security/crypto/SensitivePropertyProviderConfiguration.java
new file mode 100644
index 0000000..7859492
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/security/crypto/SensitivePropertyProviderConfiguration.java
@@ -0,0 +1,67 @@
+/*
+ * 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.security.crypto;
+
+import org.apache.nifi.registry.properties.AESSensitivePropertyProvider;
+import org.apache.nifi.registry.properties.SensitivePropertyProtectionException;
+import org.apache.nifi.registry.properties.SensitivePropertyProvider;
+import org.apache.nifi.registry.properties.SensitivePropertyProviderFactory;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+
+import javax.crypto.NoSuchPaddingException;
+import java.security.NoSuchAlgorithmException;
+import java.security.NoSuchProviderException;
+
+@Configuration
+public class SensitivePropertyProviderConfiguration implements SensitivePropertyProviderFactory {
+
+    private static final Logger logger = LoggerFactory.getLogger(SensitivePropertyProviderConfiguration.class);
+
+    @Autowired(required = false)
+    private CryptoKeyProvider masterKeyProvider;
+
+    /**
+     * @return a SensitivePropertyProvider initialized with the master key if present,
+     *         or null if the master key is not present.
+     */
+    @Bean
+    @Override
+    public SensitivePropertyProvider getProvider() {
+        if (masterKeyProvider == null || masterKeyProvider.isEmpty()) {
+            // This NiFi Registry was not configured with a master key, so the assumption is
+            // the optional Spring bean normally provided by this method will never be needed
+            return null;
+        }
+
+        try {
+            // Note, this bean is intentionally NOT a singleton because we want the
+            // returned provider, which has a copy of the sensitive master key material
+            // to be reaped when it goes out of scope in order to decrease the time
+            // key material is held in memory.
+            String key = masterKeyProvider.getKey();
+            return new AESSensitivePropertyProvider(masterKeyProvider.getKey());
+        } catch (MissingCryptoKeyException | NoSuchAlgorithmException | NoSuchProviderException | NoSuchPaddingException e) {
+            logger.warn("Error creating AES Sensitive Property Provider", e);
+            throw new SensitivePropertyProtectionException("Error creating AES Sensitive Property Provider", e);
+        }
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/6f26290d/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/security/key/Key.java
----------------------------------------------------------------------
diff --git a/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/security/key/Key.java b/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/security/key/Key.java
new file mode 100644
index 0000000..c110fa8
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/security/key/Key.java
@@ -0,0 +1,69 @@
+/*
+ * 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.security.key;
+
+import java.io.Serializable;
+
+/**
+ * An signing key for a NiFi user.
+ */
+public class Key implements Serializable {
+
+    private String id;
+    private String identity;
+    private String key;
+
+    /**
+     * The key id.
+     *
+     * @return the id
+     */
+    public String getId() {
+        return id;
+    }
+
+    public void setId(String id) {
+        this.id = id;
+    }
+
+    /**
+     * The identity of the user this key is associated with.
+     *
+     * @return the identity
+     */
+    public String getIdentity() {
+        return identity;
+    }
+
+    public void setIdentity(String identity) {
+        this.identity = identity;
+    }
+
+    /**
+     * The signing key.
+     *
+     * @return the signing key
+     */
+    public String getKey() {
+        return key;
+    }
+
+    public void setKey(String key) {
+        this.key = key;
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/6f26290d/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/security/key/KeyService.java
----------------------------------------------------------------------
diff --git a/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/security/key/KeyService.java b/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/security/key/KeyService.java
new file mode 100644
index 0000000..3b9a7ca
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/security/key/KeyService.java
@@ -0,0 +1,46 @@
+/*
+ * 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.security.key;
+
+/**
+ * Manages NiFi user keys.
+ */
+public interface KeyService {
+
+    /**
+     * Gets a key for the specified user identity. Returns null if the user has not had a key issued
+     *
+     * @param id The key id
+     * @return The key or null
+     */
+    Key getKey(String id);
+
+    /**
+     * Gets a key for the specified user identity. If a key does not exist, one will be created.
+     *
+     * @param identity The user identity
+     * @return The key
+     */
+    Key getOrCreateKey(String identity);
+
+    /**
+     * Deletes keys for the specified identity.
+     *
+     * @param identity The user identity
+     */
+    void deleteKey(String identity);
+}

http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/6f26290d/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/security/ldap/IdentityStrategy.java
----------------------------------------------------------------------
diff --git a/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/security/ldap/IdentityStrategy.java b/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/security/ldap/IdentityStrategy.java
new file mode 100644
index 0000000..135f261
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/security/ldap/IdentityStrategy.java
@@ -0,0 +1,22 @@
+/*
+ * 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.security.ldap;
+
+public enum IdentityStrategy {
+    USE_DN,
+    USE_USERNAME;
+}

http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/6f26290d/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/security/ldap/LdapAuthenticationStrategy.java
----------------------------------------------------------------------
diff --git a/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/security/ldap/LdapAuthenticationStrategy.java b/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/security/ldap/LdapAuthenticationStrategy.java
new file mode 100644
index 0000000..331fbc3
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/security/ldap/LdapAuthenticationStrategy.java
@@ -0,0 +1,24 @@
+/*
+ * 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.security.ldap;
+
+public enum LdapAuthenticationStrategy {
+    ANONYMOUS,
+    SIMPLE,
+    LDAPS,
+    START_TLS
+}

http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/6f26290d/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/security/ldap/LdapIdentityProvider.java
----------------------------------------------------------------------
diff --git a/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/security/ldap/LdapIdentityProvider.java b/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/security/ldap/LdapIdentityProvider.java
new file mode 100644
index 0000000..4427ed9
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/security/ldap/LdapIdentityProvider.java
@@ -0,0 +1,355 @@
+/*
+ * 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.security.ldap;
+
+import org.apache.commons.lang3.StringUtils;
+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.IdentityProviderConfigurationContext;
+import org.apache.nifi.registry.security.authentication.exception.IdentityAccessException;
+import org.apache.nifi.registry.security.authentication.exception.InvalidCredentialsException;
+import org.apache.nifi.registry.security.exception.SecurityProviderCreationException;
+import org.apache.nifi.registry.security.exception.SecurityProviderDestructionException;
+import org.apache.nifi.registry.security.util.SslContextFactory;
+import org.apache.nifi.registry.security.util.SslContextFactory.ClientAuth;
+import org.apache.nifi.registry.util.FormatUtils;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.ldap.AuthenticationException;
+import org.springframework.ldap.core.support.AbstractTlsDirContextAuthenticationStrategy;
+import org.springframework.ldap.core.support.DefaultTlsDirContextAuthenticationStrategy;
+import org.springframework.ldap.core.support.LdapContextSource;
+import org.springframework.ldap.core.support.SimpleDirContextAuthenticationStrategy;
+import org.springframework.security.authentication.BadCredentialsException;
+import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
+import org.springframework.security.core.Authentication;
+import org.springframework.security.core.userdetails.UsernameNotFoundException;
+import org.springframework.security.ldap.authentication.AbstractLdapAuthenticationProvider;
+import org.springframework.security.ldap.authentication.BindAuthenticator;
+import org.springframework.security.ldap.authentication.LdapAuthenticationProvider;
+import org.springframework.security.ldap.search.FilterBasedLdapUserSearch;
+import org.springframework.security.ldap.search.LdapUserSearch;
+import org.springframework.security.ldap.userdetails.LdapUserDetails;
+
+import javax.naming.Context;
+import javax.net.ssl.SSLContext;
+import java.io.IOException;
+import java.security.KeyManagementException;
+import java.security.KeyStoreException;
+import java.security.NoSuchAlgorithmException;
+import java.security.UnrecoverableKeyException;
+import java.security.cert.CertificateException;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * LDAP based implementation of a login identity provider.
+ */
+public class LdapIdentityProvider extends BasicAuthIdentityProvider implements IdentityProvider {
+
+    private static final Logger logger = LoggerFactory.getLogger(LdapIdentityProvider.class);
+
+    private static final String issuer = LdapIdentityProvider.class.getSimpleName();
+
+    private AbstractLdapAuthenticationProvider ldapAuthenticationProvider;
+    private long expiration;
+    private IdentityStrategy identityStrategy;
+
+    @Override
+    public final void onConfigured(final IdentityProviderConfigurationContext configurationContext) throws SecurityProviderCreationException {
+        final String rawExpiration = configurationContext.getProperty("Authentication Expiration");
+        if (StringUtils.isBlank(rawExpiration)) {
+            throw new SecurityProviderCreationException("The Authentication Expiration must be specified.");
+        }
+
+        try {
+            expiration = FormatUtils.getTimeDuration(rawExpiration, TimeUnit.MILLISECONDS);
+        } catch (final IllegalArgumentException iae) {
+            throw new SecurityProviderCreationException(String.format("The Expiration Duration '%s' is not a valid time duration", rawExpiration));
+        }
+
+        final LdapContextSource context = new LdapContextSource();
+
+        final Map<String, Object> baseEnvironment = new HashMap<>();
+
+        // connect/read time out
+        setTimeout(configurationContext, baseEnvironment, "Connect Timeout", "com.sun.jndi.ldap.connect.timeout");
+        setTimeout(configurationContext, baseEnvironment, "Read Timeout", "com.sun.jndi.ldap.read.timeout");
+
+        // authentication strategy
+        final String rawAuthenticationStrategy = configurationContext.getProperty("Authentication Strategy");
+        final LdapAuthenticationStrategy authenticationStrategy;
+        try {
+            authenticationStrategy = LdapAuthenticationStrategy.valueOf(rawAuthenticationStrategy);
+        } catch (final IllegalArgumentException iae) {
+            throw new SecurityProviderCreationException(String.format("Unrecognized authentication strategy '%s'. Possible values are [%s]",
+                    rawAuthenticationStrategy, StringUtils.join(LdapAuthenticationStrategy.values(), ", ")));
+        }
+
+        switch (authenticationStrategy) {
+            case ANONYMOUS:
+                context.setAnonymousReadOnly(true);
+                break;
+            default:
+                final String userDn = configurationContext.getProperty("Manager DN");
+                final String password = configurationContext.getProperty("Manager Password");
+
+                context.setUserDn(userDn);
+                context.setPassword(password);
+
+                switch (authenticationStrategy) {
+                    case SIMPLE:
+                        context.setAuthenticationStrategy(new SimpleDirContextAuthenticationStrategy());
+                        break;
+                    case LDAPS:
+                        context.setAuthenticationStrategy(new SimpleDirContextAuthenticationStrategy());
+
+                        // indicate a secure connection
+                        baseEnvironment.put(Context.SECURITY_PROTOCOL, "ssl");
+
+                        // get the configured ssl context
+                        final SSLContext ldapsSslContext = getConfiguredSslContext(configurationContext);
+                        if (ldapsSslContext != null) {
+                            // initialize the ldaps socket factory prior to use
+                            LdapsSocketFactory.initialize(ldapsSslContext.getSocketFactory());
+                            baseEnvironment.put("java.naming.ldap.factory.socket", LdapsSocketFactory.class.getName());
+                        }
+                        break;
+                    case START_TLS:
+                        final AbstractTlsDirContextAuthenticationStrategy tlsAuthenticationStrategy = new DefaultTlsDirContextAuthenticationStrategy();
+
+                        // shutdown gracefully
+                        final String rawShutdownGracefully = configurationContext.getProperty("TLS - Shutdown Gracefully");
+                        if (StringUtils.isNotBlank(rawShutdownGracefully)) {
+                            final boolean shutdownGracefully = Boolean.TRUE.toString().equalsIgnoreCase(rawShutdownGracefully);
+                            tlsAuthenticationStrategy.setShutdownTlsGracefully(shutdownGracefully);
+                        }
+
+                        // get the configured ssl context
+                        final SSLContext startTlsSslContext = getConfiguredSslContext(configurationContext);
+                        if (startTlsSslContext != null) {
+                            tlsAuthenticationStrategy.setSslSocketFactory(startTlsSslContext.getSocketFactory());
+                        }
+
+                        // set the authentication strategy
+                        context.setAuthenticationStrategy(tlsAuthenticationStrategy);
+                        break;
+                }
+                break;
+        }
+
+        // referrals
+        final String rawReferralStrategy = configurationContext.getProperty("Referral Strategy");
+
+        final ReferralStrategy referralStrategy;
+        try {
+            referralStrategy = ReferralStrategy.valueOf(rawReferralStrategy);
+        } catch (final IllegalArgumentException iae) {
+            throw new SecurityProviderCreationException(String.format("Unrecognized referral strategy '%s'. Possible values are [%s]",
+                    rawReferralStrategy, StringUtils.join(ReferralStrategy.values(), ", ")));
+        }
+
+        // using the value as this needs to be the lowercase version while the value is configured with the enum constant
+        context.setReferral(referralStrategy.getValue());
+
+        // url
+        final String urls = configurationContext.getProperty("Url");
+
+        if (StringUtils.isBlank(urls)) {
+            throw new SecurityProviderCreationException("LDAP identity provider 'Url' must be specified.");
+        }
+
+        // connection
+        context.setUrls(StringUtils.split(urls));
+
+        // search criteria
+        final String userSearchBase = configurationContext.getProperty("User Search Base");
+        final String userSearchFilter = configurationContext.getProperty("User Search Filter");
+
+        if (StringUtils.isBlank(userSearchBase) || StringUtils.isBlank(userSearchFilter)) {
+            throw new SecurityProviderCreationException("LDAP identity provider 'User Search Base' and 'User Search Filter' must be specified.");
+        }
+
+        final LdapUserSearch userSearch = new FilterBasedLdapUserSearch(userSearchBase, userSearchFilter, context);
+
+        // bind
+        final BindAuthenticator authenticator = new BindAuthenticator(context);
+        authenticator.setUserSearch(userSearch);
+
+        // identity strategy
+        final String rawIdentityStrategy = configurationContext.getProperty("Identity Strategy");
+
+        if (StringUtils.isBlank(rawIdentityStrategy)) {
+            logger.info(String.format("Identity Strategy is not configured, defaulting strategy to %s.", IdentityStrategy.USE_DN));
+
+            // if this value is not configured, default to use dn which was the previous implementation
+            identityStrategy = IdentityStrategy.USE_DN;
+        } else {
+            try {
+                // attempt to get the configured identity strategy
+                identityStrategy = IdentityStrategy.valueOf(rawIdentityStrategy);
+            } catch (final IllegalArgumentException iae) {
+                throw new SecurityProviderCreationException(String.format("Unrecognized identity strategy '%s'. Possible values are [%s]",
+                        rawIdentityStrategy, StringUtils.join(IdentityStrategy.values(), ", ")));
+            }
+        }
+
+        // set the base environment is necessary
+        if (!baseEnvironment.isEmpty()) {
+            context.setBaseEnvironmentProperties(baseEnvironment);
+        }
+
+        try {
+            // handling initializing beans
+            context.afterPropertiesSet();
+            authenticator.afterPropertiesSet();
+        } catch (final Exception e) {
+            throw new SecurityProviderCreationException(e.getMessage(), e);
+        }
+
+        // create the underlying provider
+        ldapAuthenticationProvider = new LdapAuthenticationProvider(authenticator);
+    }
+
+    @Override
+    public AuthenticationResponse authenticate(AuthenticationRequest authenticationRequest) throws InvalidCredentialsException, IdentityAccessException {
+
+        if (authenticationRequest == null || StringUtils.isEmpty(authenticationRequest.getUsername())) {
+            logger.debug("Call to authenticate method with null or empty authenticationRequest, returning null without attempting to authenticate");
+            return null;
+        }
+
+        if (ldapAuthenticationProvider == null) {
+            throw new IdentityAccessException("The LDAP authentication provider is not initialized.");
+        }
+
+        try {
+            final String username = authenticationRequest.getUsername();
+            final Object credentials = authenticationRequest.getCredentials();
+            final String password = credentials != null && credentials instanceof String ? (String) credentials : null;
+
+            // perform the authentication
+            final UsernamePasswordAuthenticationToken token = new UsernamePasswordAuthenticationToken(username, credentials);
+            final Authentication authentication = ldapAuthenticationProvider.authenticate(token);
+            logger.debug("Created authentication token: {}", token.toString());
+
+            // use dn if configured
+            if (IdentityStrategy.USE_DN.equals(identityStrategy)) {
+                // attempt to get the ldap user details to get the DN
+                if (authentication.getPrincipal() instanceof LdapUserDetails) {
+                    final LdapUserDetails userDetails = (LdapUserDetails) authentication.getPrincipal();
+                    return new AuthenticationResponse(userDetails.getDn(), username, expiration, issuer);
+                } else {
+                    logger.warn(String.format("Unable to determine user DN for %s, using username.", authentication.getName()));
+                    return new AuthenticationResponse(authentication.getName(), username, expiration, issuer);
+                }
+            } else {
+                return new AuthenticationResponse(authentication.getName(), username, expiration, issuer);
+            }
+        } catch (final BadCredentialsException | UsernameNotFoundException | AuthenticationException e) {
+            throw new InvalidCredentialsException(e.getMessage(), e);
+        } catch (final Exception e) {
+            // there appears to be a bug that generates a InternalAuthenticationServiceException wrapped around an AuthenticationException. this
+            // shouldn't be the case as they the service exception suggestions that something was wrong with the service. while the authentication
+            // exception suggests that username and/or credentials were incorrect. checking the cause seems to address this scenario.
+            final Throwable cause = e.getCause();
+            if (cause instanceof AuthenticationException) {
+                throw new InvalidCredentialsException(e.getMessage(), e);
+            }
+
+            logger.error(e.getMessage());
+            if (logger.isDebugEnabled()) {
+                logger.debug(StringUtils.EMPTY, e);
+            }
+            throw new IdentityAccessException("Unable to validate the supplied credentials. Please contact the system administrator.", e);
+        }
+    }
+
+    @Override
+    public final void preDestruction() throws SecurityProviderDestructionException {
+    }
+
+    private void setTimeout(final IdentityProviderConfigurationContext configurationContext,
+                            final Map<String, Object> baseEnvironment,
+                            final String configurationProperty,
+                            final String environmentKey) {
+
+        final String rawTimeout = configurationContext.getProperty(configurationProperty);
+        if (StringUtils.isNotBlank(rawTimeout)) {
+            try {
+                final Long timeout = FormatUtils.getTimeDuration(rawTimeout, TimeUnit.MILLISECONDS);
+                baseEnvironment.put(environmentKey, timeout.toString());
+            } catch (final IllegalArgumentException iae) {
+                throw new SecurityProviderCreationException(String.format("The %s '%s' is not a valid time duration", configurationProperty, rawTimeout));
+            }
+        }
+    }
+
+    private SSLContext getConfiguredSslContext(final IdentityProviderConfigurationContext configurationContext) {
+        final String rawKeystore = configurationContext.getProperty("TLS - Keystore");
+        final String rawKeystorePassword = configurationContext.getProperty("TLS - Keystore Password");
+        final String rawKeystoreType = configurationContext.getProperty("TLS - Keystore Type");
+        final String rawTruststore = configurationContext.getProperty("TLS - Truststore");
+        final String rawTruststorePassword = configurationContext.getProperty("TLS - Truststore Password");
+        final String rawTruststoreType = configurationContext.getProperty("TLS - Truststore Type");
+        final String rawClientAuth = configurationContext.getProperty("TLS - Client Auth");
+        final String rawProtocol = configurationContext.getProperty("TLS - Protocol");
+
+        // create the ssl context
+        final SSLContext sslContext;
+        try {
+            if (StringUtils.isBlank(rawKeystore) && StringUtils.isBlank(rawTruststore)) {
+                sslContext = null;
+            } else {
+                // ensure the protocol is specified
+                if (StringUtils.isBlank(rawProtocol)) {
+                    throw new SecurityProviderCreationException("TLS - Protocol must be specified.");
+                }
+
+                if (StringUtils.isBlank(rawKeystore)) {
+                    sslContext = SslContextFactory.createTrustSslContext(rawTruststore, rawTruststorePassword.toCharArray(), rawTruststoreType, rawProtocol);
+                } else if (StringUtils.isBlank(rawTruststore)) {
+                    sslContext = SslContextFactory.createSslContext(rawKeystore, rawKeystorePassword.toCharArray(), rawKeystoreType, rawProtocol);
+                } else {
+                    // determine the client auth if specified
+                    final ClientAuth clientAuth;
+                    if (StringUtils.isBlank(rawClientAuth)) {
+                        clientAuth = ClientAuth.NONE;
+                    } else {
+                        try {
+                            clientAuth = ClientAuth.valueOf(rawClientAuth);
+                        } catch (final IllegalArgumentException iae) {
+                            throw new SecurityProviderCreationException(String.format("Unrecognized client auth '%s'. Possible values are [%s]",
+                                    rawClientAuth, StringUtils.join(ClientAuth.values(), ", ")));
+                        }
+                    }
+
+                    sslContext = SslContextFactory.createSslContext(rawKeystore, rawKeystorePassword.toCharArray(), rawKeystoreType,
+                            rawTruststore, rawTruststorePassword.toCharArray(), rawTruststoreType, clientAuth, rawProtocol);
+                }
+            }
+        } catch (final KeyStoreException | NoSuchAlgorithmException | CertificateException | UnrecoverableKeyException | KeyManagementException | IOException e) {
+            throw new SecurityProviderCreationException(e.getMessage(), e);
+        }
+
+        return sslContext;
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/6f26290d/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/security/ldap/LdapsSocketFactory.java
----------------------------------------------------------------------
diff --git a/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/security/ldap/LdapsSocketFactory.java b/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/security/ldap/LdapsSocketFactory.java
new file mode 100644
index 0000000..dff9572
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/security/ldap/LdapsSocketFactory.java
@@ -0,0 +1,106 @@
+/*
+ * 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.security.ldap;
+
+import javax.net.SocketFactory;
+import javax.net.ssl.SSLSocketFactory;
+import java.io.IOException;
+import java.net.InetAddress;
+import java.net.Socket;
+import java.net.UnknownHostException;
+
+/**
+ * SSLSocketFactory used when connecting to a Directory Server over LDAPS.
+ */
+public class LdapsSocketFactory extends SSLSocketFactory {
+
+    // singleton
+    private static LdapsSocketFactory instance;
+
+    // delegate
+    private SSLSocketFactory delegate;
+
+    /**
+     * Initializes the LdapsSocketFactory with the specified SSLSocketFactory. The specified
+     * socket factory will be used as a delegate for all subsequent instances of this class.
+     *
+     * @param sslSocketFactory delegate socket factory
+     */
+    public static void initialize(final SSLSocketFactory sslSocketFactory) {
+        instance = new LdapsSocketFactory(sslSocketFactory);
+    }
+
+    /**
+     * Gets the LdapsSocketFactory that was previously initialized.
+     *
+      * @return socket factory
+     */
+    public static SocketFactory getDefault() {
+        return instance;
+    }
+
+    /**
+     * Creates a new LdapsSocketFactory.
+     *
+     * @param sslSocketFactory delegate socket factory
+     */
+    private LdapsSocketFactory(final SSLSocketFactory sslSocketFactory) {
+        delegate = sslSocketFactory;
+    }
+
+    // delegate methods
+
+    @Override
+    public String[] getSupportedCipherSuites() {
+        return delegate.getSupportedCipherSuites();
+    }
+
+    @Override
+    public String[] getDefaultCipherSuites() {
+        return delegate.getDefaultCipherSuites();
+    }
+
+    @Override
+    public Socket createSocket(Socket socket, String string, int i, boolean bln) throws IOException {
+        return delegate.createSocket(socket, string, i, bln);
+    }
+
+    @Override
+    public Socket createSocket(InetAddress ia, int i, InetAddress ia1, int i1) throws IOException {
+        return delegate.createSocket(ia, i, ia1, i1);
+    }
+
+    @Override
+    public Socket createSocket(InetAddress ia, int i) throws IOException {
+        return delegate.createSocket(ia, i);
+    }
+
+    @Override
+    public Socket createSocket(String string, int i, InetAddress ia, int i1) throws IOException, UnknownHostException {
+        return delegate.createSocket(string, i, ia, i1);
+    }
+
+    @Override
+    public Socket createSocket(String string, int i) throws IOException, UnknownHostException {
+        return delegate.createSocket(string, i);
+    }
+
+    @Override
+    public Socket createSocket() throws IOException {
+        return delegate.createSocket();
+    }
+}

http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/6f26290d/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/security/ldap/ReferralStrategy.java
----------------------------------------------------------------------
diff --git a/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/security/ldap/ReferralStrategy.java b/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/security/ldap/ReferralStrategy.java
new file mode 100644
index 0000000..4258cde
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/security/ldap/ReferralStrategy.java
@@ -0,0 +1,35 @@
+/*
+ * 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.security.ldap;
+
+public enum ReferralStrategy {
+
+    FOLLOW("follow"),
+    IGNORE("ignore"),
+    THROW("throw");
+
+    private final String value;
+
+    private ReferralStrategy(String value) {
+        this.value = value;
+    }
+
+    public String getValue() {
+        return value;
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/6f26290d/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/security/ldap/tenants/LdapUserGroupProvider.java
----------------------------------------------------------------------
diff --git a/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/security/ldap/tenants/LdapUserGroupProvider.java b/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/security/ldap/tenants/LdapUserGroupProvider.java
new file mode 100644
index 0000000..984c890
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/security/ldap/tenants/LdapUserGroupProvider.java
@@ -0,0 +1,815 @@
+/*
+ * 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.security.ldap.tenants;
+
+import org.apache.commons.lang3.StringUtils;
+import org.apache.nifi.registry.properties.NiFiRegistryProperties;
+import org.apache.nifi.registry.properties.util.IdentityMapping;
+import org.apache.nifi.registry.properties.util.IdentityMappingUtil;
+import org.apache.nifi.registry.security.authorization.AuthorizerConfigurationContext;
+import org.apache.nifi.registry.security.authorization.Group;
+import org.apache.nifi.registry.security.authorization.User;
+import org.apache.nifi.registry.security.authorization.UserAndGroups;
+import org.apache.nifi.registry.security.authorization.UserGroupProvider;
+import org.apache.nifi.registry.security.authorization.UserGroupProviderInitializationContext;
+import org.apache.nifi.registry.security.authorization.annotation.AuthorizerContext;
+import org.apache.nifi.registry.security.authorization.exception.AuthorizationAccessException;
+import org.apache.nifi.registry.security.exception.SecurityProviderCreationException;
+import org.apache.nifi.registry.security.exception.SecurityProviderDestructionException;
+import org.apache.nifi.registry.security.ldap.LdapAuthenticationStrategy;
+import org.apache.nifi.registry.security.ldap.LdapsSocketFactory;
+import org.apache.nifi.registry.security.ldap.ReferralStrategy;
+import org.apache.nifi.registry.security.util.SslContextFactory;
+import org.apache.nifi.registry.security.util.SslContextFactory.ClientAuth;
+import org.apache.nifi.registry.util.FormatUtils;
+import org.apache.nifi.registry.util.PropertyValue;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.ldap.control.PagedResultsDirContextProcessor;
+import org.springframework.ldap.core.ContextSource;
+import org.springframework.ldap.core.DirContextAdapter;
+import org.springframework.ldap.core.DirContextOperations;
+import org.springframework.ldap.core.DirContextProcessor;
+import org.springframework.ldap.core.LdapTemplate;
+import org.springframework.ldap.core.LdapTemplate.NullDirContextProcessor;
+import org.springframework.ldap.core.support.AbstractContextMapper;
+import org.springframework.ldap.core.support.AbstractTlsDirContextAuthenticationStrategy;
+import org.springframework.ldap.core.support.DefaultTlsDirContextAuthenticationStrategy;
+import org.springframework.ldap.core.support.LdapContextSource;
+import org.springframework.ldap.core.support.SimpleDirContextAuthenticationStrategy;
+import org.springframework.ldap.core.support.SingleContextSource;
+import org.springframework.ldap.filter.AndFilter;
+import org.springframework.ldap.filter.EqualsFilter;
+import org.springframework.ldap.filter.HardcodedFilter;
+
+import javax.naming.Context;
+import javax.naming.NamingEnumeration;
+import javax.naming.NamingException;
+import javax.naming.directory.Attribute;
+import javax.naming.directory.SearchControls;
+import javax.net.ssl.SSLContext;
+import java.io.IOException;
+import java.security.KeyManagementException;
+import java.security.KeyStoreException;
+import java.security.NoSuchAlgorithmException;
+import java.security.UnrecoverableKeyException;
+import java.security.cert.CertificateException;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.concurrent.Executors;
+import java.util.concurrent.ScheduledExecutorService;
+import java.util.concurrent.ThreadFactory;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicReference;
+
+/**
+ * Abstract LDAP based implementation of a login identity provider.
+ */
+public class LdapUserGroupProvider implements UserGroupProvider {
+
+    private static final Logger logger = LoggerFactory.getLogger(LdapUserGroupProvider.class);
+
+    public static final String PROP_CONNECT_TIMEOUT = "Connect Timeout";
+    public static final String PROP_READ_TIMEOUT = "Read Timeout";
+    public static final String PROP_AUTHENTICATION_STRATEGY = "Authentication Strategy";
+    public static final String PROP_MANAGER_DN = "Manager DN";
+    public static final String PROP_MANAGER_PASSWORD = "Manager Password";
+    public static final String PROP_REFERRAL_STRATEGY = "Referral Strategy";
+    public static final String PROP_URL = "Url";
+    public static final String PROP_PAGE_SIZE = "Page Size";
+
+    public static final String PROP_USER_SEARCH_BASE = "User Search Base";
+    public static final String PROP_USER_OBJECT_CLASS = "User Object Class";
+    public static final String PROP_USER_SEARCH_SCOPE = "User Search Scope";
+    public static final String PROP_USER_SEARCH_FILTER = "User Search Filter";
+    public static final String PROP_USER_IDENTITY_ATTRIBUTE = "User Identity Attribute";
+    public static final String PROP_USER_GROUP_ATTRIBUTE = "User Group Name Attribute";
+    public static final String PROP_USER_GROUP_REFERENCED_GROUP_ATTRIBUTE = "User Group Name Attribute - Referenced Group Attribute";
+
+    public static final String PROP_GROUP_SEARCH_BASE = "Group Search Base";
+    public static final String PROP_GROUP_OBJECT_CLASS = "Group Object Class";
+    public static final String PROP_GROUP_SEARCH_SCOPE = "Group Search Scope";
+    public static final String PROP_GROUP_SEARCH_FILTER = "Group Search Filter";
+    public static final String PROP_GROUP_NAME_ATTRIBUTE = "Group Name Attribute";
+    public static final String PROP_GROUP_MEMBER_ATTRIBUTE = "Group Member Attribute";
+    public static final String PROP_GROUP_MEMBER_REFERENCED_USER_ATTRIBUTE = "Group Member Attribute - Referenced User Attribute";
+
+
+    public static final String PROP_SYNC_INTERVAL = "Sync Interval";
+
+    private List<IdentityMapping> identityMappings;
+    private NiFiRegistryProperties properties;
+
+    private ScheduledExecutorService ldapSync;
+    private AtomicReference<TenantHolder> tenants = new AtomicReference<>(null);
+
+    private String userSearchBase;
+    private SearchScope userSearchScope;
+    private String userSearchFilter;
+    private String userIdentityAttribute;
+    private String userObjectClass;
+    private String userGroupNameAttribute;
+    private String userGroupReferencedGroupAttribute;
+    private boolean useDnForUserIdentity;
+    private boolean performUserSearch;
+
+    private String groupSearchBase;
+    private SearchScope groupSearchScope;
+    private String groupSearchFilter;
+    private String groupMemberAttribute;
+    private String groupMemberReferencedUserAttribute;
+    private String groupNameAttribute;
+    private String groupObjectClass;
+    private boolean useDnForGroupName;
+    private boolean performGroupSearch;
+
+    private Integer pageSize;
+
+    @Override
+    public void initialize(final UserGroupProviderInitializationContext initializationContext) throws SecurityProviderCreationException {
+        ldapSync = Executors.newSingleThreadScheduledExecutor(new ThreadFactory() {
+            final ThreadFactory factory = Executors.defaultThreadFactory();
+
+            @Override
+            public Thread newThread(Runnable r) {
+                final Thread thread = factory.newThread(r);
+                thread.setName(String.format("%s (%s) - background sync thread", getClass().getSimpleName(), initializationContext.getIdentifier()));
+                return thread;
+            }
+        });
+    }
+
+    @Override
+    public void onConfigured(final AuthorizerConfigurationContext configurationContext) throws SecurityProviderCreationException {
+        final LdapContextSource context = new LdapContextSource();
+
+        final Map<String, Object> baseEnvironment = new HashMap<>();
+
+        // connect/read time out
+        setTimeout(configurationContext, baseEnvironment, PROP_CONNECT_TIMEOUT, "com.sun.jndi.ldap.connect.timeout");
+        setTimeout(configurationContext, baseEnvironment, PROP_READ_TIMEOUT, "com.sun.jndi.ldap.read.timeout");
+
+        // authentication strategy
+        final PropertyValue rawAuthenticationStrategy = configurationContext.getProperty(PROP_AUTHENTICATION_STRATEGY);
+        final LdapAuthenticationStrategy authenticationStrategy;
+        try {
+            authenticationStrategy = LdapAuthenticationStrategy.valueOf(rawAuthenticationStrategy.getValue());
+        } catch (final IllegalArgumentException iae) {
+            throw new SecurityProviderCreationException(String.format("Unrecognized authentication strategy '%s'. Possible values are [%s]",
+                    rawAuthenticationStrategy.getValue(), StringUtils.join(LdapAuthenticationStrategy.values(), ", ")));
+        }
+
+        switch (authenticationStrategy) {
+            case ANONYMOUS:
+                context.setAnonymousReadOnly(true);
+                break;
+            default:
+                final String userDn = configurationContext.getProperty(PROP_MANAGER_DN).getValue();
+                final String password = configurationContext.getProperty(PROP_MANAGER_PASSWORD).getValue();
+
+                context.setUserDn(userDn);
+                context.setPassword(password);
+
+                switch (authenticationStrategy) {
+                    case SIMPLE:
+                        context.setAuthenticationStrategy(new SimpleDirContextAuthenticationStrategy());
+                        break;
+                    case LDAPS:
+                        context.setAuthenticationStrategy(new SimpleDirContextAuthenticationStrategy());
+
+                        // indicate a secure connection
+                        baseEnvironment.put(Context.SECURITY_PROTOCOL, "ssl");
+
+                        // get the configured ssl context
+                        final SSLContext ldapsSslContext = getConfiguredSslContext(configurationContext);
+                        if (ldapsSslContext != null) {
+                            // initialize the ldaps socket factory prior to use
+                            LdapsSocketFactory.initialize(ldapsSslContext.getSocketFactory());
+                            baseEnvironment.put("java.naming.ldap.factory.socket", LdapsSocketFactory.class.getName());
+                        }
+                        break;
+                    case START_TLS:
+                        final AbstractTlsDirContextAuthenticationStrategy tlsAuthenticationStrategy = new DefaultTlsDirContextAuthenticationStrategy();
+
+                        // shutdown gracefully
+                        final String rawShutdownGracefully = configurationContext.getProperty("TLS - Shutdown Gracefully").getValue();
+                        if (StringUtils.isNotBlank(rawShutdownGracefully)) {
+                            final boolean shutdownGracefully = Boolean.TRUE.toString().equalsIgnoreCase(rawShutdownGracefully);
+                            tlsAuthenticationStrategy.setShutdownTlsGracefully(shutdownGracefully);
+                        }
+
+                        // get the configured ssl context
+                        final SSLContext startTlsSslContext = getConfiguredSslContext(configurationContext);
+                        if (startTlsSslContext != null) {
+                            tlsAuthenticationStrategy.setSslSocketFactory(startTlsSslContext.getSocketFactory());
+                        }
+
+                        // set the authentication strategy
+                        context.setAuthenticationStrategy(tlsAuthenticationStrategy);
+                        break;
+                }
+                break;
+        }
+
+        // referrals
+        final String rawReferralStrategy = configurationContext.getProperty(PROP_REFERRAL_STRATEGY).getValue();
+
+        final ReferralStrategy referralStrategy;
+        try {
+            referralStrategy = ReferralStrategy.valueOf(rawReferralStrategy);
+        } catch (final IllegalArgumentException iae) {
+            throw new SecurityProviderCreationException(String.format("Unrecognized referral strategy '%s'. Possible values are [%s]",
+                    rawReferralStrategy, StringUtils.join(ReferralStrategy.values(), ", ")));
+        }
+
+        // using the value as this needs to be the lowercase version while the value is configured with the enum constant
+        context.setReferral(referralStrategy.getValue());
+
+        // url
+        final String urls = configurationContext.getProperty(PROP_URL).getValue();
+
+        if (StringUtils.isBlank(urls)) {
+            throw new SecurityProviderCreationException("LDAP identity provider 'Url' must be specified.");
+        }
+
+        // connection
+        context.setUrls(StringUtils.split(urls));
+
+        // raw user search base
+        final PropertyValue rawUserSearchBase = configurationContext.getProperty(PROP_USER_SEARCH_BASE);
+        final PropertyValue rawUserObjectClass = configurationContext.getProperty(PROP_USER_OBJECT_CLASS);
+        final PropertyValue rawUserSearchScope = configurationContext.getProperty(PROP_USER_SEARCH_SCOPE);
+
+        // if loading the users, ensure the object class set
+        if (rawUserSearchBase.isSet() && !rawUserObjectClass.isSet()) {
+            throw new SecurityProviderCreationException("LDAP user group provider 'User Object Class' must be specified when 'User Search Base' is set.");
+        }
+
+        // if loading the users, ensure the search scope is set
+        if (rawUserSearchBase.isSet() && !rawUserSearchScope.isSet()) {
+            throw new SecurityProviderCreationException("LDAP user group provider 'User Search Scope' must be specified when 'User Search Base' is set.");
+        }
+
+        // user search criteria
+        userSearchBase = rawUserSearchBase.getValue();
+        userObjectClass = rawUserObjectClass.getValue();
+        userSearchFilter = configurationContext.getProperty(PROP_USER_SEARCH_FILTER).getValue();
+        userIdentityAttribute = configurationContext.getProperty(PROP_USER_IDENTITY_ATTRIBUTE).getValue();
+        userGroupNameAttribute = configurationContext.getProperty(PROP_USER_GROUP_ATTRIBUTE).getValue();
+        userGroupReferencedGroupAttribute = configurationContext.getProperty(PROP_USER_GROUP_REFERENCED_GROUP_ATTRIBUTE).getValue();
+
+        try {
+            userSearchScope = SearchScope.valueOf(rawUserSearchScope.getValue());
+        } catch (final IllegalArgumentException iae) {
+            throw new SecurityProviderCreationException(String.format("Unrecognized user search scope '%s'. Possible values are [%s]",
+                    rawUserSearchScope.getValue(), StringUtils.join(SearchScope.values(), ", ")));
+        }
+
+        // determine user behavior
+        useDnForUserIdentity = StringUtils.isBlank(userIdentityAttribute);
+        performUserSearch = StringUtils.isNotBlank(userSearchBase);
+
+        // raw group search criteria
+        final PropertyValue rawGroupSearchBase = configurationContext.getProperty(PROP_GROUP_SEARCH_BASE);
+        final PropertyValue rawGroupObjectClass = configurationContext.getProperty(PROP_GROUP_OBJECT_CLASS);
+        final PropertyValue rawGroupSearchScope = configurationContext.getProperty(PROP_GROUP_SEARCH_SCOPE);
+
+        // if loading the groups, ensure the object class is set
+        if (rawGroupSearchBase.isSet() && !rawGroupObjectClass.isSet()) {
+            throw new SecurityProviderCreationException("LDAP user group provider 'Group Object Class' must be specified when 'Group Search Base' is set.");
+        }
+
+        // if loading the groups, ensure the search scope is set
+        if (rawGroupSearchBase.isSet() && !rawGroupSearchScope.isSet()) {
+            throw new SecurityProviderCreationException("LDAP user group provider 'Group Search Scope' must be specified when 'Group Search Base' is set.");
+        }
+
+        // group search criteria
+        groupSearchBase = rawGroupSearchBase.getValue();
+        groupObjectClass = rawGroupObjectClass.getValue();
+        groupSearchFilter = configurationContext.getProperty(PROP_GROUP_SEARCH_FILTER).getValue();
+        groupNameAttribute = configurationContext.getProperty(PROP_GROUP_NAME_ATTRIBUTE).getValue();
+        groupMemberAttribute = configurationContext.getProperty(PROP_GROUP_MEMBER_ATTRIBUTE).getValue();
+        groupMemberReferencedUserAttribute = configurationContext.getProperty(PROP_GROUP_MEMBER_REFERENCED_USER_ATTRIBUTE).getValue();
+
+        try {
+            groupSearchScope = SearchScope.valueOf(rawGroupSearchScope.getValue());
+        } catch (final IllegalArgumentException iae) {
+            throw new SecurityProviderCreationException(String.format("Unrecognized group search scope '%s'. Possible values are [%s]",
+                    rawGroupSearchScope.getValue(), StringUtils.join(SearchScope.values(), ", ")));
+        }
+
+        // determine group behavior
+        useDnForGroupName = StringUtils.isBlank(groupNameAttribute);
+        performGroupSearch = StringUtils.isNotBlank(groupSearchBase);
+
+        // ensure we are either searching users or groups (at least one must be specified)
+        if (!performUserSearch && !performGroupSearch) {
+            throw new SecurityProviderCreationException("LDAP user group provider 'User Search Base' or 'Group Search Base' must be specified.");
+        }
+
+        // ensure group member attribute is set if searching groups but not users
+        if (performGroupSearch && !performUserSearch && StringUtils.isBlank(groupMemberAttribute)) {
+            throw new SecurityProviderCreationException("'Group Member Attribute' is required when searching groups but not users.");
+        }
+
+        // ensure that performUserSearch is set when groupMemberReferencedUserAttribute is specified
+        if (StringUtils.isNotBlank(groupMemberReferencedUserAttribute) && !performUserSearch) {
+            throw new SecurityProviderCreationException("''User Search Base' must be set when specifying 'Group Member Attribute - Referenced User Attribute'.");
+        }
+
+        // ensure that performGroupSearch is set when userGroupReferencedGroupAttribute is specified
+        if (StringUtils.isNotBlank(userGroupReferencedGroupAttribute) && !performGroupSearch) {
+            throw new SecurityProviderCreationException("'Group Search Base' must be set when specifying 'User Group Name Attribute - Referenced Group Attribute'.");
+        }
+
+        // get the page size if configured
+        final PropertyValue rawPageSize = configurationContext.getProperty(PROP_PAGE_SIZE);
+        if (rawPageSize.isSet() && StringUtils.isNotBlank(rawPageSize.getValue())) {
+            pageSize = rawPageSize.asInteger();
+        }
+
+        // extract the identity mappings from nifi-registry.properties if any are provided
+        identityMappings = Collections.unmodifiableList(IdentityMappingUtil.getIdentityMappings(properties));
+
+        // set the base environment is necessary
+        if (!baseEnvironment.isEmpty()) {
+            context.setBaseEnvironmentProperties(baseEnvironment);
+        }
+
+        try {
+            // handling initializing beans
+            context.afterPropertiesSet();
+        } catch (final Exception e) {
+            throw new SecurityProviderCreationException(e.getMessage(), e);
+        }
+
+        final PropertyValue rawSyncInterval = configurationContext.getProperty(PROP_SYNC_INTERVAL);
+        final long syncInterval;
+        if (rawSyncInterval.isSet()) {
+            try {
+                syncInterval = FormatUtils.getTimeDuration(rawSyncInterval.getValue(), TimeUnit.MILLISECONDS);
+            } catch (final IllegalArgumentException iae) {
+                throw new SecurityProviderCreationException(String.format("The %s '%s' is not a valid time duration", PROP_SYNC_INTERVAL, rawSyncInterval.getValue()));
+            }
+        } else {
+            throw new SecurityProviderCreationException("The 'Sync Interval' must be specified.");
+        }
+
+        try {
+            // perform the initial load, tenants must be loaded as the configured UserGroupProvider is supplied
+            // to the AccessPolicyProvider for granting initial permissions
+            load(context);
+
+            // ensure the tenants were successfully synced
+            if (tenants.get() == null) {
+                throw new SecurityProviderCreationException("Unable to sync users and groups.");
+            }
+
+            // schedule the background thread to load the users/groups
+            ldapSync.scheduleWithFixedDelay(() -> load(context), syncInterval, syncInterval, TimeUnit.MILLISECONDS);
+        } catch (final AuthorizationAccessException e) {
+            throw new SecurityProviderCreationException(e);
+        }
+    }
+
+    @Override
+    public Set<User> getUsers() throws AuthorizationAccessException {
+        return tenants.get().getAllUsers();
+    }
+
+    @Override
+    public User getUser(String identifier) throws AuthorizationAccessException {
+        return tenants.get().getUsersById().get(identifier);
+    }
+
+    @Override
+    public User getUserByIdentity(String identity) throws AuthorizationAccessException {
+        return tenants.get().getUser(identity);
+    }
+
+    @Override
+    public Set<Group> getGroups() throws AuthorizationAccessException {
+        return tenants.get().getAllGroups();
+    }
+
+    @Override
+    public Group getGroup(String identifier) throws AuthorizationAccessException {
+        return tenants.get().getGroupsById().get(identifier);
+    }
+
+    @Override
+    public UserAndGroups getUserAndGroups(String identity) throws AuthorizationAccessException {
+        final TenantHolder holder = tenants.get();
+        return new UserAndGroups() {
+            @Override
+            public User getUser() {
+                return holder.getUser(identity);
+            }
+
+            @Override
+            public Set<Group> getGroups() {
+                return holder.getGroups(identity);
+            }
+        };
+    }
+
+    /**
+     * Reloads the tenants.
+     */
+    private void load(final ContextSource contextSource) {
+        // create the ldapTemplate based on the context source. use a single source context to use the same connection
+        // to support paging when configured
+        final SingleContextSource singleContextSource = new SingleContextSource(contextSource.getReadOnlyContext());
+        final LdapTemplate ldapTemplate = new LdapTemplate(singleContextSource);
+
+        try {
+            final List<User> userList = new ArrayList<>();
+            final List<Group> groupList = new ArrayList<>();
+
+            // group dn -> user identifiers lookup
+            final Map<String, Set<String>> groupToUserIdentifierMappings = new HashMap<>();
+
+            // user dn -> user lookup
+            final Map<String, User> userLookup = new HashMap<>();
+
+            if (performUserSearch) {
+                // search controls
+                final SearchControls userControls = new SearchControls();
+                userControls.setSearchScope(userSearchScope.ordinal());
+
+                // consider paging support for users
+                final DirContextProcessor userProcessor;
+                if (pageSize == null) {
+                    userProcessor = new NullDirContextProcessor();
+                } else {
+                    userProcessor = new PagedResultsDirContextProcessor(pageSize);
+                }
+
+                // looking for objects matching the user object class
+                final AndFilter userFilter = new AndFilter();
+                userFilter.and(new EqualsFilter("objectClass", userObjectClass));
+
+                // if a filter has been provided by the user, we add it to the filter
+                if (StringUtils.isNotBlank(userSearchFilter)) {
+                    userFilter.and(new HardcodedFilter(userSearchFilter));
+                }
+
+                do {
+                    userList.addAll(ldapTemplate.search(userSearchBase, userFilter.encode(), userControls, new AbstractContextMapper<User>() {
+                        @Override
+                        protected User doMapFromContext(DirContextOperations ctx) {
+                            // get the user identity
+                            final String identity = getUserIdentity(ctx);
+
+                            // build the user
+                            final User user = new User.Builder().identifierGenerateFromSeed(identity).identity(identity).build();
+
+                            // store the user for group member later
+                            userLookup.put(getReferencedUserValue(ctx), user);
+
+                            if (StringUtils.isNotBlank(userGroupNameAttribute)) {
+                                final Attribute attributeGroups = ctx.getAttributes().get(userGroupNameAttribute);
+
+                                if (attributeGroups == null) {
+                                    logger.warn("User group name attribute [" + userGroupNameAttribute + "] does not exist. Ignoring group membership.");
+                                } else {
+                                    try {
+                                        final NamingEnumeration<String> groupValues = (NamingEnumeration<String>) attributeGroups.getAll();
+                                        while (groupValues.hasMoreElements()) {
+                                            // store the group -> user identifier mapping
+                                            groupToUserIdentifierMappings.computeIfAbsent(groupValues.next(), g -> new HashSet<>()).add(user.getIdentifier());
+                                        }
+                                    } catch (NamingException e) {
+                                        throw new AuthorizationAccessException("Error while retrieving user group name attribute [" + userIdentityAttribute + "].");
+                                    }
+                                }
+                            }
+
+                            return user;
+                        }
+                    }, userProcessor));
+                } while (hasMorePages(userProcessor));
+            }
+
+            if (performGroupSearch) {
+                final SearchControls groupControls = new SearchControls();
+                groupControls.setSearchScope(groupSearchScope.ordinal());
+
+                // consider paging support for groups
+                final DirContextProcessor groupProcessor;
+                if (pageSize == null) {
+                    groupProcessor = new NullDirContextProcessor();
+                } else {
+                    groupProcessor = new PagedResultsDirContextProcessor(pageSize);
+                }
+
+                // looking for objects matching the group object class
+                AndFilter groupFilter = new AndFilter();
+                groupFilter.and(new EqualsFilter("objectClass", groupObjectClass));
+
+                // if a filter has been provided by the user, we add it to the filter
+                if(StringUtils.isNotBlank(groupSearchFilter)) {
+                    groupFilter.and(new HardcodedFilter(groupSearchFilter));
+                }
+
+                do {
+                    groupList.addAll(ldapTemplate.search(groupSearchBase, groupFilter.encode(), groupControls, new AbstractContextMapper<Group>() {
+                        @Override
+                        protected Group doMapFromContext(DirContextOperations ctx) {
+                            final String dn = ctx.getDn().toString();
+
+                            // get the group identity
+                            final String name = getGroupName(ctx);
+
+                            // get the value of this group that may associate it to users
+                            final String referencedGroupValue = getReferencedGroupValue(ctx);
+
+                            if (!StringUtils.isBlank(groupMemberAttribute)) {
+                                Attribute attributeUsers = ctx.getAttributes().get(groupMemberAttribute);
+                                if (attributeUsers == null) {
+                                    logger.warn("Group member attribute [" + groupMemberAttribute + "] does not exist. Ignoring group membership.");
+                                } else {
+                                    try {
+                                        final NamingEnumeration<String> userValues = (NamingEnumeration<String>) attributeUsers.getAll();
+                                        while (userValues.hasMoreElements()) {
+                                            final String userValue = userValues.next();
+
+                                            if (performUserSearch) {
+                                                // find the user by it's referenced attribute and add the identifier to this group
+                                                final User user = userLookup.get(userValue);
+
+                                                // ensure the user is known
+                                                if (user != null) {
+                                                    groupToUserIdentifierMappings.computeIfAbsent(referencedGroupValue, g -> new HashSet<>()).add(user.getIdentifier());
+                                                } else {
+                                                    logger.warn(String.format("%s contains member %s but that user was not found while searching users. Ignoring group membership.", name, userValue));
+                                                }
+                                            } else {
+                                                // since performUserSearch is false, then the referenced group attribute must be blank... the user value must be the dn
+                                                final String userDn = userValue;
+
+                                                final String userIdentity;
+                                                if (useDnForUserIdentity) {
+                                                    // use the user value to avoid the unnecessary look up
+                                                    userIdentity = userDn;
+                                                } else {
+                                                    // lookup the user to extract the user identity
+                                                    userIdentity = getUserIdentity((DirContextAdapter) ldapTemplate.lookup(userDn));
+                                                }
+
+                                                // build the user
+                                                final User user = new User.Builder().identifierGenerateFromSeed(userIdentity).identity(userIdentity).build();
+
+                                                // add this user
+                                                userList.add(user);
+                                                groupToUserIdentifierMappings.computeIfAbsent(referencedGroupValue, g -> new HashSet<>()).add(user.getIdentifier());
+                                            }
+                                        }
+                                    } catch (NamingException e) {
+                                        throw new AuthorizationAccessException("Error while retrieving group name attribute [" + groupNameAttribute + "].");
+                                    }
+                                }
+                            }
+
+                            // build this group
+                            final Group.Builder groupBuilder = new Group.Builder().identifierGenerateFromSeed(name).name(name);
+
+                            // add all users that were associated with this referenced group attribute
+                            if (groupToUserIdentifierMappings.containsKey(referencedGroupValue)) {
+                                groupToUserIdentifierMappings.remove(referencedGroupValue).forEach(userIdentifier -> groupBuilder.addUser(userIdentifier));
+                            }
+
+                            return groupBuilder.build();
+                        }
+                    }, groupProcessor));
+                } while (hasMorePages(groupProcessor));
+
+                // any remaining groupDn's were referenced by a user but not found while searching groups
+                groupToUserIdentifierMappings.forEach((referencedGroupValue, userIdentifiers) -> {
+                    logger.warn(String.format("[%s] are members of %s but that group was not found while searching users. Ignoring group membership.",
+                            StringUtils.join(userIdentifiers, ", "), referencedGroupValue));
+                });
+            } else {
+                // since performGroupSearch is false, then the referenced user attribute must be blank... the group value must be the dn
+
+                // groups are not being searched so lookup any groups identified while searching users
+                groupToUserIdentifierMappings.forEach((groupDn, userIdentifiers) -> {
+                    final String groupName;
+                    if (useDnForGroupName) {
+                        // use the dn to avoid the unnecessary look up
+                        groupName = groupDn;
+                    } else {
+                        groupName = getGroupName((DirContextAdapter) ldapTemplate.lookup(groupDn));
+                    }
+
+                    // define the group
+                    final Group.Builder groupBuilder = new Group.Builder().identifierGenerateFromSeed(groupName).name(groupName);
+
+                    // add each user
+                    userIdentifiers.forEach(userIdentifier -> groupBuilder.addUser(userIdentifier));
+
+                    // build the group
+                    groupList.add(groupBuilder.build());
+                });
+            }
+
+            // record the updated tenants
+            tenants.set(new TenantHolder(new HashSet<>(userList), new HashSet<>(groupList)));
+        } finally {
+            singleContextSource.destroy();
+        }
+    }
+
+    private boolean hasMorePages(final DirContextProcessor processor ) {
+        return processor instanceof PagedResultsDirContextProcessor && ((PagedResultsDirContextProcessor) processor).hasMore();
+    }
+
+    private String getUserIdentity(final DirContextOperations ctx) {
+        final String identity;
+
+        if (useDnForUserIdentity) {
+            identity = ctx.getDn().toString();
+        } else {
+            final Attribute attributeName = ctx.getAttributes().get(userIdentityAttribute);
+            if (attributeName == null) {
+                throw new AuthorizationAccessException("User identity attribute [" + userIdentityAttribute + "] does not exist.");
+            }
+
+            try {
+                identity = (String) attributeName.get();
+            } catch (NamingException e) {
+                throw new AuthorizationAccessException("Error while retrieving user name attribute [" + userIdentityAttribute + "].");
+            }
+        }
+
+        return IdentityMappingUtil.mapIdentity(identity, identityMappings);
+    }
+
+    private String getReferencedUserValue(final DirContextOperations ctx) {
+        final String referencedUserValue;
+
+        if (StringUtils.isBlank(groupMemberReferencedUserAttribute)) {
+            referencedUserValue = ctx.getDn().toString();
+        } else {
+            final Attribute attributeName = ctx.getAttributes().get(groupMemberReferencedUserAttribute);
+            if (attributeName == null) {
+                throw new AuthorizationAccessException("Referenced user value attribute [" + groupMemberReferencedUserAttribute + "] does not exist.");
+            }
+
+            try {
+                referencedUserValue = (String) attributeName.get();
+            } catch (NamingException e) {
+                throw new AuthorizationAccessException("Error while retrieving reference user value attribute [" + groupMemberReferencedUserAttribute + "].");
+            }
+        }
+
+        return referencedUserValue;
+    }
+
+    private String getGroupName(final DirContextOperations ctx) {
+        final String name;
+
+        if (useDnForGroupName) {
+            name = ctx.getDn().toString();
+        } else {
+            final Attribute attributeName = ctx.getAttributes().get(groupNameAttribute);
+            if (attributeName == null) {
+                throw new AuthorizationAccessException("Group identity attribute [" + groupNameAttribute + "] does not exist.");
+            }
+
+            try {
+                name = (String) attributeName.get();
+            } catch (NamingException e) {
+                throw new AuthorizationAccessException("Error while retrieving group name attribute [" + groupNameAttribute + "].");
+            }
+        }
+
+        return name;
+    }
+
+    private String getReferencedGroupValue(final DirContextOperations ctx) {
+        final String referencedGroupValue;
+
+        if (StringUtils.isBlank(userGroupReferencedGroupAttribute)) {
+            referencedGroupValue = ctx.getDn().toString();
+        } else {
+            final Attribute attributeName = ctx.getAttributes().get(userGroupReferencedGroupAttribute);
+            if (attributeName == null) {
+                throw new AuthorizationAccessException("Referenced group value attribute [" + userGroupReferencedGroupAttribute + "] does not exist.");
+            }
+
+            try {
+                referencedGroupValue = (String) attributeName.get();
+            } catch (NamingException e) {
+                throw new AuthorizationAccessException("Error while retrieving referenced group value attribute [" + userGroupReferencedGroupAttribute + "].");
+            }
+        }
+
+        return referencedGroupValue;
+    }
+
+    @AuthorizerContext
+    public void setNiFiProperties(NiFiRegistryProperties properties) {
+        this.properties = properties;
+    }
+
+    @Override
+    public final void preDestruction() throws SecurityProviderDestructionException {
+        ldapSync.shutdown();
+        try {
+            if (!ldapSync.awaitTermination(10000, TimeUnit.MILLISECONDS)) {
+                logger.info("Failed to stop ldap sync thread in 10 sec. Terminating");
+                ldapSync.shutdownNow();
+            }
+        } catch (InterruptedException e) {
+            Thread.currentThread().interrupt();
+        }
+    }
+
+    private void setTimeout(final AuthorizerConfigurationContext configurationContext,
+                            final Map<String, Object> baseEnvironment,
+                            final String configurationProperty,
+                            final String environmentKey) {
+
+        final PropertyValue rawTimeout = configurationContext.getProperty(configurationProperty);
+        if (rawTimeout.isSet()) {
+            try {
+                final Long timeout = FormatUtils.getTimeDuration(rawTimeout.getValue(), TimeUnit.MILLISECONDS);
+                baseEnvironment.put(environmentKey, timeout.toString());
+            } catch (final IllegalArgumentException iae) {
+                throw new SecurityProviderCreationException(String.format("The %s '%s' is not a valid time duration", configurationProperty, rawTimeout));
+            }
+        }
+    }
+
+    private SSLContext getConfiguredSslContext(final AuthorizerConfigurationContext configurationContext) {
+        final String rawKeystore = configurationContext.getProperty("TLS - Keystore").getValue();
+        final String rawKeystorePassword = configurationContext.getProperty("TLS - Keystore Password").getValue();
+        final String rawKeystoreType = configurationContext.getProperty("TLS - Keystore Type").getValue();
+        final String rawTruststore = configurationContext.getProperty("TLS - Truststore").getValue();
+        final String rawTruststorePassword = configurationContext.getProperty("TLS - Truststore Password").getValue();
+        final String rawTruststoreType = configurationContext.getProperty("TLS - Truststore Type").getValue();
+        final String rawClientAuth = configurationContext.getProperty("TLS - Client Auth").getValue();
+        final String rawProtocol = configurationContext.getProperty("TLS - Protocol").getValue();
+
+        // create the ssl context
+        final SSLContext sslContext;
+        try {
+            if (StringUtils.isBlank(rawKeystore) && StringUtils.isBlank(rawTruststore)) {
+                sslContext = null;
+            } else {
+                // ensure the protocol is specified
+                if (StringUtils.isBlank(rawProtocol)) {
+                    throw new SecurityProviderCreationException("TLS - Protocol must be specified.");
+                }
+
+                if (StringUtils.isBlank(rawKeystore)) {
+                    sslContext = SslContextFactory.createTrustSslContext(rawTruststore, rawTruststorePassword.toCharArray(), rawTruststoreType, rawProtocol);
+                } else if (StringUtils.isBlank(rawTruststore)) {
+                    sslContext = SslContextFactory.createSslContext(rawKeystore, rawKeystorePassword.toCharArray(), rawKeystoreType, rawProtocol);
+                } else {
+                    // determine the client auth if specified
+                    final ClientAuth clientAuth;
+                    if (StringUtils.isBlank(rawClientAuth)) {
+                        clientAuth = ClientAuth.NONE;
+                    } else {
+                        try {
+                            clientAuth = ClientAuth.valueOf(rawClientAuth);
+                        } catch (final IllegalArgumentException iae) {
+                            throw new SecurityProviderCreationException(String.format("Unrecognized client auth '%s'. Possible values are [%s]",
+                                    rawClientAuth, StringUtils.join(ClientAuth.values(), ", ")));
+                        }
+                    }
+
+                    sslContext = SslContextFactory.createSslContext(rawKeystore, rawKeystorePassword.toCharArray(), rawKeystoreType,
+                            rawTruststore, rawTruststorePassword.toCharArray(), rawTruststoreType, clientAuth, rawProtocol);
+                }
+            }
+        } catch (final KeyStoreException | NoSuchAlgorithmException | CertificateException | UnrecoverableKeyException | KeyManagementException | IOException e) {
+            throw new SecurityProviderCreationException(e.getMessage(), e);
+        }
+
+        return sslContext;
+    }
+
+}


[28/51] [partial] nifi-registry git commit: NIFIREG-201 Refactoring project structure to better isolate extensions

Posted by kd...@apache.org.
http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/6f26290d/nifi-registry-core/nifi-registry-framework/src/test/java/org/apache/nifi/registry/security/ldap/tenants/LdapUserGroupProviderTest.java
----------------------------------------------------------------------
diff --git a/nifi-registry-core/nifi-registry-framework/src/test/java/org/apache/nifi/registry/security/ldap/tenants/LdapUserGroupProviderTest.java b/nifi-registry-core/nifi-registry-framework/src/test/java/org/apache/nifi/registry/security/ldap/tenants/LdapUserGroupProviderTest.java
new file mode 100644
index 0000000..242cf28
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-framework/src/test/java/org/apache/nifi/registry/security/ldap/tenants/LdapUserGroupProviderTest.java
@@ -0,0 +1,639 @@
+/*
+ * 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.security.ldap.tenants;
+
+import org.apache.directory.server.annotations.CreateLdapServer;
+import org.apache.directory.server.annotations.CreateTransport;
+import org.apache.directory.server.core.annotations.ApplyLdifFiles;
+import org.apache.directory.server.core.annotations.CreateDS;
+import org.apache.directory.server.core.annotations.CreatePartition;
+import org.apache.directory.server.core.integ.AbstractLdapTestUnit;
+import org.apache.directory.server.core.integ.FrameworkRunner;
+import org.apache.nifi.registry.properties.NiFiRegistryProperties;
+import org.apache.nifi.registry.security.authorization.AuthorizerConfigurationContext;
+import org.apache.nifi.registry.security.authorization.Group;
+import org.apache.nifi.registry.security.authorization.UserAndGroups;
+import org.apache.nifi.registry.security.authorization.UserGroupProviderInitializationContext;
+import org.apache.nifi.registry.security.exception.SecurityProviderCreationException;
+import org.apache.nifi.registry.security.ldap.LdapAuthenticationStrategy;
+import org.apache.nifi.registry.security.ldap.ReferralStrategy;
+import org.apache.nifi.registry.util.StandardPropertyValue;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mockito;
+
+import java.util.Properties;
+import java.util.Set;
+
+import static org.apache.nifi.registry.security.ldap.tenants.LdapUserGroupProvider.PROP_AUTHENTICATION_STRATEGY;
+import static org.apache.nifi.registry.security.ldap.tenants.LdapUserGroupProvider.PROP_CONNECT_TIMEOUT;
+import static org.apache.nifi.registry.security.ldap.tenants.LdapUserGroupProvider.PROP_GROUP_MEMBER_ATTRIBUTE;
+import static org.apache.nifi.registry.security.ldap.tenants.LdapUserGroupProvider.PROP_GROUP_MEMBER_REFERENCED_USER_ATTRIBUTE;
+import static org.apache.nifi.registry.security.ldap.tenants.LdapUserGroupProvider.PROP_GROUP_NAME_ATTRIBUTE;
+import static org.apache.nifi.registry.security.ldap.tenants.LdapUserGroupProvider.PROP_GROUP_OBJECT_CLASS;
+import static org.apache.nifi.registry.security.ldap.tenants.LdapUserGroupProvider.PROP_GROUP_SEARCH_BASE;
+import static org.apache.nifi.registry.security.ldap.tenants.LdapUserGroupProvider.PROP_GROUP_SEARCH_FILTER;
+import static org.apache.nifi.registry.security.ldap.tenants.LdapUserGroupProvider.PROP_GROUP_SEARCH_SCOPE;
+import static org.apache.nifi.registry.security.ldap.tenants.LdapUserGroupProvider.PROP_MANAGER_DN;
+import static org.apache.nifi.registry.security.ldap.tenants.LdapUserGroupProvider.PROP_MANAGER_PASSWORD;
+import static org.apache.nifi.registry.security.ldap.tenants.LdapUserGroupProvider.PROP_PAGE_SIZE;
+import static org.apache.nifi.registry.security.ldap.tenants.LdapUserGroupProvider.PROP_READ_TIMEOUT;
+import static org.apache.nifi.registry.security.ldap.tenants.LdapUserGroupProvider.PROP_REFERRAL_STRATEGY;
+import static org.apache.nifi.registry.security.ldap.tenants.LdapUserGroupProvider.PROP_SYNC_INTERVAL;
+import static org.apache.nifi.registry.security.ldap.tenants.LdapUserGroupProvider.PROP_URL;
+import static org.apache.nifi.registry.security.ldap.tenants.LdapUserGroupProvider.PROP_USER_GROUP_ATTRIBUTE;
+import static org.apache.nifi.registry.security.ldap.tenants.LdapUserGroupProvider.PROP_USER_GROUP_REFERENCED_GROUP_ATTRIBUTE;
+import static org.apache.nifi.registry.security.ldap.tenants.LdapUserGroupProvider.PROP_USER_IDENTITY_ATTRIBUTE;
+import static org.apache.nifi.registry.security.ldap.tenants.LdapUserGroupProvider.PROP_USER_OBJECT_CLASS;
+import static org.apache.nifi.registry.security.ldap.tenants.LdapUserGroupProvider.PROP_USER_SEARCH_BASE;
+import static org.apache.nifi.registry.security.ldap.tenants.LdapUserGroupProvider.PROP_USER_SEARCH_FILTER;
+import static org.apache.nifi.registry.security.ldap.tenants.LdapUserGroupProvider.PROP_USER_SEARCH_SCOPE;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertTrue;
+import static org.mockito.Matchers.anyString;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+@RunWith(FrameworkRunner.class)
+@CreateLdapServer(transports = {@CreateTransport(protocol = "LDAP")})
+@CreateDS(name = "nifi-example", partitions = {@CreatePartition(name = "example", suffix = "o=nifi")})
+@ApplyLdifFiles("nifi-example.ldif")
+public class LdapUserGroupProviderTest extends AbstractLdapTestUnit {
+
+    private static final String USER_SEARCH_BASE = "ou=users,o=nifi";
+    private static final String GROUP_SEARCH_BASE = "ou=groups,o=nifi";
+
+    private LdapUserGroupProvider ldapUserGroupProvider;
+
+    @Before
+    public void setup() {
+        final UserGroupProviderInitializationContext initializationContext = mock(UserGroupProviderInitializationContext.class);
+        when(initializationContext.getIdentifier()).thenReturn("identifier");
+
+        ldapUserGroupProvider = new LdapUserGroupProvider();
+        ldapUserGroupProvider.setNiFiProperties(getNiFiProperties(new Properties()));
+        ldapUserGroupProvider.initialize(initializationContext);
+    }
+
+    @Test(expected = SecurityProviderCreationException.class)
+    public void testNoSearchBasesSpecified() throws Exception {
+        final AuthorizerConfigurationContext configurationContext = getBaseConfiguration(null, null);
+        ldapUserGroupProvider.onConfigured(configurationContext);
+    }
+
+    @Test(expected = SecurityProviderCreationException.class)
+    public void testUserSearchBaseSpecifiedButNoUserObjectClass() throws Exception {
+        final AuthorizerConfigurationContext configurationContext = getBaseConfiguration(USER_SEARCH_BASE, null);
+        when(configurationContext.getProperty(PROP_USER_OBJECT_CLASS)).thenReturn(new StandardPropertyValue(null));
+        ldapUserGroupProvider.onConfigured(configurationContext);
+    }
+
+    @Test(expected = SecurityProviderCreationException.class)
+    public void testUserSearchBaseSpecifiedButNoUserSearchScope() throws Exception {
+        final AuthorizerConfigurationContext configurationContext = getBaseConfiguration(USER_SEARCH_BASE, null);
+        when(configurationContext.getProperty(PROP_USER_SEARCH_SCOPE)).thenReturn(new StandardPropertyValue(null));
+        ldapUserGroupProvider.onConfigured(configurationContext);
+    }
+
+    @Test(expected = SecurityProviderCreationException.class)
+    public void testInvalidUserSearchScope() throws Exception {
+        final AuthorizerConfigurationContext configurationContext = getBaseConfiguration(USER_SEARCH_BASE, null);
+        when(configurationContext.getProperty(PROP_USER_SEARCH_SCOPE)).thenReturn(new StandardPropertyValue("not-valid"));
+        ldapUserGroupProvider.onConfigured(configurationContext);
+    }
+
+    @Test
+    public void testSearchUsersWithNoIdentityAttribute() throws Exception {
+        final AuthorizerConfigurationContext configurationContext = getBaseConfiguration(USER_SEARCH_BASE, null);
+        ldapUserGroupProvider.onConfigured(configurationContext);
+
+        assertEquals(8, ldapUserGroupProvider.getUsers().size());
+        assertNotNull(ldapUserGroupProvider.getUserByIdentity("cn=User 1,ou=users,o=nifi"));
+        assertTrue(ldapUserGroupProvider.getGroups().isEmpty());
+    }
+
+    @Test
+    public void testSearchUsersWithUidIdentityAttribute() throws Exception {
+        final AuthorizerConfigurationContext configurationContext = getBaseConfiguration(USER_SEARCH_BASE, null);
+        when(configurationContext.getProperty(PROP_USER_IDENTITY_ATTRIBUTE)).thenReturn(new StandardPropertyValue("uid"));
+        ldapUserGroupProvider.onConfigured(configurationContext);
+
+        assertEquals(8, ldapUserGroupProvider.getUsers().size());
+        assertNotNull(ldapUserGroupProvider.getUserByIdentity("user1"));
+        assertTrue(ldapUserGroupProvider.getGroups().isEmpty());
+    }
+
+    @Test
+    public void testSearchUsersWithCnIdentityAttribute() throws Exception {
+        final AuthorizerConfigurationContext configurationContext = getBaseConfiguration(USER_SEARCH_BASE, null);
+        when(configurationContext.getProperty(PROP_USER_IDENTITY_ATTRIBUTE)).thenReturn(new StandardPropertyValue("cn"));
+        ldapUserGroupProvider.onConfigured(configurationContext);
+
+        assertEquals(8, ldapUserGroupProvider.getUsers().size());
+        assertNotNull(ldapUserGroupProvider.getUserByIdentity("User 1"));
+        assertTrue(ldapUserGroupProvider.getGroups().isEmpty());
+    }
+
+    @Test
+    public void testSearchUsersObjectSearchScope() throws Exception {
+        final AuthorizerConfigurationContext configurationContext = getBaseConfiguration(USER_SEARCH_BASE, null);
+        when(configurationContext.getProperty(PROP_USER_SEARCH_SCOPE)).thenReturn(new StandardPropertyValue(SearchScope.OBJECT.name()));
+        ldapUserGroupProvider.onConfigured(configurationContext);
+
+        assertTrue(ldapUserGroupProvider.getUsers().isEmpty());
+        assertTrue(ldapUserGroupProvider.getGroups().isEmpty());
+    }
+
+    @Test
+    public void testSearchUsersSubtreeSearchScope() throws Exception {
+        final AuthorizerConfigurationContext configurationContext = getBaseConfiguration("o=nifi", null);
+        when(configurationContext.getProperty(PROP_USER_SEARCH_SCOPE)).thenReturn(new StandardPropertyValue(SearchScope.SUBTREE.name()));
+        ldapUserGroupProvider.onConfigured(configurationContext);
+
+        assertEquals(9, ldapUserGroupProvider.getUsers().size());
+        assertTrue(ldapUserGroupProvider.getGroups().isEmpty());
+    }
+
+    @Test
+    public void testSearchUsersWithFilter() throws Exception {
+        final AuthorizerConfigurationContext configurationContext = getBaseConfiguration(USER_SEARCH_BASE, null);
+        when(configurationContext.getProperty(PROP_USER_IDENTITY_ATTRIBUTE)).thenReturn(new StandardPropertyValue("uid"));
+        when(configurationContext.getProperty(PROP_USER_SEARCH_FILTER)).thenReturn(new StandardPropertyValue("(uid=user1)"));
+        ldapUserGroupProvider.onConfigured(configurationContext);
+
+        assertEquals(1, ldapUserGroupProvider.getUsers().size());
+        assertNotNull(ldapUserGroupProvider.getUserByIdentity("user1"));
+        assertTrue(ldapUserGroupProvider.getGroups().isEmpty());
+    }
+
+    @Test
+    public void testSearchUsersWithPaging() throws Exception {
+        final AuthorizerConfigurationContext configurationContext = getBaseConfiguration(USER_SEARCH_BASE, null);
+        when(configurationContext.getProperty(PROP_PAGE_SIZE)).thenReturn(new StandardPropertyValue("1"));
+        ldapUserGroupProvider.onConfigured(configurationContext);
+
+        assertEquals(8, ldapUserGroupProvider.getUsers().size());
+        assertTrue(ldapUserGroupProvider.getGroups().isEmpty());
+    }
+
+    @Test
+    public void testSearchUsersWithGroupingNoGroupName() throws Exception {
+        final AuthorizerConfigurationContext configurationContext = getBaseConfiguration(USER_SEARCH_BASE, null);
+        when(configurationContext.getProperty(PROP_USER_IDENTITY_ATTRIBUTE)).thenReturn(new StandardPropertyValue("uid"));
+        when(configurationContext.getProperty(PROP_USER_GROUP_ATTRIBUTE)).thenReturn(new StandardPropertyValue("description")); // using description in lieu of memberof
+        ldapUserGroupProvider.onConfigured(configurationContext);
+
+        assertEquals(8, ldapUserGroupProvider.getUsers().size());
+        assertEquals(2, ldapUserGroupProvider.getGroups().size());
+
+        final UserAndGroups userAndGroups = ldapUserGroupProvider.getUserAndGroups("user4");
+        assertNotNull(userAndGroups.getUser());
+        assertEquals(1, userAndGroups.getGroups().size());
+        assertEquals("cn=team1,ou=groups,o=nifi", userAndGroups.getGroups().iterator().next().getName());
+    }
+
+    @Test
+    public void testSearchUsersWithGroupingAndGroupName() throws Exception {
+        final AuthorizerConfigurationContext configurationContext = getBaseConfiguration(USER_SEARCH_BASE, null);
+        when(configurationContext.getProperty(PROP_USER_IDENTITY_ATTRIBUTE)).thenReturn(new StandardPropertyValue("uid"));
+        when(configurationContext.getProperty(PROP_USER_GROUP_ATTRIBUTE)).thenReturn(new StandardPropertyValue("description")); // using description in lieu of memberof
+        when(configurationContext.getProperty(PROP_GROUP_NAME_ATTRIBUTE)).thenReturn(new StandardPropertyValue("cn"));
+        ldapUserGroupProvider.onConfigured(configurationContext);
+
+        assertEquals(8, ldapUserGroupProvider.getUsers().size());
+        assertEquals(2, ldapUserGroupProvider.getGroups().size());
+
+        final UserAndGroups userAndGroups = ldapUserGroupProvider.getUserAndGroups("user4");
+        assertNotNull(userAndGroups.getUser());
+        assertEquals(1, userAndGroups.getGroups().size());
+        assertEquals("team1", userAndGroups.getGroups().iterator().next().getName());
+    }
+
+    @Test(expected = SecurityProviderCreationException.class)
+    public void testSearchGroupsWithoutMemberAttribute() throws Exception {
+        final AuthorizerConfigurationContext configurationContext = getBaseConfiguration(null, GROUP_SEARCH_BASE);
+        ldapUserGroupProvider.onConfigured(configurationContext);
+    }
+
+    @Test(expected = SecurityProviderCreationException.class)
+    public void testGroupSearchBaseSpecifiedButNoGroupObjectClass() throws Exception {
+        final AuthorizerConfigurationContext configurationContext = getBaseConfiguration(null, GROUP_SEARCH_BASE);
+        when(configurationContext.getProperty(PROP_GROUP_MEMBER_ATTRIBUTE)).thenReturn(new StandardPropertyValue("member"));
+        when(configurationContext.getProperty(PROP_GROUP_OBJECT_CLASS)).thenReturn(new StandardPropertyValue(null));
+        ldapUserGroupProvider.onConfigured(configurationContext);
+    }
+
+    @Test(expected = SecurityProviderCreationException.class)
+    public void testUserSearchBaseSpecifiedButNoGroupSearchScope() throws Exception {
+        final AuthorizerConfigurationContext configurationContext = getBaseConfiguration(null, GROUP_SEARCH_BASE);
+        when(configurationContext.getProperty(PROP_GROUP_MEMBER_ATTRIBUTE)).thenReturn(new StandardPropertyValue("member"));
+        when(configurationContext.getProperty(PROP_GROUP_SEARCH_SCOPE)).thenReturn(new StandardPropertyValue(null));
+        ldapUserGroupProvider.onConfigured(configurationContext);
+    }
+
+    @Test(expected = SecurityProviderCreationException.class)
+    public void testInvalidGroupSearchScope() throws Exception {
+        final AuthorizerConfigurationContext configurationContext = getBaseConfiguration(null, GROUP_SEARCH_BASE);
+        when(configurationContext.getProperty(PROP_GROUP_MEMBER_ATTRIBUTE)).thenReturn(new StandardPropertyValue("member"));
+        when(configurationContext.getProperty(PROP_GROUP_SEARCH_SCOPE)).thenReturn(new StandardPropertyValue("not-valid"));
+        ldapUserGroupProvider.onConfigured(configurationContext);
+    }
+
+    @Test
+    public void testSearchGroupsWithNoNameAttribute() throws Exception {
+        final AuthorizerConfigurationContext configurationContext = getBaseConfiguration(null, GROUP_SEARCH_BASE);
+        when(configurationContext.getProperty(PROP_GROUP_MEMBER_ATTRIBUTE)).thenReturn(new StandardPropertyValue("member"));
+        ldapUserGroupProvider.onConfigured(configurationContext);
+
+        final Set<Group> groups = ldapUserGroupProvider.getGroups();
+        assertEquals(4, groups.size());
+        assertEquals(1, groups.stream().filter(group -> "cn=admins,ou=groups,o=nifi".equals(group.getName())).count());
+    }
+
+    @Test
+    public void testSearchGroupsWithPaging() throws Exception {
+        final AuthorizerConfigurationContext configurationContext = getBaseConfiguration(null, GROUP_SEARCH_BASE);
+        when(configurationContext.getProperty(PROP_GROUP_MEMBER_ATTRIBUTE)).thenReturn(new StandardPropertyValue("member"));
+        when(configurationContext.getProperty(PROP_PAGE_SIZE)).thenReturn(new StandardPropertyValue("1"));
+        ldapUserGroupProvider.onConfigured(configurationContext);
+
+        assertEquals(4, ldapUserGroupProvider.getGroups().size());
+    }
+
+    @Test
+    public void testSearchGroupsObjectSearchScope() throws Exception {
+        final AuthorizerConfigurationContext configurationContext = getBaseConfiguration(null, GROUP_SEARCH_BASE);
+        when(configurationContext.getProperty(PROP_GROUP_MEMBER_ATTRIBUTE)).thenReturn(new StandardPropertyValue("member"));
+        when(configurationContext.getProperty(PROP_GROUP_SEARCH_SCOPE)).thenReturn(new StandardPropertyValue(SearchScope.OBJECT.name()));
+        ldapUserGroupProvider.onConfigured(configurationContext);
+
+        assertTrue(ldapUserGroupProvider.getUsers().isEmpty());
+        assertTrue(ldapUserGroupProvider.getGroups().isEmpty());
+    }
+
+    @Test
+    public void testSearchGroupsSubtreeSearchScope() throws Exception {
+        final AuthorizerConfigurationContext configurationContext = getBaseConfiguration(null, "o=nifi");
+        when(configurationContext.getProperty(PROP_GROUP_MEMBER_ATTRIBUTE)).thenReturn(new StandardPropertyValue("member"));
+        when(configurationContext.getProperty(PROP_GROUP_SEARCH_SCOPE)).thenReturn(new StandardPropertyValue(SearchScope.SUBTREE.name()));
+        ldapUserGroupProvider.onConfigured(configurationContext);
+
+        assertEquals(4, ldapUserGroupProvider.getGroups().size());
+    }
+
+    @Test
+    public void testSearchGroupsWithNameAttribute() throws Exception {
+        final AuthorizerConfigurationContext configurationContext = getBaseConfiguration(null, GROUP_SEARCH_BASE);
+        when(configurationContext.getProperty(PROP_GROUP_MEMBER_ATTRIBUTE)).thenReturn(new StandardPropertyValue("member"));
+        when(configurationContext.getProperty(PROP_GROUP_NAME_ATTRIBUTE)).thenReturn(new StandardPropertyValue("cn"));
+        ldapUserGroupProvider.onConfigured(configurationContext);
+
+        final Set<Group> groups = ldapUserGroupProvider.getGroups();
+        assertEquals(4, groups.size());
+
+        final Group admins = groups.stream().filter(group -> "admins".equals(group.getName())).findFirst().orElse(null);
+        assertNotNull(admins);
+        assertFalse(admins.getUsers().isEmpty());
+        assertEquals(1, admins.getUsers().stream().map(
+                userIdentifier -> ldapUserGroupProvider.getUser(userIdentifier)).filter(
+                        user -> "cn=User 1,ou=users,o=nifi".equals(user.getIdentity())).count());
+    }
+
+    @Test
+    public void testSearchGroupsWithNoNameAndUserIdentityUidAttribute() throws Exception {
+        final AuthorizerConfigurationContext configurationContext = getBaseConfiguration(null, GROUP_SEARCH_BASE);
+        when(configurationContext.getProperty(PROP_GROUP_MEMBER_ATTRIBUTE)).thenReturn(new StandardPropertyValue("member"));
+        when(configurationContext.getProperty(PROP_USER_IDENTITY_ATTRIBUTE)).thenReturn(new StandardPropertyValue("uid"));
+        ldapUserGroupProvider.onConfigured(configurationContext);
+
+        final Set<Group> groups = ldapUserGroupProvider.getGroups();
+        assertEquals(4, groups.size());
+
+        final Group admins = groups.stream().filter(group -> "cn=admins,ou=groups,o=nifi".equals(group.getName())).findFirst().orElse(null);
+        assertNotNull(admins);
+        assertFalse(admins.getUsers().isEmpty());
+        assertEquals(1, admins.getUsers().stream().map(
+                userIdentifier -> ldapUserGroupProvider.getUser(userIdentifier)).filter(
+                user -> "user1".equals(user.getIdentity())).count());
+    }
+
+    @Test
+    public void testSearchGroupsWithNameAndUserIdentityCnAttribute() throws Exception {
+        final AuthorizerConfigurationContext configurationContext = getBaseConfiguration(null, GROUP_SEARCH_BASE);
+        when(configurationContext.getProperty(PROP_GROUP_MEMBER_ATTRIBUTE)).thenReturn(new StandardPropertyValue("member"));
+        when(configurationContext.getProperty(PROP_GROUP_NAME_ATTRIBUTE)).thenReturn(new StandardPropertyValue("cn"));
+        when(configurationContext.getProperty(PROP_USER_IDENTITY_ATTRIBUTE)).thenReturn(new StandardPropertyValue("cn"));
+        ldapUserGroupProvider.onConfigured(configurationContext);
+
+        final Set<Group> groups = ldapUserGroupProvider.getGroups();
+        assertEquals(4, groups.size());
+
+        final Group admins = groups.stream().filter(group -> "admins".equals(group.getName())).findFirst().orElse(null);
+        assertNotNull(admins);
+        assertFalse(admins.getUsers().isEmpty());
+        assertEquals(1, admins.getUsers().stream().map(
+                userIdentifier -> ldapUserGroupProvider.getUser(userIdentifier)).filter(
+                user -> "User 1".equals(user.getIdentity())).count());
+    }
+
+    @Test
+    public void testSearchGroupsWithFilter() throws Exception {
+        final AuthorizerConfigurationContext configurationContext = getBaseConfiguration(null, GROUP_SEARCH_BASE);
+        when(configurationContext.getProperty(PROP_GROUP_MEMBER_ATTRIBUTE)).thenReturn(new StandardPropertyValue("member"));
+        when(configurationContext.getProperty(PROP_GROUP_SEARCH_FILTER)).thenReturn(new StandardPropertyValue("(cn=admins)"));
+        ldapUserGroupProvider.onConfigured(configurationContext);
+
+        final Set<Group> groups = ldapUserGroupProvider.getGroups();
+        assertEquals(1, groups.size());
+        assertEquals(1, groups.stream().filter(group -> "cn=admins,ou=groups,o=nifi".equals(group.getName())).count());
+    }
+
+    @Test
+    public void testSearchUsersAndGroupsNoMembership() throws Exception {
+        final AuthorizerConfigurationContext configurationContext = getBaseConfiguration(USER_SEARCH_BASE, GROUP_SEARCH_BASE);
+        ldapUserGroupProvider.onConfigured(configurationContext);
+
+        assertEquals(8, ldapUserGroupProvider.getUsers().size());
+
+        final Set<Group> groups = ldapUserGroupProvider.getGroups();
+        assertEquals(4, groups.size());
+        groups.forEach(group -> assertTrue(group.getUsers().isEmpty()));
+    }
+
+    @Test
+    public void testSearchUsersAndGroupsMembershipThroughUsers() throws Exception {
+        final AuthorizerConfigurationContext configurationContext = getBaseConfiguration(USER_SEARCH_BASE, GROUP_SEARCH_BASE);
+        when(configurationContext.getProperty(PROP_USER_IDENTITY_ATTRIBUTE)).thenReturn(new StandardPropertyValue("uid"));
+        when(configurationContext.getProperty(PROP_USER_GROUP_ATTRIBUTE)).thenReturn(new StandardPropertyValue("description")); // using description in lieu of memberof
+        when(configurationContext.getProperty(PROP_GROUP_NAME_ATTRIBUTE)).thenReturn(new StandardPropertyValue("cn"));
+        ldapUserGroupProvider.onConfigured(configurationContext);
+
+        assertEquals(8, ldapUserGroupProvider.getUsers().size());
+
+        final Set<Group> groups = ldapUserGroupProvider.getGroups();
+        assertEquals(4, groups.size());
+
+        final Group team1 = groups.stream().filter(group -> "team1".equals(group.getName())).findFirst().orElse(null);
+        assertNotNull(team1);
+        assertEquals(2, team1.getUsers().size());
+        assertEquals(2, team1.getUsers().stream().map(
+                userIdentifier -> ldapUserGroupProvider.getUser(userIdentifier)).filter(
+                user -> "user4".equals(user.getIdentity()) || "user5".equals(user.getIdentity())).count());
+
+        final Group team2 = groups.stream().filter(group -> "team2".equals(group.getName())).findFirst().orElse(null);
+        assertNotNull(team2);
+        assertEquals(2, team2.getUsers().size());
+        assertEquals(2, team2.getUsers().stream().map(
+                userIdentifier -> ldapUserGroupProvider.getUser(userIdentifier)).filter(
+                user -> "user6".equals(user.getIdentity()) || "user7".equals(user.getIdentity())).count());
+    }
+
+    @Test
+    public void testSearchUsersAndGroupsMembershipThroughGroups() throws Exception {
+        final AuthorizerConfigurationContext configurationContext = getBaseConfiguration(USER_SEARCH_BASE, GROUP_SEARCH_BASE);
+        when(configurationContext.getProperty(PROP_USER_IDENTITY_ATTRIBUTE)).thenReturn(new StandardPropertyValue("uid"));
+        when(configurationContext.getProperty(PROP_GROUP_MEMBER_ATTRIBUTE)).thenReturn(new StandardPropertyValue("member"));
+        when(configurationContext.getProperty(PROP_GROUP_NAME_ATTRIBUTE)).thenReturn(new StandardPropertyValue("cn"));
+        ldapUserGroupProvider.onConfigured(configurationContext);
+
+        assertEquals(8, ldapUserGroupProvider.getUsers().size());
+
+        final Set<Group> groups = ldapUserGroupProvider.getGroups();
+        assertEquals(4, groups.size());
+
+        final Group admins = groups.stream().filter(group -> "admins".equals(group.getName())).findFirst().orElse(null);
+        assertNotNull(admins);
+        assertEquals(2, admins.getUsers().size());
+        assertEquals(2, admins.getUsers().stream().map(
+                userIdentifier -> ldapUserGroupProvider.getUser(userIdentifier)).filter(
+                user -> "user1".equals(user.getIdentity()) || "user3".equals(user.getIdentity())).count());
+
+        final Group readOnly = groups.stream().filter(group -> "read-only".equals(group.getName())).findFirst().orElse(null);
+        assertNotNull(readOnly);
+        assertEquals(1, readOnly.getUsers().size());
+        assertEquals(1, readOnly.getUsers().stream().map(
+                userIdentifier -> ldapUserGroupProvider.getUser(userIdentifier)).filter(
+                user -> "user2".equals(user.getIdentity())).count());
+
+        final Group team1 = groups.stream().filter(group -> "team1".equals(group.getName())).findFirst().orElse(null);
+        assertNotNull(team1);
+        assertEquals(1, team1.getUsers().size());
+        assertEquals(1, team1.getUsers().stream().map(
+                userIdentifier -> ldapUserGroupProvider.getUser(userIdentifier)).filter(
+                user -> "user1".equals(user.getIdentity())).count());
+
+        final Group team2 = groups.stream().filter(group -> "team2".equals(group.getName())).findFirst().orElse(null);
+        assertNotNull(team2);
+        assertEquals(1, team2.getUsers().size());
+        assertEquals(1, team2.getUsers().stream().map(
+                userIdentifier -> ldapUserGroupProvider.getUser(userIdentifier)).filter(
+                user -> "user1".equals(user.getIdentity())).count());
+    }
+
+    @Test
+    public void testSearchUsersAndGroupsMembershipThroughUsersAndGroups() throws Exception {
+        final AuthorizerConfigurationContext configurationContext = getBaseConfiguration(USER_SEARCH_BASE, GROUP_SEARCH_BASE);
+        when(configurationContext.getProperty(PROP_USER_IDENTITY_ATTRIBUTE)).thenReturn(new StandardPropertyValue("uid"));
+        when(configurationContext.getProperty(PROP_USER_GROUP_ATTRIBUTE)).thenReturn(new StandardPropertyValue("description")); // using description in lieu of memberof
+        when(configurationContext.getProperty(PROP_GROUP_MEMBER_ATTRIBUTE)).thenReturn(new StandardPropertyValue("member"));
+        when(configurationContext.getProperty(PROP_GROUP_NAME_ATTRIBUTE)).thenReturn(new StandardPropertyValue("cn"));
+        ldapUserGroupProvider.onConfigured(configurationContext);
+
+        assertEquals(8, ldapUserGroupProvider.getUsers().size());
+
+        final Set<Group> groups = ldapUserGroupProvider.getGroups();
+        assertEquals(4, groups.size());
+
+        final Group admins = groups.stream().filter(group -> "admins".equals(group.getName())).findFirst().orElse(null);
+        assertNotNull(admins);
+        assertEquals(2, admins.getUsers().size());
+        assertEquals(2, admins.getUsers().stream().map(
+                userIdentifier -> ldapUserGroupProvider.getUser(userIdentifier)).filter(
+                user -> "user1".equals(user.getIdentity()) || "user3".equals(user.getIdentity())).count());
+
+        final Group readOnly = groups.stream().filter(group -> "read-only".equals(group.getName())).findFirst().orElse(null);
+        assertNotNull(readOnly);
+        assertEquals(1, readOnly.getUsers().size());
+        assertEquals(1, readOnly.getUsers().stream().map(
+                userIdentifier -> ldapUserGroupProvider.getUser(userIdentifier)).filter(
+                user -> "user2".equals(user.getIdentity())).count());
+
+        final Group team1 = groups.stream().filter(group -> "team1".equals(group.getName())).findFirst().orElse(null);
+        assertNotNull(team1);
+        assertEquals(3, team1.getUsers().size());
+        assertEquals(3, team1.getUsers().stream().map(
+                userIdentifier -> ldapUserGroupProvider.getUser(userIdentifier)).filter(
+                user -> "user1".equals(user.getIdentity()) || "user4".equals(user.getIdentity()) || "user5".equals(user.getIdentity())).count());
+
+        final Group team2 = groups.stream().filter(group -> "team2".equals(group.getName())).findFirst().orElse(null);
+        assertNotNull(team2);
+        assertEquals(3, team2.getUsers().size());
+        assertEquals(3, team2.getUsers().stream().map(
+                userIdentifier -> ldapUserGroupProvider.getUser(userIdentifier)).filter(
+                user -> "user1".equals(user.getIdentity()) || "user6".equals(user.getIdentity()) || "user7".equals(user.getIdentity())).count());
+    }
+
+    @Test
+    public void testUserIdentityMapping() throws Exception {
+        final Properties props = new Properties();
+        props.setProperty("nifi.registry.security.identity.mapping.pattern.dn1", "^cn=(.*?),o=(.*?)$");
+        props.setProperty("nifi.registry.security.identity.mapping.value.dn1", "$1");
+
+        final NiFiRegistryProperties properties = getNiFiProperties(props);
+        ldapUserGroupProvider.setNiFiProperties(properties);
+
+        final AuthorizerConfigurationContext configurationContext = getBaseConfiguration(USER_SEARCH_BASE, null);
+        when(configurationContext.getProperty(PROP_USER_SEARCH_FILTER)).thenReturn(new StandardPropertyValue("(uid=user1)"));
+        ldapUserGroupProvider.onConfigured(configurationContext);
+
+        assertEquals(1, ldapUserGroupProvider.getUsers().size());
+        assertNotNull(ldapUserGroupProvider.getUserByIdentity("User 1,ou=users"));
+    }
+
+    @Test(expected = SecurityProviderCreationException.class)
+    public void testReferencedGroupAttributeWithoutGroupSearchBase() throws Exception {
+        final AuthorizerConfigurationContext configurationContext = getBaseConfiguration("ou=users-2,o=nifi", null);
+        when(configurationContext.getProperty(PROP_USER_GROUP_REFERENCED_GROUP_ATTRIBUTE)).thenReturn(new StandardPropertyValue("cn"));
+        ldapUserGroupProvider.onConfigured(configurationContext);
+    }
+
+    @Test
+    public void testReferencedGroupWithoutDefiningReferencedAttribute() throws Exception {
+        final AuthorizerConfigurationContext configurationContext = getBaseConfiguration("ou=users-2,o=nifi", "ou=groups-2,o=nifi");
+        when(configurationContext.getProperty(PROP_USER_IDENTITY_ATTRIBUTE)).thenReturn(new StandardPropertyValue("uid"));
+        when(configurationContext.getProperty(PROP_USER_OBJECT_CLASS)).thenReturn(new StandardPropertyValue("room")); // using room due to reqs of groupOfNames
+        when(configurationContext.getProperty(PROP_USER_GROUP_ATTRIBUTE)).thenReturn(new StandardPropertyValue("description")); // using description in lieu of member
+        when(configurationContext.getProperty(PROP_GROUP_NAME_ATTRIBUTE)).thenReturn(new StandardPropertyValue("cn"));
+        when(configurationContext.getProperty(PROP_GROUP_OBJECT_CLASS)).thenReturn(new StandardPropertyValue("room")); // using room due to reqs of groupOfNames
+        ldapUserGroupProvider.onConfigured(configurationContext);
+
+        final Set<Group> groups = ldapUserGroupProvider.getGroups();
+        assertEquals(1, groups.size());
+
+        final Group team3 = groups.stream().filter(group -> "team3".equals(group.getName())).findFirst().orElse(null);
+        assertNotNull(team3);
+        assertTrue(team3.getUsers().isEmpty());
+    }
+
+    @Test
+    public void testReferencedGroupUsingReferencedAttribute() throws Exception {
+        final AuthorizerConfigurationContext configurationContext = getBaseConfiguration("ou=users-2,o=nifi", "ou=groups-2,o=nifi");
+        when(configurationContext.getProperty(PROP_USER_IDENTITY_ATTRIBUTE)).thenReturn(new StandardPropertyValue("uid"));
+        when(configurationContext.getProperty(PROP_USER_GROUP_ATTRIBUTE)).thenReturn(new StandardPropertyValue("description")); // using description in lieu of member
+        when(configurationContext.getProperty(PROP_USER_GROUP_REFERENCED_GROUP_ATTRIBUTE)).thenReturn(new StandardPropertyValue("cn"));
+        when(configurationContext.getProperty(PROP_GROUP_NAME_ATTRIBUTE)).thenReturn(new StandardPropertyValue("cn"));
+        when(configurationContext.getProperty(PROP_GROUP_OBJECT_CLASS)).thenReturn(new StandardPropertyValue("room")); // using room because groupOfNames requires a member
+        ldapUserGroupProvider.onConfigured(configurationContext);
+
+        final Set<Group> groups = ldapUserGroupProvider.getGroups();
+        assertEquals(1, groups.size());
+
+        final Group team3 = groups.stream().filter(group -> "team3".equals(group.getName())).findFirst().orElse(null);
+        assertNotNull(team3);
+        assertEquals(1, team3.getUsers().size());
+        assertEquals(1, team3.getUsers().stream().map(
+                userIdentifier -> ldapUserGroupProvider.getUser(userIdentifier)).filter(
+                user -> "user9".equals(user.getIdentity())).count());
+    }
+
+    @Test(expected = SecurityProviderCreationException.class)
+    public void testReferencedUserWithoutUserSearchBase() throws Exception {
+        final AuthorizerConfigurationContext configurationContext = getBaseConfiguration(null, "ou=groups-2,o=nifi");
+        when(configurationContext.getProperty(PROP_GROUP_MEMBER_REFERENCED_USER_ATTRIBUTE)).thenReturn(new StandardPropertyValue("uid"));
+        ldapUserGroupProvider.onConfigured(configurationContext);
+    }
+
+    @Test
+    public void testReferencedUserWithoutDefiningReferencedAttribute() throws Exception {
+        final AuthorizerConfigurationContext configurationContext = getBaseConfiguration("ou=users-2,o=nifi", "ou=groups-2,o=nifi");
+        when(configurationContext.getProperty(PROP_USER_IDENTITY_ATTRIBUTE)).thenReturn(new StandardPropertyValue("uid"));
+        when(configurationContext.getProperty(PROP_GROUP_OBJECT_CLASS)).thenReturn(new StandardPropertyValue("room")); // using room due to reqs of groupOfNames
+        when(configurationContext.getProperty(PROP_GROUP_MEMBER_ATTRIBUTE)).thenReturn(new StandardPropertyValue("description")); // using description in lieu of member
+        when(configurationContext.getProperty(PROP_GROUP_NAME_ATTRIBUTE)).thenReturn(new StandardPropertyValue("cn"));
+        ldapUserGroupProvider.onConfigured(configurationContext);
+
+        final Set<Group> groups = ldapUserGroupProvider.getGroups();
+        assertEquals(1, groups.size());
+
+        final Group team3 = groups.stream().filter(group -> "team3".equals(group.getName())).findFirst().orElse(null);
+        assertNotNull(team3);
+        assertTrue(team3.getUsers().isEmpty());
+    }
+
+    @Test
+    public void testReferencedUserUsingReferencedAttribute() throws Exception {
+        final AuthorizerConfigurationContext configurationContext = getBaseConfiguration("ou=users-2,o=nifi", "ou=groups-2,o=nifi");
+        when(configurationContext.getProperty(PROP_USER_IDENTITY_ATTRIBUTE)).thenReturn(new StandardPropertyValue("sn"));
+        when(configurationContext.getProperty(PROP_GROUP_OBJECT_CLASS)).thenReturn(new StandardPropertyValue("room")); // using room due to reqs of groupOfNames
+        when(configurationContext.getProperty(PROP_GROUP_MEMBER_ATTRIBUTE)).thenReturn(new StandardPropertyValue("description")); // using description in lieu of member
+        when(configurationContext.getProperty(PROP_GROUP_NAME_ATTRIBUTE)).thenReturn(new StandardPropertyValue("cn"));
+        when(configurationContext.getProperty(PROP_GROUP_MEMBER_REFERENCED_USER_ATTRIBUTE)).thenReturn(new StandardPropertyValue("uid")); // does not need to be the same as user id attr
+        ldapUserGroupProvider.onConfigured(configurationContext);
+
+        final Set<Group> groups = ldapUserGroupProvider.getGroups();
+        assertEquals(1, groups.size());
+
+        final Group team3 = groups.stream().filter(group -> "team3".equals(group.getName())).findFirst().orElse(null);
+        assertNotNull(team3);
+        assertEquals(1, team3.getUsers().size());
+        assertEquals(1, team3.getUsers().stream().map(
+                userIdentifier -> ldapUserGroupProvider.getUser(userIdentifier)).filter(
+                user -> "User9".equals(user.getIdentity())).count());
+    }
+
+    private AuthorizerConfigurationContext getBaseConfiguration(final String userSearchBase, final String groupSearchBase) {
+        final AuthorizerConfigurationContext configurationContext = mock(AuthorizerConfigurationContext.class);
+        when(configurationContext.getProperty(PROP_URL)).thenReturn(new StandardPropertyValue("ldap://127.0.0.1:" + getLdapServer().getPort()));
+        when(configurationContext.getProperty(PROP_CONNECT_TIMEOUT)).thenReturn(new StandardPropertyValue("30 secs"));
+        when(configurationContext.getProperty(PROP_READ_TIMEOUT)).thenReturn(new StandardPropertyValue("30 secs"));
+        when(configurationContext.getProperty(PROP_REFERRAL_STRATEGY)).thenReturn(new StandardPropertyValue(ReferralStrategy.FOLLOW.name()));
+        when(configurationContext.getProperty(PROP_PAGE_SIZE)).thenReturn(new StandardPropertyValue(null));
+        when(configurationContext.getProperty(PROP_SYNC_INTERVAL)).thenReturn(new StandardPropertyValue("30 mins"));
+
+        when(configurationContext.getProperty(PROP_AUTHENTICATION_STRATEGY)).thenReturn(new StandardPropertyValue(LdapAuthenticationStrategy.SIMPLE.name()));
+        when(configurationContext.getProperty(PROP_MANAGER_DN)).thenReturn(new StandardPropertyValue("uid=admin,ou=system"));
+        when(configurationContext.getProperty(PROP_MANAGER_PASSWORD)).thenReturn(new StandardPropertyValue("secret"));
+
+        when(configurationContext.getProperty(PROP_USER_SEARCH_BASE)).thenReturn(new StandardPropertyValue(userSearchBase));
+        when(configurationContext.getProperty(PROP_USER_OBJECT_CLASS)).thenReturn(new StandardPropertyValue("person"));
+        when(configurationContext.getProperty(PROP_USER_SEARCH_SCOPE)).thenReturn(new StandardPropertyValue(SearchScope.ONE_LEVEL.name()));
+        when(configurationContext.getProperty(PROP_USER_SEARCH_FILTER)).thenReturn(new StandardPropertyValue(null));
+        when(configurationContext.getProperty(PROP_USER_IDENTITY_ATTRIBUTE)).thenReturn(new StandardPropertyValue(null));
+        when(configurationContext.getProperty(PROP_USER_GROUP_ATTRIBUTE)).thenReturn(new StandardPropertyValue(null));
+        when(configurationContext.getProperty(PROP_USER_GROUP_REFERENCED_GROUP_ATTRIBUTE)).thenReturn(new StandardPropertyValue(null));
+
+        when(configurationContext.getProperty(PROP_GROUP_SEARCH_BASE)).thenReturn(new StandardPropertyValue(groupSearchBase));
+        when(configurationContext.getProperty(PROP_GROUP_OBJECT_CLASS)).thenReturn(new StandardPropertyValue("groupOfNames"));
+        when(configurationContext.getProperty(PROP_GROUP_SEARCH_SCOPE)).thenReturn(new StandardPropertyValue(SearchScope.ONE_LEVEL.name()));
+        when(configurationContext.getProperty(PROP_GROUP_SEARCH_FILTER)).thenReturn(new StandardPropertyValue(null));
+        when(configurationContext.getProperty(PROP_GROUP_NAME_ATTRIBUTE)).thenReturn(new StandardPropertyValue(null));
+        when(configurationContext.getProperty(PROP_GROUP_MEMBER_ATTRIBUTE)).thenReturn(new StandardPropertyValue(null));
+        when(configurationContext.getProperty(PROP_GROUP_MEMBER_REFERENCED_USER_ATTRIBUTE)).thenReturn(new StandardPropertyValue(null));
+
+        return configurationContext;
+    }
+
+    private NiFiRegistryProperties getNiFiProperties(final Properties properties) {
+        final NiFiRegistryProperties registryProperties = Mockito.mock(NiFiRegistryProperties.class);
+        when(registryProperties.getPropertyKeys()).thenReturn(properties.stringPropertyNames());
+        when(registryProperties.getProperty(anyString())).then(invocationOnMock -> properties.getProperty((String) invocationOnMock.getArguments()[0]));
+        return registryProperties;
+    }
+}
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/6f26290d/nifi-registry-core/nifi-registry-framework/src/test/java/org/apache/nifi/registry/serialization/TestVersionedProcessGroupSerializer.java
----------------------------------------------------------------------
diff --git a/nifi-registry-core/nifi-registry-framework/src/test/java/org/apache/nifi/registry/serialization/TestVersionedProcessGroupSerializer.java b/nifi-registry-core/nifi-registry-framework/src/test/java/org/apache/nifi/registry/serialization/TestVersionedProcessGroupSerializer.java
new file mode 100644
index 0000000..584e2f7
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-framework/src/test/java/org/apache/nifi/registry/serialization/TestVersionedProcessGroupSerializer.java
@@ -0,0 +1,130 @@
+/*
+ * 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.serialization;
+
+import org.apache.nifi.registry.flow.VersionedProcessGroup;
+import org.apache.nifi.registry.flow.VersionedProcessor;
+import org.junit.Assert;
+import org.junit.Test;
+
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.fail;
+
+public class TestVersionedProcessGroupSerializer {
+
+    @Test
+    public void testSerializeDeserializeFlowSnapshot() throws SerializationException {
+        final Serializer<VersionedProcessGroup> serializer = new VersionedProcessGroupSerializer();
+
+        final VersionedProcessGroup processGroup1 = new VersionedProcessGroup();
+        processGroup1.setIdentifier("pg1");
+        processGroup1.setName("My Process Group");
+
+        final VersionedProcessor processor1 = new VersionedProcessor();
+        processor1.setIdentifier("processor1");
+        processor1.setName("My Processor 1");
+
+        // make sure nested objects are serialized/deserialized
+        processGroup1.getProcessors().add(processor1);
+
+        final ByteArrayOutputStream out = new ByteArrayOutputStream();
+        serializer.serialize(processGroup1, out);
+
+        final ByteArrayInputStream in = new ByteArrayInputStream(out.toByteArray());
+        final VersionedProcessGroup deserializedProcessGroup1 = serializer.deserialize(in);
+
+        Assert.assertEquals(processGroup1.getIdentifier(), deserializedProcessGroup1.getIdentifier());
+        Assert.assertEquals(processGroup1.getName(), deserializedProcessGroup1.getName());
+
+        Assert.assertEquals(1, deserializedProcessGroup1.getProcessors().size());
+
+        final VersionedProcessor deserializedProcessor1 = deserializedProcessGroup1.getProcessors().iterator().next();
+        Assert.assertEquals(processor1.getIdentifier(), deserializedProcessor1.getIdentifier());
+        Assert.assertEquals(processor1.getName(), deserializedProcessor1.getName());
+
+    }
+
+    @Test
+    public void testDeserializeJsonNonIntegerVersion() throws IOException {
+        final String file = "/serialization/json/non-integer-version.snapshot";
+        final VersionedProcessGroupSerializer serializer = new VersionedProcessGroupSerializer();
+        try (final InputStream is = this.getClass().getResourceAsStream(file)) {
+            try {
+                serializer.deserialize(is);
+                fail("Should fail");
+            } catch (SerializationException e) {
+                assertEquals("Unable to find a process group serializer compatible with the input.", e.getMessage());
+            }
+        }
+    }
+
+    @Test
+    public void testDeserializeJsonNoVersion() throws IOException {
+        final String file = "/serialization/json/no-version.snapshot";
+        final VersionedProcessGroupSerializer serializer = new VersionedProcessGroupSerializer();
+        try (final InputStream is = this.getClass().getResourceAsStream(file)) {
+            try {
+                serializer.deserialize(is);
+                fail("Should fail");
+            } catch (SerializationException e) {
+                assertEquals("Unable to find a process group serializer compatible with the input.", e.getMessage());
+            }
+        }
+    }
+
+    @Test
+    public void testDeserializeVer1() throws IOException {
+        final String file = "/serialization/ver1.snapshot";
+        final VersionedProcessGroupSerializer serializer = new VersionedProcessGroupSerializer();
+        final VersionedProcessGroup processGroup;
+        try (final InputStream is = this.getClass().getResourceAsStream(file)) {
+            processGroup = serializer.deserialize(is);
+        }
+        System.out.printf("processGroup=" + processGroup);
+    }
+
+    @Test
+    public void testDeserializeVer2() throws IOException {
+        final String file = "/serialization/ver2.snapshot";
+        final VersionedProcessGroupSerializer serializer = new VersionedProcessGroupSerializer();
+        final VersionedProcessGroup processGroup;
+        try (final InputStream is = this.getClass().getResourceAsStream(file)) {
+            processGroup = serializer.deserialize(is);
+        }
+        System.out.printf("processGroup=" + processGroup);
+    }
+
+    @Test
+    public void testDeserializeVer3() throws IOException {
+        final String file = "/serialization/ver3.snapshot";
+        final VersionedProcessGroupSerializer serializer = new VersionedProcessGroupSerializer();
+        try (final InputStream is = this.getClass().getResourceAsStream(file)) {
+            try {
+                serializer.deserialize(is);
+                fail("Should fail");
+            } catch (SerializationException e) {
+                assertEquals("Unable to find a process group serializer compatible with the input.", e.getMessage());
+            }
+        }
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/6f26290d/nifi-registry-core/nifi-registry-framework/src/test/java/org/apache/nifi/registry/serialization/jaxb/TestJAXBVersionedProcessGroupSerializer.java
----------------------------------------------------------------------
diff --git a/nifi-registry-core/nifi-registry-framework/src/test/java/org/apache/nifi/registry/serialization/jaxb/TestJAXBVersionedProcessGroupSerializer.java b/nifi-registry-core/nifi-registry-framework/src/test/java/org/apache/nifi/registry/serialization/jaxb/TestJAXBVersionedProcessGroupSerializer.java
new file mode 100644
index 0000000..916e053
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-framework/src/test/java/org/apache/nifi/registry/serialization/jaxb/TestJAXBVersionedProcessGroupSerializer.java
@@ -0,0 +1,72 @@
+/*
+ * 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.serialization.jaxb;
+
+import org.apache.nifi.registry.flow.VersionedProcessGroup;
+import org.apache.nifi.registry.flow.VersionedProcessor;
+import org.apache.nifi.registry.serialization.SerializationException;
+import org.apache.nifi.registry.serialization.VersionedSerializer;
+import org.junit.Assert;
+import org.junit.Test;
+
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.nio.charset.StandardCharsets;
+
+public class TestJAXBVersionedProcessGroupSerializer {
+
+    @Test
+    public void testSerializeDeserializeFlowSnapshot() throws SerializationException {
+        final VersionedSerializer<VersionedProcessGroup> serializer = new JAXBVersionedProcessGroupSerializer();
+
+        final VersionedProcessGroup processGroup1 = new VersionedProcessGroup();
+        processGroup1.setIdentifier("pg1");
+        processGroup1.setName("My Process Group");
+
+        final VersionedProcessor processor1 = new VersionedProcessor();
+        processor1.setIdentifier("processor1");
+        processor1.setName("My Processor 1");
+
+        // make sure nested objects are serialized/deserialized
+        processGroup1.getProcessors().add(processor1);
+
+        final ByteArrayOutputStream out = new ByteArrayOutputStream();
+        serializer.serialize(1, processGroup1, out);
+
+        final String snapshotStr = new String(out.toByteArray(), StandardCharsets.UTF_8);
+        //System.out.println(snapshotStr);
+
+        final ByteArrayInputStream in = new ByteArrayInputStream(out.toByteArray());
+        in.mark(1024);
+        final int version = serializer.readDataModelVersion(in);
+
+        Assert.assertEquals(1, version);
+
+        in.reset();
+        final VersionedProcessGroup deserializedProcessGroup1 = serializer.deserialize(in);
+
+        Assert.assertEquals(processGroup1.getIdentifier(), deserializedProcessGroup1.getIdentifier());
+        Assert.assertEquals(processGroup1.getName(), deserializedProcessGroup1.getName());
+
+        Assert.assertEquals(1, deserializedProcessGroup1.getProcessors().size());
+
+        final VersionedProcessor deserializedProcessor1 = deserializedProcessGroup1.getProcessors().iterator().next();
+        Assert.assertEquals(processor1.getIdentifier(), deserializedProcessor1.getIdentifier());
+        Assert.assertEquals(processor1.getName(), deserializedProcessor1.getName());
+    }
+
+}


[48/51] [partial] nifi-registry git commit: NIFIREG-201 Refactoring project structure to better isolate extensions

Posted by kd...@apache.org.
http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/6f26290d/nifi-registry-core/nifi-registry-bootstrap/src/main/java/org/apache/nifi/registry/bootstrap/BootstrapCodec.java
----------------------------------------------------------------------
diff --git a/nifi-registry-core/nifi-registry-bootstrap/src/main/java/org/apache/nifi/registry/bootstrap/BootstrapCodec.java b/nifi-registry-core/nifi-registry-bootstrap/src/main/java/org/apache/nifi/registry/bootstrap/BootstrapCodec.java
new file mode 100644
index 0000000..a273e07
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-bootstrap/src/main/java/org/apache/nifi/registry/bootstrap/BootstrapCodec.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.bootstrap;
+
+import java.io.BufferedReader;
+import java.io.BufferedWriter;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.io.OutputStream;
+import java.io.OutputStreamWriter;
+import java.util.Arrays;
+
+import org.apache.nifi.registry.bootstrap.exception.InvalidCommandException;
+
+public class BootstrapCodec {
+
+    private final RunNiFiRegistry runner;
+    private final BufferedReader reader;
+    private final BufferedWriter writer;
+
+    public BootstrapCodec(final RunNiFiRegistry runner, final InputStream in, final OutputStream out) {
+        this.runner = runner;
+        this.reader = new BufferedReader(new InputStreamReader(in));
+        this.writer = new BufferedWriter(new OutputStreamWriter(out));
+    }
+
+    public void communicate() throws IOException {
+        final String line = reader.readLine();
+        final String[] splits = line.split(" ");
+        if (splits.length < 0) {
+            throw new IOException("Received invalid command from NiFi Registry: " + line);
+        }
+
+        final String cmd = splits[0];
+        final String[] args;
+        if (splits.length == 1) {
+            args = new String[0];
+        } else {
+            args = Arrays.copyOfRange(splits, 1, splits.length);
+        }
+
+        try {
+            processRequest(cmd, args);
+        } catch (final InvalidCommandException ice) {
+            throw new IOException("Received invalid command from NiFi Registry: " + line + (ice.getMessage() == null ? "" : " - Details: " + ice.toString()));
+        }
+    }
+
+    private void processRequest(final String cmd, final String[] args) throws InvalidCommandException, IOException {
+        switch (cmd) {
+            case "PORT": {
+                if (args.length != 2) {
+                    throw new InvalidCommandException();
+                }
+
+                final int port;
+                try {
+                    port = Integer.parseInt(args[0]);
+                } catch (final NumberFormatException nfe) {
+                    throw new InvalidCommandException("Invalid Port number; should be integer between 1 and 65535");
+                }
+
+                if (port < 1 || port > 65535) {
+                    throw new InvalidCommandException("Invalid Port number; should be integer between 1 and 65535");
+                }
+
+                final String secretKey = args[1];
+
+                runner.setNiFiRegistryCommandControlPort(port, secretKey);
+                writer.write("OK");
+                writer.newLine();
+                writer.flush();
+            }
+            break;
+            case "STARTED": {
+                if (args.length != 1) {
+                    throw new InvalidCommandException("STARTED command must contain a status argument");
+                }
+
+                if (!"true".equals(args[0]) && !"false".equals(args[0])) {
+                    throw new InvalidCommandException("Invalid status for STARTED command; should be true or false, but was '" + args[0] + "'");
+                }
+
+                final boolean started = Boolean.parseBoolean(args[0]);
+                runner.setNiFiRegistryStarted(started);
+                writer.write("OK");
+                writer.newLine();
+                writer.flush();
+            }
+            break;
+        }
+    }
+}

http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/6f26290d/nifi-registry-core/nifi-registry-bootstrap/src/main/java/org/apache/nifi/registry/bootstrap/NiFiRegistryListener.java
----------------------------------------------------------------------
diff --git a/nifi-registry-core/nifi-registry-bootstrap/src/main/java/org/apache/nifi/registry/bootstrap/NiFiRegistryListener.java b/nifi-registry-core/nifi-registry-bootstrap/src/main/java/org/apache/nifi/registry/bootstrap/NiFiRegistryListener.java
new file mode 100644
index 0000000..f2ead2e
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-bootstrap/src/main/java/org/apache/nifi/registry/bootstrap/NiFiRegistryListener.java
@@ -0,0 +1,141 @@
+/*
+ * 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.bootstrap;
+
+import org.apache.nifi.registry.bootstrap.util.LimitingInputStream;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.net.InetSocketAddress;
+import java.net.ServerSocket;
+import java.net.Socket;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.concurrent.ThreadFactory;
+import java.util.concurrent.TimeUnit;
+
+public class NiFiRegistryListener {
+
+    private ServerSocket serverSocket;
+    private volatile Listener listener;
+
+    int start(final RunNiFiRegistry runner) throws IOException {
+        serverSocket = new ServerSocket();
+        serverSocket.bind(new InetSocketAddress("localhost", 0));
+
+        final int localPort = serverSocket.getLocalPort();
+        listener = new Listener(serverSocket, runner);
+        final Thread listenThread = new Thread(listener);
+        listenThread.setName("Listen to NiFi Registry");
+        listenThread.setDaemon(true);
+        listenThread.start();
+        return localPort;
+    }
+
+    public void stop() throws IOException {
+        final Listener listener = this.listener;
+        if (listener == null) {
+            return;
+        }
+
+        listener.stop();
+    }
+
+    private class Listener implements Runnable {
+
+        private final ServerSocket serverSocket;
+        private final ExecutorService executor;
+        private final RunNiFiRegistry runner;
+        private volatile boolean stopped = false;
+
+        public Listener(final ServerSocket serverSocket, final RunNiFiRegistry runner) {
+            this.serverSocket = serverSocket;
+            this.executor = Executors.newFixedThreadPool(2, new ThreadFactory() {
+                @Override
+                public Thread newThread(final Runnable runnable) {
+                    final Thread t = Executors.defaultThreadFactory().newThread(runnable);
+                    t.setDaemon(true);
+                    t.setName("NiFi Registry Bootstrap Command Listener");
+                    return t;
+                }
+            });
+
+            this.runner = runner;
+        }
+
+        public void stop() throws IOException {
+            stopped = true;
+
+            executor.shutdown();
+            try {
+                executor.awaitTermination(3, TimeUnit.SECONDS);
+            } catch (final InterruptedException ie) {
+            }
+
+            serverSocket.close();
+        }
+
+        @Override
+        public void run() {
+            while (!serverSocket.isClosed()) {
+                try {
+                    if (stopped) {
+                        return;
+                    }
+
+                    final Socket socket;
+                    try {
+                        socket = serverSocket.accept();
+                    } catch (final IOException ioe) {
+                        if (stopped) {
+                            return;
+                        }
+
+                        throw ioe;
+                    }
+
+                    executor.submit(new Runnable() {
+                        @Override
+                        public void run() {
+                            try {
+                                // we want to ensure that we don't try to read data from an InputStream directly
+                                // by a BufferedReader because any user on the system could open a socket and send
+                                // a multi-gigabyte file without any new lines in order to crash the Bootstrap,
+                                // which in turn may cause the Shutdown Hook to shutdown NiFi.
+                                // So we will limit the amount of data to read to 4 KB
+                                final InputStream limitingIn = new LimitingInputStream(socket.getInputStream(), 4096);
+                                final BootstrapCodec codec = new BootstrapCodec(runner, limitingIn, socket.getOutputStream());
+                                codec.communicate();
+                            } catch (final Throwable t) {
+                                System.out.println("Failed to communicate with NiFi Registry due to " + t);
+                                t.printStackTrace();
+                            } finally {
+                                try {
+                                    socket.close();
+                                } catch (final IOException ioe) {
+                                }
+                            }
+                        }
+                    });
+                } catch (final Throwable t) {
+                    System.err.println("Failed to receive information from NiFi Registry due to " + t);
+                    t.printStackTrace();
+                }
+            }
+        }
+    }
+}

http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/6f26290d/nifi-registry-core/nifi-registry-bootstrap/src/main/java/org/apache/nifi/registry/bootstrap/RunNiFiRegistry.java
----------------------------------------------------------------------
diff --git a/nifi-registry-core/nifi-registry-bootstrap/src/main/java/org/apache/nifi/registry/bootstrap/RunNiFiRegistry.java b/nifi-registry-core/nifi-registry-bootstrap/src/main/java/org/apache/nifi/registry/bootstrap/RunNiFiRegistry.java
new file mode 100644
index 0000000..769d1c4
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-bootstrap/src/main/java/org/apache/nifi/registry/bootstrap/RunNiFiRegistry.java
@@ -0,0 +1,1246 @@
+/*
+ * 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.bootstrap;
+
+import org.apache.commons.lang3.StringUtils;
+import org.apache.nifi.registry.bootstrap.util.OSUtils;
+import org.apache.nifi.registry.util.FileUtils;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.BufferedReader;
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileNotFoundException;
+import java.io.FileOutputStream;
+import java.io.FilenameFilter;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.io.OutputStream;
+import java.io.Reader;
+import java.lang.reflect.Method;
+import java.net.InetAddress;
+import java.net.InetSocketAddress;
+import java.net.Socket;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
+import java.nio.file.attribute.PosixFilePermission;
+import java.text.SimpleDateFormat;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Map.Entry;
+import java.util.Properties;
+import java.util.Set;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.concurrent.Future;
+import java.util.concurrent.ThreadFactory;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.locks.Condition;
+import java.util.concurrent.locks.Lock;
+import java.util.concurrent.locks.ReentrantLock;
+
+/**
+ * <p>
+ * The class which bootstraps Apache NiFi Registry. This class looks for the
+ * bootstrap.conf file by looking in the following places (in order):</p>
+ * <ol>
+ * <li>Java System Property named
+ * {@code org.apache.nifi.registry.bootstrap.config.file}</li>
+ * <li>${NIFI_HOME}/./conf/bootstrap.conf, where ${NIFI_REGISTRY_HOME} references an
+ * environment variable {@code NIFI_REGISTRY_HOME}</li>
+ * <li>./conf/bootstrap.conf, where {@code ./} represents the working
+ * directory.</li>
+ * </ol>
+ * <p>
+ * If the {@code bootstrap.conf} file cannot be found, throws a {@code FileNotFoundException}.
+ */
+public class RunNiFiRegistry {
+
+    public static final String DEFAULT_CONFIG_FILE = "./conf/bootstrap.conf";
+    public static final String DEFAULT_JAVA_CMD = "java";
+    public static final String DEFAULT_PID_DIR = "bin";
+    public static final String DEFAULT_LOG_DIR = "./logs";
+
+    public static final String GRACEFUL_SHUTDOWN_PROP = "graceful.shutdown.seconds";
+    public static final String DEFAULT_GRACEFUL_SHUTDOWN_VALUE = "20";
+
+    public static final String NIFI_REGISTRY_PID_DIR_PROP = "org.apache.nifi.registry.bootstrap.config.pid.dir";
+    public static final String NIFI_REGISTRY_PID_FILE_NAME = "nifi-registry.pid";
+    public static final String NIFI_REGISTRY_STATUS_FILE_NAME = "nifi-registry.status";
+    public static final String NIFI_REGISTRY_LOCK_FILE_NAME = "nifi-registry.lock";
+    public static final String NIFI_REGISTRY_BOOTSTRAP_SENSITIVE_KEY = "nifi.registry.bootstrap.sensitive.key";
+
+    public static final String PID_KEY = "pid";
+
+    public static final int STARTUP_WAIT_SECONDS = 60;
+
+    public static final String SHUTDOWN_CMD = "SHUTDOWN";
+    public static final String PING_CMD = "PING";
+    public static final String DUMP_CMD = "DUMP";
+
+    private volatile boolean autoRestartNiFiRegistry = true;
+    private volatile int ccPort = -1;
+    private volatile long nifiRegistryPid = -1L;
+    private volatile String secretKey;
+    private volatile ShutdownHook shutdownHook;
+    private volatile boolean nifiRegistryStarted;
+
+    private final Lock startedLock = new ReentrantLock();
+    private final Lock lock = new ReentrantLock();
+    private final Condition startupCondition = lock.newCondition();
+
+    private final File bootstrapConfigFile;
+
+    // used for logging initial info; these will be logged to console by default when the app is started
+    private final Logger cmdLogger = LoggerFactory.getLogger("org.apache.nifi.registry.bootstrap.Command");
+    // used for logging all info. These by default will be written to the log file
+    private final Logger defaultLogger = LoggerFactory.getLogger(RunNiFiRegistry.class);
+
+
+    private final ExecutorService loggingExecutor;
+    private volatile Set<Future<?>> loggingFutures = new HashSet<>(2);
+
+    public RunNiFiRegistry(final File bootstrapConfigFile, final boolean verbose) throws IOException {
+        this.bootstrapConfigFile = bootstrapConfigFile;
+
+        loggingExecutor = Executors.newFixedThreadPool(2, new ThreadFactory() {
+            @Override
+            public Thread newThread(final Runnable runnable) {
+                final Thread t = Executors.defaultThreadFactory().newThread(runnable);
+                t.setDaemon(true);
+                t.setName("NiFi logging handler");
+                return t;
+            }
+        });
+    }
+
+    private static void printUsage() {
+        System.out.println("Usage:");
+        System.out.println();
+        System.out.println("java org.apache.nifi.bootstrap.RunNiFiRegistry [<-verbose>] <command> [options]");
+        System.out.println();
+        System.out.println("Valid commands include:");
+        System.out.println("");
+        System.out.println("Start : Start a new instance of Apache NiFi Registry");
+        System.out.println("Stop : Stop a running instance of Apache NiFi Registry");
+        System.out.println("Restart : Stop Apache NiFi Registry, if it is running, and then start a new instance");
+        System.out.println("Status : Determine if there is a running instance of Apache NiFi Registry");
+        System.out.println("Dump : Write a Thread Dump to the file specified by [options], or to the log if no file is given");
+        System.out.println("Run : Start a new instance of Apache NiFi Registry and monitor the Process, restarting if the instance dies");
+        System.out.println();
+    }
+
+    private static String[] shift(final String[] orig) {
+        return Arrays.copyOfRange(orig, 1, orig.length);
+    }
+
+    public static void main(String[] args) throws IOException, InterruptedException {
+        if (args.length < 1 || args.length > 3) {
+            printUsage();
+            return;
+        }
+
+        File dumpFile = null;
+        boolean verbose = false;
+        if (args[0].equals("-verbose")) {
+            verbose = true;
+            args = shift(args);
+        }
+
+        final String cmd = args[0];
+        if (cmd.equals("dump")) {
+            if (args.length > 1) {
+                dumpFile = new File(args[1]);
+            } else {
+                dumpFile = null;
+            }
+        }
+
+        switch (cmd.toLowerCase()) {
+            case "start":
+            case "run":
+            case "stop":
+            case "status":
+            case "dump":
+            case "restart":
+            case "env":
+                break;
+            default:
+                printUsage();
+                return;
+        }
+
+        final File configFile = getDefaultBootstrapConfFile();
+        final RunNiFiRegistry runNiFiRegistry = new RunNiFiRegistry(configFile, verbose);
+
+        Integer exitStatus = null;
+        switch (cmd.toLowerCase()) {
+            case "start":
+                runNiFiRegistry.start();
+                break;
+            case "run":
+                runNiFiRegistry.start();
+                break;
+            case "stop":
+                runNiFiRegistry.stop();
+                break;
+            case "status":
+                exitStatus = runNiFiRegistry.status();
+                break;
+            case "restart":
+                runNiFiRegistry.stop();
+                runNiFiRegistry.start();
+                break;
+            case "dump":
+                runNiFiRegistry.dump(dumpFile);
+                break;
+            case "env":
+                runNiFiRegistry.env();
+                break;
+        }
+        if (exitStatus != null) {
+            System.exit(exitStatus);
+        }
+    }
+
+    private static File getDefaultBootstrapConfFile() {
+        String configFilename = System.getProperty("org.apache.nifi.registry.bootstrap.config.file");
+
+        if (configFilename == null) {
+            final String nifiRegistryHome = System.getenv("NIFI_REGISTRY_HOME");
+            if (nifiRegistryHome != null) {
+                final File nifiRegistryHomeFile = new File(nifiRegistryHome.trim());
+                final File configFile = new File(nifiRegistryHomeFile, DEFAULT_CONFIG_FILE);
+                configFilename = configFile.getAbsolutePath();
+            }
+        }
+
+        if (configFilename == null) {
+            configFilename = DEFAULT_CONFIG_FILE;
+        }
+
+        final File configFile = new File(configFilename);
+        return configFile;
+    }
+
+    protected File getBootstrapFile(final Logger logger, String directory, String defaultDirectory, String fileName) throws IOException {
+
+        final File confDir = bootstrapConfigFile.getParentFile();
+        final File nifiHome = confDir.getParentFile();
+
+        String confFileDir = System.getProperty(directory);
+
+        final File fileDir;
+
+        if (confFileDir != null) {
+            fileDir = new File(confFileDir.trim());
+        } else {
+            fileDir = new File(nifiHome, defaultDirectory);
+        }
+
+        FileUtils.ensureDirectoryExistAndCanAccess(fileDir);
+        final File statusFile = new File(fileDir, fileName);
+        logger.debug("Status File: {}", statusFile);
+        return statusFile;
+    }
+
+    protected File getPidFile(final Logger logger) throws IOException {
+        return getBootstrapFile(logger, NIFI_REGISTRY_PID_DIR_PROP, DEFAULT_PID_DIR, NIFI_REGISTRY_PID_FILE_NAME);
+    }
+
+    protected File getStatusFile(final Logger logger) throws IOException {
+        return getBootstrapFile(logger, NIFI_REGISTRY_PID_DIR_PROP, DEFAULT_PID_DIR, NIFI_REGISTRY_STATUS_FILE_NAME);
+    }
+
+    protected File getLockFile(final Logger logger) throws IOException {
+        return getBootstrapFile(logger, NIFI_REGISTRY_PID_DIR_PROP, DEFAULT_PID_DIR, NIFI_REGISTRY_LOCK_FILE_NAME);
+    }
+
+    protected File getStatusFile() throws IOException {
+        return getStatusFile(defaultLogger);
+    }
+
+    private Properties loadProperties(final Logger logger) throws IOException {
+        final Properties props = new Properties();
+        final File statusFile = getStatusFile(logger);
+        if (statusFile == null || !statusFile.exists()) {
+            logger.debug("No status file to load properties from");
+            return props;
+        }
+
+        try (final FileInputStream fis = new FileInputStream(getStatusFile(logger))) {
+            props.load(fis);
+        }
+
+        final Map<Object, Object> modified = new HashMap<>(props);
+        modified.remove("secret.key");
+        logger.debug("Properties: {}", modified);
+
+        return props;
+    }
+
+    private synchronized void savePidProperties(final Properties pidProperties, final Logger logger) throws IOException {
+        final String pid = pidProperties.getProperty(PID_KEY);
+        if (!StringUtils.isBlank(pid)) {
+            writePidFile(pid, logger);
+        }
+
+        final File statusFile = getStatusFile(logger);
+        if (statusFile.exists() && !statusFile.delete()) {
+            logger.warn("Failed to delete {}", statusFile);
+        }
+
+        if (!statusFile.createNewFile()) {
+            throw new IOException("Failed to create file " + statusFile);
+        }
+
+        try {
+            final Set<PosixFilePermission> perms = new HashSet<>();
+            perms.add(PosixFilePermission.OWNER_READ);
+            perms.add(PosixFilePermission.OWNER_WRITE);
+            Files.setPosixFilePermissions(statusFile.toPath(), perms);
+        } catch (final Exception e) {
+            logger.warn("Failed to set permissions so that only the owner can read status file {}; "
+                    + "this may allows others to have access to the key needed to communicate with NiFi Registry. "
+                    + "Permissions should be changed so that only the owner can read this file", statusFile);
+        }
+
+        try (final FileOutputStream fos = new FileOutputStream(statusFile)) {
+            pidProperties.store(fos, null);
+            fos.getFD().sync();
+        }
+
+        logger.debug("Saved Properties {} to {}", new Object[]{pidProperties, statusFile});
+    }
+
+    private synchronized void writePidFile(final String pid, final Logger logger) throws IOException {
+        final File pidFile = getPidFile(logger);
+        if (pidFile.exists() && !pidFile.delete()) {
+            logger.warn("Failed to delete {}", pidFile);
+        }
+
+        if (!pidFile.createNewFile()) {
+            throw new IOException("Failed to create file " + pidFile);
+        }
+
+        try {
+            final Set<PosixFilePermission> perms = new HashSet<>();
+            perms.add(PosixFilePermission.OWNER_WRITE);
+            perms.add(PosixFilePermission.OWNER_READ);
+            perms.add(PosixFilePermission.GROUP_READ);
+            perms.add(PosixFilePermission.OTHERS_READ);
+            Files.setPosixFilePermissions(pidFile.toPath(), perms);
+        } catch (final Exception e) {
+            logger.warn("Failed to set permissions so that only the owner can read pid file {}; "
+                    + "this may allows others to have access to the key needed to communicate with NiFi Registry. "
+                    + "Permissions should be changed so that only the owner can read this file", pidFile);
+        }
+
+        try (final FileOutputStream fos = new FileOutputStream(pidFile)) {
+            fos.write(pid.getBytes(StandardCharsets.UTF_8));
+            fos.getFD().sync();
+        }
+
+        logger.debug("Saved Pid {} to {}", new Object[]{pid, pidFile});
+    }
+
+    private boolean isPingSuccessful(final int port, final String secretKey, final Logger logger) {
+        logger.debug("Pinging {}", port);
+
+        try (final Socket socket = new Socket("localhost", port)) {
+            final OutputStream out = socket.getOutputStream();
+            out.write((PING_CMD + " " + secretKey + "\n").getBytes(StandardCharsets.UTF_8));
+            out.flush();
+
+            logger.debug("Sent PING command");
+            socket.setSoTimeout(5000);
+            final InputStream in = socket.getInputStream();
+            final BufferedReader reader = new BufferedReader(new InputStreamReader(in));
+            final String response = reader.readLine();
+            logger.debug("PING response: {}", response);
+            out.close();
+            reader.close();
+
+            return PING_CMD.equals(response);
+        } catch (final IOException ioe) {
+            return false;
+        }
+    }
+
+    private Integer getCurrentPort(final Logger logger) throws IOException {
+        final Properties props = loadProperties(logger);
+        final String portVal = props.getProperty("port");
+        if (portVal == null) {
+            logger.debug("No Port found in status file");
+            return null;
+        } else {
+            logger.debug("Port defined in status file: {}", portVal);
+        }
+
+        final int port = Integer.parseInt(portVal);
+        final boolean success = isPingSuccessful(port, props.getProperty("secret.key"), logger);
+        if (success) {
+            logger.debug("Successful PING on port {}", port);
+            return port;
+        }
+
+        final String pid = props.getProperty(PID_KEY);
+        logger.debug("PID in status file is {}", pid);
+        if (pid != null) {
+            final boolean procRunning = isProcessRunning(pid, logger);
+            if (procRunning) {
+                return port;
+            } else {
+                return null;
+            }
+        }
+
+        return null;
+    }
+
+    private boolean isProcessRunning(final String pid, final Logger logger) {
+        try {
+            // We use the "ps" command to check if the process is still running.
+            final ProcessBuilder builder = new ProcessBuilder();
+
+            builder.command("ps", "-p", pid);
+            final Process proc = builder.start();
+
+            // Look for the pid in the output of the 'ps' command.
+            boolean running = false;
+            String line;
+            try (final InputStream in = proc.getInputStream();
+                 final Reader streamReader = new InputStreamReader(in);
+                 final BufferedReader reader = new BufferedReader(streamReader)) {
+
+                while ((line = reader.readLine()) != null) {
+                    if (line.trim().startsWith(pid)) {
+                        running = true;
+                    }
+                }
+            }
+
+            // If output of the ps command had our PID, the process is running.
+            if (running) {
+                logger.debug("Process with PID {} is running", pid);
+            } else {
+                logger.debug("Process with PID {} is not running", pid);
+            }
+
+            return running;
+        } catch (final IOException ioe) {
+            System.err.println("Failed to determine if Process " + pid + " is running; assuming that it is not");
+            return false;
+        }
+    }
+
+    private Status getStatus(final Logger logger) {
+        final Properties props;
+        try {
+            props = loadProperties(logger);
+        } catch (final IOException ioe) {
+            return new Status(null, null, false, false);
+        }
+
+        if (props == null) {
+            return new Status(null, null, false, false);
+        }
+
+        final String portValue = props.getProperty("port");
+        final String pid = props.getProperty(PID_KEY);
+        final String secretKey = props.getProperty("secret.key");
+
+        if (portValue == null && pid == null) {
+            return new Status(null, null, false, false);
+        }
+
+        Integer port = null;
+        boolean pingSuccess = false;
+        if (portValue != null) {
+            try {
+                port = Integer.parseInt(portValue);
+                pingSuccess = isPingSuccessful(port, secretKey, logger);
+            } catch (final NumberFormatException nfe) {
+                return new Status(null, null, false, false);
+            }
+        }
+
+        if (pingSuccess) {
+            return new Status(port, pid, true, true);
+        }
+
+        final boolean alive = pid != null && isProcessRunning(pid, logger);
+        return new Status(port, pid, pingSuccess, alive);
+    }
+
+    public int status() throws IOException {
+        final Logger logger = cmdLogger;
+        final Status status = getStatus(logger);
+        if (status.isRespondingToPing()) {
+            logger.info("Apache NiFi Registry is currently running, listening to Bootstrap on port {}, PID={}",
+                    new Object[]{status.getPort(), status.getPid() == null ? "unknown" : status.getPid()});
+            return 0;
+        }
+
+        if (status.isProcessRunning()) {
+            logger.info("Apache NiFi Registry is running at PID {} but is not responding to ping requests", status.getPid());
+            return 4;
+        }
+
+        if (status.getPort() == null) {
+            logger.info("Apache NiFi Registry is not running");
+            return 3;
+        }
+
+        if (status.getPid() == null) {
+            logger.info("Apache NiFi Registry is not responding to Ping requests. The process may have died or may be hung");
+        } else {
+            logger.info("Apache NiFi Registry is not running");
+        }
+        return 3;
+    }
+
+    public void env() {
+        final Logger logger = cmdLogger;
+        final Status status = getStatus(logger);
+        if (status.getPid() == null) {
+            logger.info("Apache NiFi Registry is not running");
+            return;
+        }
+        final Class<?> virtualMachineClass;
+        try {
+            virtualMachineClass = Class.forName("com.sun.tools.attach.VirtualMachine");
+        } catch (final ClassNotFoundException cnfe) {
+            logger.error("Seems tools.jar (Linux / Windows JDK) or classes.jar (Mac OS) is not available in classpath");
+            return;
+        }
+        final Method attachMethod;
+        final Method detachMethod;
+
+        try {
+            attachMethod = virtualMachineClass.getMethod("attach", String.class);
+            detachMethod = virtualMachineClass.getDeclaredMethod("detach");
+        } catch (final Exception e) {
+            logger.error("Methods required for getting environment not available", e);
+            return;
+        }
+
+        final Object virtualMachine;
+        try {
+            virtualMachine = attachMethod.invoke(null, status.getPid());
+        } catch (final Throwable t) {
+            logger.error("Problem attaching to NiFi", t);
+            return;
+        }
+
+        try {
+            final Method getSystemPropertiesMethod = virtualMachine.getClass().getMethod("getSystemProperties");
+
+            final Properties sysProps = (Properties) getSystemPropertiesMethod.invoke(virtualMachine);
+            for (Entry<Object, Object> syspropEntry : sysProps.entrySet()) {
+                logger.info(syspropEntry.getKey().toString() + " = " + syspropEntry.getValue().toString());
+            }
+        } catch (Throwable t) {
+            throw new RuntimeException(t);
+        } finally {
+            try {
+                detachMethod.invoke(virtualMachine);
+            } catch (final Exception e) {
+                logger.warn("Caught exception detaching from process", e);
+            }
+        }
+    }
+
+    /**
+     * Writes a NiFi thread dump to the given file; if file is null, logs at
+     * INFO level instead.
+     *
+     * @param dumpFile the file to write the dump content to
+     * @throws IOException if any issues occur while writing the dump file
+     */
+    public void dump(final File dumpFile) throws IOException {
+        final Logger logger = defaultLogger;    // dump to bootstrap log file by default
+        final Integer port = getCurrentPort(logger);
+        if (port == null) {
+            logger.info("Apache NiFi Registry is not currently running");
+            return;
+        }
+
+        final Properties nifiRegistryProps = loadProperties(logger);
+        final String secretKey = nifiRegistryProps.getProperty("secret.key");
+
+        final StringBuilder sb = new StringBuilder();
+        try (final Socket socket = new Socket()) {
+            logger.debug("Connecting to NiFi Registry instance");
+            socket.setSoTimeout(60000);
+            socket.connect(new InetSocketAddress("localhost", port));
+            logger.debug("Established connection to NiFi Registry instance.");
+            socket.setSoTimeout(60000);
+
+            logger.debug("Sending DUMP Command to port {}", port);
+            final OutputStream out = socket.getOutputStream();
+            out.write((DUMP_CMD + " " + secretKey + "\n").getBytes(StandardCharsets.UTF_8));
+            out.flush();
+
+            final InputStream in = socket.getInputStream();
+            try (final BufferedReader reader = new BufferedReader(new InputStreamReader(in))) {
+                String line;
+                while ((line = reader.readLine()) != null) {
+                    sb.append(line).append("\n");
+                }
+            }
+        }
+
+        final String dump = sb.toString();
+        if (dumpFile == null) {
+            logger.info(dump);
+        } else {
+            try (final FileOutputStream fos = new FileOutputStream(dumpFile)) {
+                fos.write(dump.getBytes(StandardCharsets.UTF_8));
+            }
+            // we want to log to the console (by default) that we wrote the thread dump to the specified file
+            cmdLogger.info("Successfully wrote thread dump to {}", dumpFile.getAbsolutePath());
+        }
+    }
+
+    public void notifyStop() {
+        final String hostname = getHostname();
+        final SimpleDateFormat sdf = new SimpleDateFormat("yyyy/MM/dd HH:mm:ss.SSS");
+        final String now = sdf.format(System.currentTimeMillis());
+        String user = System.getProperty("user.name");
+        if (user == null || user.trim().isEmpty()) {
+            user = "Unknown User";
+        }
+    }
+
+    public void stop() throws IOException {
+        final Logger logger = cmdLogger;
+        final Integer port = getCurrentPort(logger);
+        if (port == null) {
+            logger.info("Apache NiFi Registry is not currently running");
+            return;
+        }
+
+        // indicate that a stop command is in progress
+        final File lockFile = getLockFile(logger);
+        if (!lockFile.exists()) {
+            lockFile.createNewFile();
+        }
+
+        final Properties nifiRegistryProps = loadProperties(logger);
+        final String secretKey = nifiRegistryProps.getProperty("secret.key");
+        final String pid = nifiRegistryProps.getProperty(PID_KEY);
+        final File statusFile = getStatusFile(logger);
+        final File pidFile = getPidFile(logger);
+
+        try (final Socket socket = new Socket()) {
+            logger.debug("Connecting to NiFi Registry instance");
+            socket.setSoTimeout(10000);
+            socket.connect(new InetSocketAddress("localhost", port));
+            logger.debug("Established connection to NiFi Registry instance.");
+            socket.setSoTimeout(10000);
+
+            logger.debug("Sending SHUTDOWN Command to port {}", port);
+            final OutputStream out = socket.getOutputStream();
+            out.write((SHUTDOWN_CMD + " " + secretKey + "\n").getBytes(StandardCharsets.UTF_8));
+            out.flush();
+            socket.shutdownOutput();
+
+            final InputStream in = socket.getInputStream();
+            int lastChar;
+            final StringBuilder sb = new StringBuilder();
+            while ((lastChar = in.read()) > -1) {
+                sb.append((char) lastChar);
+            }
+            final String response = sb.toString().trim();
+
+            logger.debug("Received response to SHUTDOWN command: {}", response);
+
+            if (SHUTDOWN_CMD.equals(response)) {
+                logger.info("Apache NiFi Registry has accepted the Shutdown Command and is shutting down now");
+
+                if (pid != null) {
+                    final Properties bootstrapProperties = new Properties();
+                    try (final FileInputStream fis = new FileInputStream(bootstrapConfigFile)) {
+                        bootstrapProperties.load(fis);
+                    }
+
+                    String gracefulShutdown = bootstrapProperties.getProperty(GRACEFUL_SHUTDOWN_PROP, DEFAULT_GRACEFUL_SHUTDOWN_VALUE);
+                    int gracefulShutdownSeconds;
+                    try {
+                        gracefulShutdownSeconds = Integer.parseInt(gracefulShutdown);
+                    } catch (final NumberFormatException nfe) {
+                        gracefulShutdownSeconds = Integer.parseInt(DEFAULT_GRACEFUL_SHUTDOWN_VALUE);
+                    }
+
+                    notifyStop();
+                    final long startWait = System.nanoTime();
+                    while (isProcessRunning(pid, logger)) {
+                        logger.info("Waiting for Apache NiFi Registry to finish shutting down...");
+                        final long waitNanos = System.nanoTime() - startWait;
+                        final long waitSeconds = TimeUnit.NANOSECONDS.toSeconds(waitNanos);
+                        if (waitSeconds >= gracefulShutdownSeconds && gracefulShutdownSeconds > 0) {
+                            if (isProcessRunning(pid, logger)) {
+                                logger.warn("NiFi Registry has not finished shutting down after {} seconds. Killing process.", gracefulShutdownSeconds);
+                                try {
+                                    killProcessTree(pid, logger);
+                                } catch (final IOException ioe) {
+                                    logger.error("Failed to kill Process with PID {}", pid);
+                                }
+                            }
+                            break;
+                        } else {
+                            try {
+                                Thread.sleep(2000L);
+                            } catch (final InterruptedException ie) {
+                            }
+                        }
+                    }
+
+                    if (statusFile.exists() && !statusFile.delete()) {
+                        logger.error("Failed to delete status file {}; this file should be cleaned up manually", statusFile);
+                    }
+
+                    if (pidFile.exists() && !pidFile.delete()) {
+                        logger.error("Failed to delete pid file {}; this file should be cleaned up manually", pidFile);
+                    }
+
+                    logger.info("NiFi Registry has finished shutting down.");
+                }
+            } else {
+                logger.error("When sending SHUTDOWN command to NiFi Registry , got unexpected response {}", response);
+            }
+        } catch (final IOException ioe) {
+            if (pid == null) {
+                logger.error("Failed to send shutdown command to port {} due to {}. No PID found for the NiFi Registry process, so unable to kill process; "
+                        + "the process should be killed manually.", new Object[]{port, ioe.toString()});
+            } else {
+                logger.error("Failed to send shutdown command to port {} due to {}. Will kill the NiFi Registry Process with PID {}.", port, ioe.toString(), pid);
+                notifyStop();
+                killProcessTree(pid, logger);
+                if (statusFile.exists() && !statusFile.delete()) {
+                    logger.error("Failed to delete status file {}; this file should be cleaned up manually", statusFile);
+                }
+            }
+        } finally {
+            if (lockFile.exists() && !lockFile.delete()) {
+                logger.error("Failed to delete lock file {}; this file should be cleaned up manually", lockFile);
+            }
+        }
+    }
+
+    private static List<String> getChildProcesses(final String ppid) throws IOException {
+        final Process proc = Runtime.getRuntime().exec(new String[]{"ps", "-o", "pid", "--no-headers", "--ppid", ppid});
+        final List<String> childPids = new ArrayList<>();
+        try (final InputStream in = proc.getInputStream();
+             final BufferedReader reader = new BufferedReader(new InputStreamReader(in))) {
+
+            String line;
+            while ((line = reader.readLine()) != null) {
+                childPids.add(line.trim());
+            }
+        }
+
+        return childPids;
+    }
+
+    private void killProcessTree(final String pid, final Logger logger) throws IOException {
+        logger.debug("Killing Process Tree for PID {}", pid);
+
+        final List<String> children = getChildProcesses(pid);
+        logger.debug("Children of PID {}: {}", new Object[]{pid, children});
+
+        for (final String childPid : children) {
+            killProcessTree(childPid, logger);
+        }
+
+        Runtime.getRuntime().exec(new String[]{"kill", "-9", pid});
+    }
+
+    public static boolean isAlive(final Process process) {
+        try {
+            process.exitValue();
+            return false;
+        } catch (final IllegalStateException | IllegalThreadStateException itse) {
+            return true;
+        }
+    }
+
+    private String getHostname() {
+        String hostname = "Unknown Host";
+        String ip = "Unknown IP Address";
+        try {
+            final InetAddress localhost = InetAddress.getLocalHost();
+            hostname = localhost.getHostName();
+            ip = localhost.getHostAddress();
+        } catch (final Exception e) {
+            defaultLogger.warn("Failed to obtain hostname for notification due to:", e);
+        }
+
+        return hostname + " (" + ip + ")";
+    }
+
+    @SuppressWarnings({"rawtypes", "unchecked"})
+    public void start() throws IOException, InterruptedException {
+        final Integer port = getCurrentPort(cmdLogger);
+        if (port != null) {
+            cmdLogger.info("Apache NiFi Registry is already running, listening to Bootstrap on port " + port);
+            return;
+        }
+
+        final File prevLockFile = getLockFile(cmdLogger);
+        if (prevLockFile.exists() && !prevLockFile.delete()) {
+            cmdLogger.warn("Failed to delete previous lock file {}; this file should be cleaned up manually", prevLockFile);
+        }
+
+        final ProcessBuilder builder = new ProcessBuilder();
+
+        if (!bootstrapConfigFile.exists()) {
+            throw new FileNotFoundException(bootstrapConfigFile.getAbsolutePath());
+        }
+
+        final Properties properties = new Properties();
+        try (final FileInputStream fis = new FileInputStream(bootstrapConfigFile)) {
+            properties.load(fis);
+        }
+
+        final Map<String, String> props = new HashMap<>();
+        props.putAll((Map) properties);
+
+        final String specifiedWorkingDir = props.get("working.dir");
+        if (specifiedWorkingDir != null) {
+            builder.directory(new File(specifiedWorkingDir));
+        }
+
+        final File bootstrapConfigAbsoluteFile = bootstrapConfigFile.getAbsoluteFile();
+        final File binDir = bootstrapConfigAbsoluteFile.getParentFile();
+        final File workingDir = binDir.getParentFile();
+
+        if (specifiedWorkingDir == null) {
+            builder.directory(workingDir);
+        }
+
+        final String nifiRegistryLogDir = replaceNull(System.getProperty("org.apache.nifi.registry.bootstrap.config.log.dir"), DEFAULT_LOG_DIR).trim();
+
+        final String libFilename = replaceNull(props.get("lib.dir"), "./lib").trim();
+        File libDir = getFile(libFilename, workingDir);
+        File libSharedDir = getFile(libFilename + "/shared", workingDir);
+
+        final String confFilename = replaceNull(props.get("conf.dir"), "./conf").trim();
+        File confDir = getFile(confFilename, workingDir);
+
+        String nifiRegistryPropsFilename = props.get("props.file");
+        if (nifiRegistryPropsFilename == null) {
+            if (confDir.exists()) {
+                nifiRegistryPropsFilename = new File(confDir, "nifi-registry.properties").getAbsolutePath();
+            } else {
+                nifiRegistryPropsFilename = DEFAULT_CONFIG_FILE;
+            }
+        }
+
+        nifiRegistryPropsFilename = nifiRegistryPropsFilename.trim();
+
+        final List<String> javaAdditionalArgs = new ArrayList<>();
+        for (final Map.Entry<String, String> entry : props.entrySet()) {
+            final String key = entry.getKey();
+            final String value = entry.getValue();
+
+            if (key.startsWith("java.arg")) {
+                javaAdditionalArgs.add(value);
+            }
+        }
+
+        final File[] libSharedFiles = libSharedDir.listFiles(new FilenameFilter() {
+            @Override
+            public boolean accept(final File dir, final String filename) {
+                return filename.toLowerCase().endsWith(".jar");
+            }
+        });
+
+        if (libSharedFiles == null || libSharedFiles.length == 0) {
+            throw new RuntimeException("Could not find lib shared directory at " + libSharedDir.getAbsolutePath());
+        }
+
+        final File[] libFiles = libDir.listFiles(new FilenameFilter() {
+            @Override
+            public boolean accept(final File dir, final String filename) {
+                return filename.toLowerCase().endsWith(".jar");
+            }
+        });
+
+        if (libFiles == null || libFiles.length == 0) {
+            throw new RuntimeException("Could not find lib directory at " + libDir.getAbsolutePath());
+        }
+
+        final File[] confFiles = confDir.listFiles();
+        if (confFiles == null || confFiles.length == 0) {
+            throw new RuntimeException("Could not find conf directory at " + confDir.getAbsolutePath());
+        }
+
+        final List<String> cpFiles = new ArrayList<>(confFiles.length + libFiles.length + libSharedFiles.length);
+        cpFiles.add(confDir.getAbsolutePath());
+        for (final File file : libSharedFiles) {
+            cpFiles.add(file.getAbsolutePath());
+        }
+        for (final File file : libFiles) {
+            cpFiles.add(file.getAbsolutePath());
+        }
+
+        final StringBuilder classPathBuilder = new StringBuilder();
+        for (int i = 0; i < cpFiles.size(); i++) {
+            final String filename = cpFiles.get(i);
+            classPathBuilder.append(filename);
+            if (i < cpFiles.size() - 1) {
+                classPathBuilder.append(File.pathSeparatorChar);
+            }
+        }
+
+        final String classPath = classPathBuilder.toString();
+        String javaCmd = props.get("java");
+        if (javaCmd == null) {
+            javaCmd = DEFAULT_JAVA_CMD;
+        }
+        if (javaCmd.equals(DEFAULT_JAVA_CMD)) {
+            String javaHome = System.getenv("JAVA_HOME");
+            if (javaHome != null) {
+                String fileExtension = isWindows() ? ".exe" : "";
+                File javaFile = new File(javaHome + File.separatorChar + "bin"
+                        + File.separatorChar + "java" + fileExtension);
+                if (javaFile.exists() && javaFile.canExecute()) {
+                    javaCmd = javaFile.getAbsolutePath();
+                }
+            }
+        }
+
+        final NiFiRegistryListener listener = new NiFiRegistryListener();
+        final int listenPort = listener.start(this);
+
+        final List<String> cmd = new ArrayList<>();
+
+        cmd.add(javaCmd);
+        cmd.add("-classpath");
+        cmd.add(classPath);
+        cmd.addAll(javaAdditionalArgs);
+        cmd.add("-Dnifi.registry.properties.file.path=" + nifiRegistryPropsFilename);
+        cmd.add("-Dnifi.registry.bootstrap.config.file.path=" + bootstrapConfigFile.getAbsolutePath());
+        cmd.add("-Dnifi.registry.bootstrap.listen.port=" + listenPort);
+        cmd.add("-Dapp=NiFiRegistry");
+        cmd.add("-Dorg.apache.nifi.registry.bootstrap.config.log.dir=" + nifiRegistryLogDir);
+        cmd.add("org.apache.nifi.registry.NiFiRegistry");
+
+        builder.command(cmd);
+
+        final StringBuilder cmdBuilder = new StringBuilder();
+        for (final String s : cmd) {
+            cmdBuilder.append(s).append(" ");
+        }
+
+        cmdLogger.info("Starting Apache NiFi Registry...");
+        cmdLogger.info("Working Directory: {}", workingDir.getAbsolutePath());
+        cmdLogger.info("Command: {}", cmdBuilder.toString());
+
+        String gracefulShutdown = props.get(GRACEFUL_SHUTDOWN_PROP);
+        if (gracefulShutdown == null) {
+            gracefulShutdown = DEFAULT_GRACEFUL_SHUTDOWN_VALUE;
+        }
+
+        final int gracefulShutdownSeconds;
+        try {
+            gracefulShutdownSeconds = Integer.parseInt(gracefulShutdown);
+        } catch (final NumberFormatException nfe) {
+            throw new NumberFormatException("The '" + GRACEFUL_SHUTDOWN_PROP + "' property in Bootstrap Config File "
+                    + bootstrapConfigAbsoluteFile.getAbsolutePath() + " has an invalid value. Must be a non-negative integer");
+        }
+
+        if (gracefulShutdownSeconds < 0) {
+            throw new NumberFormatException("The '" + GRACEFUL_SHUTDOWN_PROP + "' property in Bootstrap Config File "
+                    + bootstrapConfigAbsoluteFile.getAbsolutePath() + " has an invalid value. Must be a non-negative integer");
+        }
+
+        Process process = builder.start();
+        handleLogging(process);
+        Long pid = OSUtils.getProcessId(process, cmdLogger);
+        if (pid == null) {
+            cmdLogger.warn("Launched Apache NiFi Registry but could not determined the Process ID");
+        } else {
+            nifiRegistryPid = pid;
+            final Properties pidProperties = new Properties();
+            pidProperties.setProperty(PID_KEY, String.valueOf(nifiRegistryPid));
+            savePidProperties(pidProperties, cmdLogger);
+            cmdLogger.info("Launched Apache NiFi Registry with Process ID " + pid);
+        }
+
+        shutdownHook = new ShutdownHook(process, this, secretKey, gracefulShutdownSeconds, loggingExecutor);
+        final Runtime runtime = Runtime.getRuntime();
+        runtime.addShutdownHook(shutdownHook);
+
+        final String hostname = getHostname();
+        final SimpleDateFormat sdf = new SimpleDateFormat("yyyy/MM/dd HH:mm:ss.SSS");
+        String now = sdf.format(System.currentTimeMillis());
+        String user = System.getProperty("user.name");
+        if (user == null || user.trim().isEmpty()) {
+            user = "Unknown User";
+        }
+
+        while (true) {
+            final boolean alive = isAlive(process);
+
+            if (alive) {
+                try {
+                    Thread.sleep(1000L);
+                } catch (final InterruptedException ie) {
+                }
+            } else {
+                try {
+                    runtime.removeShutdownHook(shutdownHook);
+                } catch (final IllegalStateException ise) {
+                    // happens when already shutting down
+                }
+
+                now = sdf.format(System.currentTimeMillis());
+                if (autoRestartNiFiRegistry) {
+                    final File statusFile = getStatusFile(defaultLogger);
+                    if (!statusFile.exists()) {
+                        defaultLogger.info("Status File no longer exists. Will not restart NiFi Registry ");
+                        return;
+                    }
+
+                    final File lockFile = getLockFile(defaultLogger);
+                    if (lockFile.exists()) {
+                        defaultLogger.info("A shutdown was initiated. Will not restart NiFi Registry ");
+                        return;
+                    }
+
+                    final boolean previouslyStarted = getNifiRegistryStarted();
+                    if (!previouslyStarted) {
+                        defaultLogger.info("NiFi Registry never started. Will not restart NiFi Registry ");
+                        return;
+                    } else {
+                        setNiFiRegistryStarted(false);
+                    }
+
+                    defaultLogger.warn("Apache NiFi Registry appears to have died. Restarting...");
+                    process = builder.start();
+                    handleLogging(process);
+
+                    pid = OSUtils.getProcessId(process, defaultLogger);
+                    if (pid == null) {
+                        cmdLogger.warn("Launched Apache NiFi Registry but could not obtain the Process ID");
+                    } else {
+                        nifiRegistryPid = pid;
+                        final Properties pidProperties = new Properties();
+                        pidProperties.setProperty(PID_KEY, String.valueOf(nifiRegistryPid));
+                        savePidProperties(pidProperties, defaultLogger);
+                        cmdLogger.info("Launched Apache NiFi Registry with Process ID " + pid);
+                    }
+
+                    shutdownHook = new ShutdownHook(process, this, secretKey, gracefulShutdownSeconds, loggingExecutor);
+                    runtime.addShutdownHook(shutdownHook);
+
+                    final boolean started = waitForStart();
+
+                    if (started) {
+                        defaultLogger.info("Successfully started Apache NiFi Registry {}", (pid == null ? "" : " with PID " + pid));
+                    } else {
+                        defaultLogger.error("Apache NiFi Registry does not appear to have started");
+                    }
+                } else {
+                    return;
+                }
+            }
+        }
+    }
+
+    private void handleLogging(final Process process) {
+        final Set<Future<?>> existingFutures = loggingFutures;
+        if (existingFutures != null) {
+            for (final Future<?> future : existingFutures) {
+                future.cancel(false);
+            }
+        }
+
+        final Future<?> stdOutFuture = loggingExecutor.submit(new Runnable() {
+            @Override
+            public void run() {
+                final Logger stdOutLogger = LoggerFactory.getLogger("org.apache.nifi.registry.StdOut");
+                final InputStream in = process.getInputStream();
+                try (final BufferedReader reader = new BufferedReader(new InputStreamReader(in))) {
+                    String line;
+                    while ((line = reader.readLine()) != null) {
+                        stdOutLogger.info(line);
+                    }
+                } catch (IOException e) {
+                    defaultLogger.error("Failed to read from NiFi Registry's Standard Out stream", e);
+                }
+            }
+        });
+
+        final Future<?> stdErrFuture = loggingExecutor.submit(new Runnable() {
+            @Override
+            public void run() {
+                final Logger stdErrLogger = LoggerFactory.getLogger("org.apache.nifi.registry.StdErr");
+                final InputStream in = process.getErrorStream();
+                try (final BufferedReader reader = new BufferedReader(new InputStreamReader(in))) {
+                    String line;
+                    while ((line = reader.readLine()) != null) {
+                        stdErrLogger.error(line);
+                    }
+                } catch (IOException e) {
+                    defaultLogger.error("Failed to read from NiFi Registry's Standard Error stream", e);
+                }
+            }
+        });
+
+        final Set<Future<?>> futures = new HashSet<>();
+        futures.add(stdOutFuture);
+        futures.add(stdErrFuture);
+        this.loggingFutures = futures;
+    }
+
+
+    private boolean isWindows() {
+        final String osName = System.getProperty("os.name");
+        return osName != null && osName.toLowerCase().contains("win");
+    }
+
+    private boolean waitForStart() {
+        lock.lock();
+        try {
+            final long startTime = System.nanoTime();
+
+            while (ccPort < 1) {
+                try {
+                    startupCondition.await(1, TimeUnit.SECONDS);
+                } catch (final InterruptedException ie) {
+                    return false;
+                }
+
+                final long waitNanos = System.nanoTime() - startTime;
+                final long waitSeconds = TimeUnit.NANOSECONDS.toSeconds(waitNanos);
+                if (waitSeconds > STARTUP_WAIT_SECONDS) {
+                    return false;
+                }
+            }
+        } finally {
+            lock.unlock();
+        }
+        return true;
+    }
+
+    private File getFile(final String filename, final File workingDir) {
+        File file = new File(filename);
+        if (!file.isAbsolute()) {
+            file = new File(workingDir, filename);
+        }
+
+        return file;
+    }
+
+    private String replaceNull(final String value, final String replacement) {
+        return (value == null) ? replacement : value;
+    }
+
+    void setAutoRestartNiFiRegistry(final boolean restart) {
+        this.autoRestartNiFiRegistry = restart;
+    }
+
+    void setNiFiRegistryCommandControlPort(final int port, final String secretKey) throws IOException {
+        this.ccPort = port;
+        this.secretKey = secretKey;
+
+        if (shutdownHook != null) {
+            shutdownHook.setSecretKey(secretKey);
+        }
+
+        final File statusFile = getStatusFile(defaultLogger);
+
+        final Properties nifiProps = new Properties();
+        if (nifiRegistryPid != -1) {
+            nifiProps.setProperty(PID_KEY, String.valueOf(nifiRegistryPid));
+        }
+        nifiProps.setProperty("port", String.valueOf(ccPort));
+        nifiProps.setProperty("secret.key", secretKey);
+
+        try {
+            savePidProperties(nifiProps, defaultLogger);
+        } catch (final IOException ioe) {
+            defaultLogger.warn("Apache NiFi Registry has started but failed to persist NiFi Registry Port information to {} due to {}", new Object[]{statusFile.getAbsolutePath(), ioe});
+        }
+
+        defaultLogger.info("Apache NiFi Registry now running and listening for Bootstrap requests on port {}", port);
+    }
+
+    int getNiFiRegistryCommandControlPort() {
+        return this.ccPort;
+    }
+
+    void setNiFiRegistryStarted(final boolean nifiStarted) {
+        startedLock.lock();
+        try {
+            this.nifiRegistryStarted = nifiStarted;
+        } finally {
+            startedLock.unlock();
+        }
+    }
+
+    boolean getNifiRegistryStarted() {
+        startedLock.lock();
+        try {
+            return nifiRegistryStarted;
+        } finally {
+            startedLock.unlock();
+        }
+    }
+
+    private static class Status {
+
+        private final Integer port;
+        private final String pid;
+
+        private final Boolean respondingToPing;
+        private final Boolean processRunning;
+
+        public Status(final Integer port, final String pid, final Boolean respondingToPing, final Boolean processRunning) {
+            this.port = port;
+            this.pid = pid;
+            this.respondingToPing = respondingToPing;
+            this.processRunning = processRunning;
+        }
+
+        public String getPid() {
+            return pid;
+        }
+
+        public Integer getPort() {
+            return port;
+        }
+
+        public boolean isRespondingToPing() {
+            return Boolean.TRUE.equals(respondingToPing);
+        }
+
+        public boolean isProcessRunning() {
+            return Boolean.TRUE.equals(processRunning);
+        }
+    }
+}

http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/6f26290d/nifi-registry-core/nifi-registry-bootstrap/src/main/java/org/apache/nifi/registry/bootstrap/ShutdownHook.java
----------------------------------------------------------------------
diff --git a/nifi-registry-core/nifi-registry-bootstrap/src/main/java/org/apache/nifi/registry/bootstrap/ShutdownHook.java b/nifi-registry-core/nifi-registry-bootstrap/src/main/java/org/apache/nifi/registry/bootstrap/ShutdownHook.java
new file mode 100644
index 0000000..ba370e6
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-bootstrap/src/main/java/org/apache/nifi/registry/bootstrap/ShutdownHook.java
@@ -0,0 +1,97 @@
+/*
+ * 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.bootstrap;
+
+import java.io.File;
+import java.io.IOException;
+import java.io.OutputStream;
+import java.net.Socket;
+import java.nio.charset.StandardCharsets;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.TimeUnit;
+
+public class ShutdownHook extends Thread {
+
+    private final Process nifiRegistryProcess;
+    private final RunNiFiRegistry runner;
+    private final int gracefulShutdownSeconds;
+    private final ExecutorService executor;
+
+    private volatile String secretKey;
+
+    public ShutdownHook(final Process nifiRegistryProcess, final RunNiFiRegistry runner, final String secretKey, final int gracefulShutdownSeconds, final ExecutorService executor) {
+        this.nifiRegistryProcess = nifiRegistryProcess;
+        this.runner = runner;
+        this.secretKey = secretKey;
+        this.gracefulShutdownSeconds = gracefulShutdownSeconds;
+        this.executor = executor;
+    }
+
+    void setSecretKey(final String secretKey) {
+        this.secretKey = secretKey;
+    }
+
+    @Override
+    public void run() {
+        executor.shutdown();
+        runner.setAutoRestartNiFiRegistry(false);
+        final int ccPort = runner.getNiFiRegistryCommandControlPort();
+        if (ccPort > 0) {
+            System.out.println("Initiating Shutdown of NiFi Registry...");
+
+            try {
+                final Socket socket = new Socket("localhost", ccPort);
+                final OutputStream out = socket.getOutputStream();
+                out.write(("SHUTDOWN " + secretKey + "\n").getBytes(StandardCharsets.UTF_8));
+                out.flush();
+
+                socket.close();
+            } catch (final IOException ioe) {
+                System.out.println("Failed to Shutdown NiFi Registry due to " + ioe);
+            }
+        }
+
+        runner.notifyStop();
+        System.out.println("Waiting for Apache NiFi Registry to finish shutting down...");
+        final long startWait = System.nanoTime();
+        while (RunNiFiRegistry.isAlive(nifiRegistryProcess)) {
+            final long waitNanos = System.nanoTime() - startWait;
+            final long waitSeconds = TimeUnit.NANOSECONDS.toSeconds(waitNanos);
+            if (waitSeconds >= gracefulShutdownSeconds && gracefulShutdownSeconds > 0) {
+                if (RunNiFiRegistry.isAlive(nifiRegistryProcess)) {
+                    System.out.println("NiFi Registry has not finished shutting down after " + gracefulShutdownSeconds + " seconds. Killing process.");
+                    nifiRegistryProcess.destroy();
+                }
+                break;
+            } else {
+                try {
+                    Thread.sleep(1000L);
+                } catch (final InterruptedException ie) {
+                }
+            }
+        }
+
+        try {
+            final File statusFile = runner.getStatusFile();
+            if (!statusFile.delete()) {
+                System.err.println("Failed to delete status file " + statusFile.getAbsolutePath() + "; this file should be cleaned up manually");
+            }
+        }catch (IOException ex){
+            System.err.println("Failed to retrieve status file " + ex);
+        }
+    }
+}

http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/6f26290d/nifi-registry-core/nifi-registry-bootstrap/src/main/java/org/apache/nifi/registry/bootstrap/exception/InvalidCommandException.java
----------------------------------------------------------------------
diff --git a/nifi-registry-core/nifi-registry-bootstrap/src/main/java/org/apache/nifi/registry/bootstrap/exception/InvalidCommandException.java b/nifi-registry-core/nifi-registry-bootstrap/src/main/java/org/apache/nifi/registry/bootstrap/exception/InvalidCommandException.java
new file mode 100644
index 0000000..6c51c08
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-bootstrap/src/main/java/org/apache/nifi/registry/bootstrap/exception/InvalidCommandException.java
@@ -0,0 +1,38 @@
+/*
+ * 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.bootstrap.exception;
+
+public class InvalidCommandException extends Exception {
+
+    private static final long serialVersionUID = 1L;
+
+    public InvalidCommandException() {
+        super();
+    }
+
+    public InvalidCommandException(final String message) {
+        super(message);
+    }
+
+    public InvalidCommandException(final Throwable t) {
+        super(t);
+    }
+
+    public InvalidCommandException(final String message, final Throwable t) {
+        super(message, t);
+    }
+}

http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/6f26290d/nifi-registry-core/nifi-registry-bootstrap/src/main/java/org/apache/nifi/registry/bootstrap/util/LimitingInputStream.java
----------------------------------------------------------------------
diff --git a/nifi-registry-core/nifi-registry-bootstrap/src/main/java/org/apache/nifi/registry/bootstrap/util/LimitingInputStream.java b/nifi-registry-core/nifi-registry-bootstrap/src/main/java/org/apache/nifi/registry/bootstrap/util/LimitingInputStream.java
new file mode 100644
index 0000000..79af09e
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-bootstrap/src/main/java/org/apache/nifi/registry/bootstrap/util/LimitingInputStream.java
@@ -0,0 +1,107 @@
+/*
+ * 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.bootstrap.util;
+
+import java.io.IOException;
+import java.io.InputStream;
+
+public class LimitingInputStream extends InputStream {
+
+    private final InputStream in;
+    private final long limit;
+    private long bytesRead = 0;
+
+    public LimitingInputStream(final InputStream in, final long limit) {
+        this.in = in;
+        this.limit = limit;
+    }
+
+    @Override
+    public int read() throws IOException {
+        if (bytesRead >= limit) {
+            return -1;
+        }
+
+        final int val = in.read();
+        if (val > -1) {
+            bytesRead++;
+        }
+        return val;
+    }
+
+    @Override
+    public int read(final byte[] b) throws IOException {
+        if (bytesRead >= limit) {
+            return -1;
+        }
+
+        final int maxToRead = (int) Math.min(b.length, limit - bytesRead);
+
+        final int val = in.read(b, 0, maxToRead);
+        if (val > 0) {
+            bytesRead += val;
+        }
+        return val;
+    }
+
+    @Override
+    public int read(byte[] b, int off, int len) throws IOException {
+        if (bytesRead >= limit) {
+            return -1;
+        }
+
+        final int maxToRead = (int) Math.min(len, limit - bytesRead);
+
+        final int val = in.read(b, off, maxToRead);
+        if (val > 0) {
+            bytesRead += val;
+        }
+        return val;
+    }
+
+    @Override
+    public long skip(final long n) throws IOException {
+        final long skipped = in.skip(Math.min(n, limit - bytesRead));
+        bytesRead += skipped;
+        return skipped;
+    }
+
+    @Override
+    public int available() throws IOException {
+        return in.available();
+    }
+
+    @Override
+    public void close() throws IOException {
+        in.close();
+    }
+
+    @Override
+    public void mark(int readlimit) {
+        in.mark(readlimit);
+    }
+
+    @Override
+    public boolean markSupported() {
+        return in.markSupported();
+    }
+
+    @Override
+    public void reset() throws IOException {
+        in.reset();
+    }
+}

http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/6f26290d/nifi-registry-core/nifi-registry-bootstrap/src/main/java/org/apache/nifi/registry/bootstrap/util/OSUtils.java
----------------------------------------------------------------------
diff --git a/nifi-registry-core/nifi-registry-bootstrap/src/main/java/org/apache/nifi/registry/bootstrap/util/OSUtils.java b/nifi-registry-core/nifi-registry-bootstrap/src/main/java/org/apache/nifi/registry/bootstrap/util/OSUtils.java
new file mode 100644
index 0000000..17c43df
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-bootstrap/src/main/java/org/apache/nifi/registry/bootstrap/util/OSUtils.java
@@ -0,0 +1,107 @@
+/*
+ * 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.bootstrap.util;
+
+import java.lang.reflect.Field;
+
+import org.slf4j.Logger;
+import com.sun.jna.Pointer;
+import com.sun.jna.platform.win32.Kernel32;
+import com.sun.jna.platform.win32.WinNT;
+
+/**
+ * OS specific utilities with generic method interfaces
+ */
+public final class OSUtils {
+    /**
+     * @param process NiFi Process Reference
+     * @param logger  Logger Reference for Debug
+     * @return        Returns pid or null in-case pid could not be determined
+     * This method takes {@link Process} and {@link Logger} and returns
+     * the platform specific ProcessId for Unix like systems, a.k.a <b>pid</b>
+     * In-case it fails to determine the pid, it will return Null.
+     * Purpose for the Logger is to log any interaction for debugging.
+     */
+    private static Long getUnicesPid(final Process process, final Logger logger) {
+        try {
+            final Class<?> procClass = process.getClass();
+            final Field pidField = procClass.getDeclaredField("pid");
+            pidField.setAccessible(true);
+            final Object pidObject = pidField.get(process);
+
+            logger.debug("PID Object = {}", pidObject);
+
+            if (pidObject instanceof Number) {
+                return ((Number) pidObject).longValue();
+            }
+            return null;
+        } catch (final IllegalAccessException | NoSuchFieldException nsfe) {
+            logger.debug("Could not find PID for child process due to {}", nsfe);
+            return null;
+        }
+    }
+
+    /**
+     * @param process NiFi Registry Process Reference
+     * @param logger  Logger Reference for Debug
+     * @return        Returns pid or null in-case pid could not be determined
+     * This method takes {@link Process} and {@link Logger} and returns
+     * the platform specific Handle for Win32 Systems, a.k.a <b>pid</b>
+     * In-case it fails to determine the pid, it will return Null.
+     * Purpose for the Logger is to log any interaction for debugging.
+     */
+    private static Long getWindowsProcessId(final Process process, final Logger logger) {
+        /* determine the pid on windows plattforms */
+        try {
+            Field f = process.getClass().getDeclaredField("handle");
+            f.setAccessible(true);
+            long handl = f.getLong(process);
+
+            Kernel32 kernel = Kernel32.INSTANCE;
+            WinNT.HANDLE handle = new WinNT.HANDLE();
+            handle.setPointer(Pointer.createConstant(handl));
+            int ret = kernel.GetProcessId(handle);
+            logger.debug("Detected pid: {}", ret);
+            return Long.valueOf(ret);
+        } catch (final IllegalAccessException | NoSuchFieldException nsfe) {
+            logger.debug("Could not find PID for child process due to {}", nsfe);
+        }
+        return null;
+    }
+
+    /**
+     * @param process NiFi Process Reference
+     * @param logger  Logger Reference for Debug
+     * @return        Returns pid or null in-case pid could not be determined
+     * This method takes {@link Process} and {@link Logger} and returns
+     * the platform specific ProcessId for Unix like systems or Handle for Win32 Systems, a.k.a <b>pid</b>
+     * In-case it fails to determine the pid, it will return Null.
+     * Purpose for the Logger is to log any interaction for debugging.
+     */
+    public static Long getProcessId(final Process process, final Logger logger) {
+        if (process.getClass().getName().equals("java.lang.UNIXProcess")) {
+            return getUnicesPid(process, logger);
+        } else if (process.getClass().getName().equals("java.lang.Win32Process")
+                || process.getClass().getName().equals("java.lang.ProcessImpl")) {
+            return getWindowsProcessId(process, logger);
+        }
+
+        return null;
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/6f26290d/nifi-registry-core/nifi-registry-client/pom.xml
----------------------------------------------------------------------
diff --git a/nifi-registry-core/nifi-registry-client/pom.xml b/nifi-registry-core/nifi-registry-client/pom.xml
new file mode 100644
index 0000000..1daa1fb
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-client/pom.xml
@@ -0,0 +1,61 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 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. -->
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+    <modelVersion>4.0.0</modelVersion>
+    <parent>
+        <groupId>org.apache.nifi.registry</groupId>
+        <artifactId>nifi-registry-core</artifactId>
+        <version>0.3.0-SNAPSHOT</version>
+    </parent>
+    
+    <artifactId>nifi-registry-client</artifactId>
+    <packaging>jar</packaging>
+
+    <dependencies>
+        <dependency>
+            <groupId>org.apache.nifi.registry</groupId>
+            <artifactId>nifi-registry-data-model</artifactId>
+            <version>0.3.0-SNAPSHOT</version>
+        </dependency>
+        <dependency>
+            <groupId>org.apache.nifi.registry</groupId>
+            <artifactId>nifi-registry-security-utils</artifactId>
+            <version>0.3.0-SNAPSHOT</version>
+        </dependency>
+        <dependency>
+            <groupId>org.glassfish.jersey.core</groupId>
+            <artifactId>jersey-client</artifactId>
+            <version>${jersey.version}</version>
+        </dependency>
+        <dependency>
+            <groupId>org.glassfish.jersey.media</groupId>
+            <artifactId>jersey-media-json-jackson</artifactId>
+            <version>${jersey.version}</version>
+        </dependency>
+        <dependency>
+            <groupId>org.glassfish.jersey.inject</groupId>
+            <artifactId>jersey-hk2</artifactId>
+            <version>${jersey.version}</version>
+        </dependency>
+        <dependency>
+            <groupId>org.glassfish.jersey.core</groupId>
+            <artifactId>jersey-common</artifactId>
+            <version>${jersey.version}</version>
+        </dependency>
+        <dependency>
+            <groupId>org.slf4j</groupId>
+            <artifactId>slf4j-simple</artifactId>
+            <version>${org.slf4j.version}</version>
+            <scope>test</scope>
+        </dependency>
+    </dependencies>
+</project>

http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/6f26290d/nifi-registry-core/nifi-registry-client/src/main/java/org/apache/nifi/registry/client/BucketClient.java
----------------------------------------------------------------------
diff --git a/nifi-registry-core/nifi-registry-client/src/main/java/org/apache/nifi/registry/client/BucketClient.java b/nifi-registry-core/nifi-registry-client/src/main/java/org/apache/nifi/registry/client/BucketClient.java
new file mode 100644
index 0000000..80f72bb
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-client/src/main/java/org/apache/nifi/registry/client/BucketClient.java
@@ -0,0 +1,76 @@
+/*
+ * 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.client;
+
+import org.apache.nifi.registry.bucket.Bucket;
+import org.apache.nifi.registry.field.Fields;
+
+import java.io.IOException;
+import java.util.List;
+
+/**
+ * Client for interacting with buckets.
+ */
+public interface BucketClient {
+
+    /**
+     * Creates the given bucket.
+     *
+     * @param bucket the bucket to create
+     * @return the created bucket with containing identifier that was generated
+     */
+    Bucket create(Bucket bucket) throws NiFiRegistryException, IOException;
+
+    /**
+     * Gets the bucket with the given id.
+     *
+     * @param bucketId the id of the bucket to retrieve
+     * @return the bucket with the given id
+     */
+    Bucket get(String bucketId) throws NiFiRegistryException, IOException;
+
+    /**
+     * Updates the given bucket. Only the name and description can be updated.
+     *
+     * @param bucket the bucket with updates, must contain the id
+     * @return the updated bucket
+     */
+    Bucket update(Bucket bucket) throws NiFiRegistryException, IOException;
+
+    /**
+     * Deletes the bucket with the given id.
+     *
+     * @param bucketId the id of the bucket to delete
+     * @return the deleted bucket
+     */
+    Bucket delete(String bucketId) throws NiFiRegistryException, IOException;
+
+    /**
+     * Gets the fields that can be used to sort/search buckets.
+     *
+     * @return the bucket fields
+     */
+    Fields getFields() throws NiFiRegistryException, IOException;
+
+    /**
+     * Gets all buckets.
+     *
+     * @return the list of all buckets
+     */
+    List<Bucket> getAll() throws NiFiRegistryException, IOException;
+
+}


[40/51] [partial] nifi-registry git commit: NIFIREG-201 Refactoring project structure to better isolate extensions

Posted by kd...@apache.org.
http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/6f26290d/nifi-registry-core/nifi-registry-framework/pom.xml
----------------------------------------------------------------------
diff --git a/nifi-registry-core/nifi-registry-framework/pom.xml b/nifi-registry-core/nifi-registry-framework/pom.xml
new file mode 100644
index 0000000..ca74ab1
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-framework/pom.xml
@@ -0,0 +1,366 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+  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.
+-->
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+    <parent>
+        <groupId>org.apache.nifi.registry</groupId>
+        <artifactId>nifi-registry-core</artifactId>
+        <version>0.3.0-SNAPSHOT</version>
+    </parent>
+    <modelVersion>4.0.0</modelVersion>
+    <artifactId>nifi-registry-framework</artifactId>
+    <packaging>jar</packaging>
+
+    <build>
+        <resources>
+            <resource>
+                <directory>src/main/resources</directory>
+            </resource>
+            <resource>
+                <directory>src/main/xsd</directory>
+            </resource>
+        </resources>
+        <plugins>
+            <plugin>
+                <groupId>org.codehaus.mojo</groupId>
+                <artifactId>jaxb2-maven-plugin</artifactId>
+                <executions>
+                    <execution>
+                        <id>providers</id>
+                        <goals>
+                            <goal>xjc</goal>
+                        </goals>
+                        <configuration>
+                            <sources>
+                                <source>src/main/xsd/providers.xsd</source>
+                            </sources>
+                            <packageName>org.apache.nifi.registry.provider.generated</packageName>
+                            <clearOutputDir>false</clearOutputDir>
+                        </configuration>
+                    </execution>
+                    <execution>
+                        <id>authorizers</id>
+                        <goals>
+                            <goal>xjc</goal>
+                        </goals>
+                        <configuration>
+                            <sources>
+                                <source>src/main/xsd/authorizers.xsd</source>
+                            </sources>
+                            <packageName>org.apache.nifi.registry.security.authorization.generated</packageName>
+                            <clearOutputDir>false</clearOutputDir>
+                        </configuration>
+                    </execution>
+                    <execution>
+                        <id>authorizations</id>
+                        <goals>
+                            <goal>xjc</goal>
+                        </goals>
+                        <configuration>
+                            <sources>
+                                <source>src/main/xsd/authorizations.xsd</source>
+                            </sources>
+                            <packageName>org.apache.nifi.registry.security.authorization.file.generated</packageName>
+                            <clearOutputDir>false</clearOutputDir>
+                        </configuration>
+                    </execution>
+                    <execution>
+                        <id>tenants</id>
+                        <goals>
+                            <goal>xjc</goal>
+                        </goals>
+                        <configuration>
+                            <sources>
+                                <source>src/main/xsd/tenants.xsd</source>
+                            </sources>
+                            <packageName>org.apache.nifi.registry.security.authorization.file.tenants.generated</packageName>
+                            <clearOutputDir>false</clearOutputDir>
+                        </configuration>
+                    </execution>
+                    <execution>
+                        <id>identity-providers</id>
+                        <goals>
+                            <goal>xjc</goal>
+                        </goals>
+                        <configuration>
+                            <sources>
+                                <source>src/main/xsd/identity-providers.xsd</source>
+                            </sources>
+                            <packageName>org.apache.nifi.registry.security.authentication.generated</packageName>
+                            <clearOutputDir>false</clearOutputDir>
+                        </configuration>
+                    </execution>
+                </executions>
+            </plugin>
+            <plugin>
+                <groupId>org.apache.maven.plugins</groupId>
+                <artifactId>maven-checkstyle-plugin</artifactId>
+                <configuration>
+                    <excludes>**/generated/*.java</excludes>
+                </configuration>
+            </plugin>
+            <plugin>
+                <groupId>org.codehaus.gmavenplus</groupId>
+                <artifactId>gmavenplus-plugin</artifactId>
+                <version>1.5</version>
+                <executions>
+                    <execution>
+                        <goals>
+                            <goal>addTestSources</goal>
+                            <goal>testCompile</goal>
+                        </goals>
+                    </execution>
+                </executions>
+            </plugin>
+            <plugin>
+                <groupId>org.apache.maven.plugins</groupId>
+                <artifactId>maven-compiler-plugin</artifactId>
+                <version>3.6.1</version>
+                <configuration>
+                    <source>1.8</source>
+                    <target>1.8</target>
+                </configuration>
+            </plugin>
+            <plugin>
+                <groupId>org.apache.rat</groupId>
+                <artifactId>apache-rat-plugin</artifactId>
+                <configuration>
+                    <excludes combine.children="append">
+                        <exclude>src/test/resources/serialization/json/no-version.snapshot</exclude>
+                        <exclude>src/test/resources/serialization/json/non-integer-version.snapshot</exclude>
+                        <exclude>src/test/resources/serialization/ver1.snapshot</exclude>
+                        <exclude>src/test/resources/serialization/ver2.snapshot</exclude>
+                        <exclude>src/test/resources/serialization/ver3.snapshot</exclude>
+                    </excludes>
+                </configuration>
+            </plugin>
+        </plugins>
+    </build>
+
+    <dependencies>
+        <dependency>
+            <groupId>org.apache.nifi.registry</groupId>
+            <artifactId>nifi-registry-data-model</artifactId>
+            <version>0.3.0-SNAPSHOT</version>
+        </dependency>
+        <dependency>
+            <groupId>org.apache.nifi.registry</groupId>
+            <artifactId>nifi-registry-properties</artifactId>
+            <version>0.3.0-SNAPSHOT</version>
+        </dependency>
+        <dependency>
+            <groupId>org.apache.nifi.registry</groupId>
+            <artifactId>nifi-registry-utils</artifactId>
+            <version>0.3.0-SNAPSHOT</version>
+            <scope>provided</scope> <!-- will be in lib dir -->
+        </dependency>
+        <dependency>
+            <groupId>org.apache.nifi.registry</groupId>
+            <artifactId>nifi-registry-provider-api</artifactId>
+            <scope>provided</scope> <!-- will be in lib dir -->
+        </dependency>
+        <dependency>
+            <groupId>org.apache.nifi.registry</groupId>
+            <artifactId>nifi-registry-security-api</artifactId>
+            <scope>provided</scope> <!-- will be in lib dir -->
+        </dependency>
+        <dependency>
+            <groupId>org.apache.nifi.registry</groupId>
+            <artifactId>nifi-registry-security-utils</artifactId>
+            <version>0.3.0-SNAPSHOT</version>
+        </dependency>
+        <dependency>
+            <groupId>javax.servlet</groupId>
+            <artifactId>javax.servlet-api</artifactId>
+            <version>3.1.0</version>
+            <scope>provided</scope>
+        </dependency>
+        <dependency>
+            <groupId>org.springframework.boot</groupId>
+            <artifactId>spring-boot-starter-security</artifactId>
+            <version>${spring.boot.version}</version>
+        </dependency>
+        <dependency>
+            <groupId>org.springframework.security</groupId>
+            <artifactId>spring-security-ldap</artifactId>
+            <version>${spring.security.version}</version>
+            <exclusions>
+                <exclusion>
+                    <groupId>org.springframework.security</groupId>
+                    <artifactId>spring-security-core</artifactId>
+                </exclusion>
+                <exclusion>
+                    <groupId>org.springframework</groupId>
+                    <artifactId>spring-beans</artifactId>
+                </exclusion>
+                <exclusion>
+                    <groupId>org.springframework</groupId>
+                    <artifactId>spring-context</artifactId>
+                </exclusion>
+                <exclusion>
+                    <groupId>org.springframework</groupId>
+                    <artifactId>spring-core</artifactId>
+                </exclusion>
+                <exclusion>
+                    <groupId>org.springframework</groupId>
+                    <artifactId>spring-tx</artifactId>
+                </exclusion>
+                <exclusion>
+                    <groupId>commons-logging</groupId>
+                    <artifactId>commons-logging</artifactId>
+                </exclusion>
+            </exclusions>
+        </dependency>
+        <dependency>
+            <groupId>org.bouncycastle</groupId>
+            <artifactId>bcprov-jdk15on</artifactId>
+            <version>1.55</version>
+        </dependency>
+        <dependency>
+            <groupId>commons-io</groupId>
+            <artifactId>commons-io</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>org.hibernate</groupId>
+            <artifactId>hibernate-validator</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>org.glassfish</groupId>
+            <artifactId>javax.el</artifactId>
+        </dependency>
+        <!-- We're using spring-boot-starter-data-jpa to bring in spring-data-jdbc and a few other dependencies, but
+            we aren't actually using any JPA dependencies, this also brings in the standard Spring dependencies for the backend -->
+        <dependency>
+            <groupId>org.springframework.boot</groupId>
+            <artifactId>spring-boot-starter-data-jpa</artifactId>
+            <version>${spring.boot.version}</version>
+            <exclusions>
+                <exclusion>
+                    <groupId>org.apache.tomcat</groupId>
+                    <artifactId>tomcat-jdbc</artifactId>
+                </exclusion>
+                <exclusion>
+                    <groupId>org.springframework.data</groupId>
+                    <artifactId>spring-data-jpa</artifactId>
+                </exclusion>
+                <exclusion>
+                    <groupId>org.hibernate</groupId>
+                    <artifactId>hibernate-entitymanager</artifactId>
+                </exclusion>
+                <exclusion>
+                    <groupId>org.hibernate</groupId>
+                    <artifactId>hibernate-core</artifactId>
+                </exclusion>
+            </exclusions>
+        </dependency>
+        <dependency>
+            <groupId>org.flywaydb</groupId>
+            <artifactId>flyway-core</artifactId>
+            <version>${flyway.version}</version>
+        </dependency>
+        <dependency>
+            <groupId>com.h2database</groupId>
+            <artifactId>h2</artifactId>
+            <version>1.4.196</version>
+        </dependency>
+        <dependency>
+            <groupId>org.eclipse.jgit</groupId>
+            <artifactId>org.eclipse.jgit</artifactId>
+            <version>4.11.0.201803080745-r</version>
+        </dependency>
+        <dependency>
+            <groupId>com.jcraft</groupId>
+            <artifactId>jsch</artifactId>
+            <version>0.1.54</version>
+        </dependency>
+        <dependency>
+            <groupId>org.yaml</groupId>
+            <artifactId>snakeyaml</artifactId>
+            <version>1.20</version>
+        </dependency>
+        <dependency>
+            <groupId>com.fasterxml.jackson.core</groupId>
+            <artifactId>jackson-databind</artifactId>
+            <version>${jackson.version}</version>
+        </dependency>
+        <dependency>
+            <groupId>com.fasterxml.jackson.core</groupId>
+            <artifactId>jackson-core</artifactId>
+            <version>${jackson.version}</version>
+        </dependency>
+        <dependency>
+            <groupId>com.fasterxml.jackson.module</groupId>
+            <artifactId>jackson-module-jaxb-annotations</artifactId>
+            <version>${jackson.version}</version>
+        </dependency>
+        <!-- Test Dependencies -->
+        <dependency>
+            <groupId>org.springframework.boot</groupId>
+            <artifactId>spring-boot-starter-test</artifactId>
+            <version>${spring.boot.version}</version>
+            <scope>test</scope>
+        </dependency>
+        <dependency>
+            <groupId>org.flywaydb.flyway-test-extensions</groupId>
+            <artifactId>flyway-spring-test</artifactId>
+            <version>${flyway.version}</version>
+            <scope>test</scope>
+            <exclusions>
+                <exclusion>
+                    <groupId>org.springframework</groupId>
+                    <artifactId>spring-context</artifactId>
+                </exclusion>
+                <exclusion>
+                    <groupId>org.springframework</groupId>
+                    <artifactId>spring-jdbc</artifactId>
+                </exclusion>
+            </exclusions>
+        </dependency>
+        <dependency>
+            <groupId>org.mockito</groupId>
+            <artifactId>mockito-core</artifactId>
+            <scope>test</scope>
+        </dependency>
+        <dependency>
+            <groupId>org.spockframework</groupId>
+            <artifactId>spock-core</artifactId>
+            <version>1.0-groovy-2.4</version>
+            <scope>test</scope>
+        </dependency>
+        <dependency>
+            <groupId>org.codehaus.groovy</groupId>
+            <artifactId>groovy-all</artifactId>
+            <version>2.4.12</version>
+            <scope>test</scope>
+        </dependency>
+        <dependency>
+            <groupId>cglib</groupId>
+            <artifactId>cglib-nodep</artifactId>
+            <version>2.2.2</version>
+            <scope>test</scope>
+        </dependency>
+        <dependency>
+            <groupId>org.apache.directory.server</groupId>
+            <artifactId>apacheds-all</artifactId>
+            <version>2.0.0-M24</version>
+            <scope>test</scope>
+	</dependency>
+	<dependency>
+            <groupId>org.apache.nifi.registry</groupId>
+            <artifactId>nifi-registry-flow-diff</artifactId>
+            <version>0.3.0-SNAPSHOT</version>
+        </dependency>
+    </dependencies>
+</project>

http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/6f26290d/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/db/CustomFlywayMigrationStrategy.java
----------------------------------------------------------------------
diff --git a/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/db/CustomFlywayMigrationStrategy.java b/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/db/CustomFlywayMigrationStrategy.java
new file mode 100644
index 0000000..7748acf
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/db/CustomFlywayMigrationStrategy.java
@@ -0,0 +1,157 @@
+/*
+ * 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.db;
+
+import org.apache.commons.lang3.StringUtils;
+import org.apache.nifi.registry.db.migration.BucketEntityV1;
+import org.apache.nifi.registry.db.migration.FlowEntityV1;
+import org.apache.nifi.registry.db.migration.FlowSnapshotEntityV1;
+import org.apache.nifi.registry.db.migration.LegacyDataSourceFactory;
+import org.apache.nifi.registry.db.migration.LegacyDatabaseService;
+import org.apache.nifi.registry.db.migration.LegacyEntityMapper;
+import org.apache.nifi.registry.properties.NiFiRegistryProperties;
+import org.apache.nifi.registry.service.MetadataService;
+import org.flywaydb.core.Flyway;
+import org.flywaydb.core.api.FlywayException;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.boot.autoconfigure.flyway.FlywayMigrationStrategy;
+import org.springframework.jdbc.core.JdbcTemplate;
+import org.springframework.stereotype.Component;
+
+import javax.sql.DataSource;
+import java.io.File;
+import java.sql.Connection;
+import java.sql.ResultSet;
+import java.sql.SQLException;
+import java.util.List;
+
+/**
+ * Custom Flyway migration strategy that lets us perform data migration from the original database used in the
+ * 0.1.0 release, to the new database. The data migration will be triggered when it is determined that new database
+ * is brand new AND the legacy DB properties are specified. If the primary database already contains the 'BUCKET' table,
+ * or if the legacy database properties are not specified, then no data migration is performed.
+ */
+@Component
+public class CustomFlywayMigrationStrategy implements FlywayMigrationStrategy {
+
+    private static final Logger LOGGER = LoggerFactory.getLogger(CustomFlywayMigrationStrategy.class);
+
+    private NiFiRegistryProperties properties;
+
+    @Autowired
+    public CustomFlywayMigrationStrategy(final NiFiRegistryProperties properties) {
+        this.properties = properties;
+    }
+
+    @Override
+    public void migrate(Flyway flyway) {
+        final boolean newDatabase = isNewDatabase(flyway.getDataSource());
+        if (newDatabase) {
+            LOGGER.info("First time initializing database...");
+        } else {
+            LOGGER.info("Found existing database...");
+        }
+
+        boolean existingLegacyDatabase = false;
+        if (!StringUtils.isBlank(properties.getLegacyDatabaseDirectory())) {
+            LOGGER.info("Found legacy database properties...");
+
+            final File legacyDatabaseFile = new File(properties.getLegacyDatabaseDirectory(), "nifi-registry.mv.db");
+            if (legacyDatabaseFile.exists()) {
+                LOGGER.info("Found legacy database file...");
+                existingLegacyDatabase = true;
+            } else {
+                LOGGER.info("Did not find legacy database file...");
+                existingLegacyDatabase = false;
+            }
+        }
+
+        // If newDatabase is true, then we need to run the Flyway migration first to create all the tables, then the data migration
+        // If newDatabase is false, then we need to run the Flyway migration to run any schema updates, but no data migration
+
+        flyway.migrate();
+
+        if (newDatabase && existingLegacyDatabase) {
+            final LegacyDataSourceFactory legacyDataSourceFactory = new LegacyDataSourceFactory(properties);
+            final DataSource legacyDataSource = legacyDataSourceFactory.getDataSource();
+            final DataSource primaryDataSource = flyway.getDataSource();
+            migrateData(legacyDataSource, primaryDataSource);
+        }
+    }
+
+    /**
+     * Determines if the database represented by this data source is being initialized for the first time based on
+     * whether or not the table named 'BUCKET' or 'bucket' already exists.
+     *
+     * @param dataSource the data source
+     * @return true if the database has never been initialized before, false otherwise
+     */
+    private boolean isNewDatabase(final DataSource dataSource) {
+        try (final Connection connection = dataSource.getConnection();
+             final ResultSet rsUpper = connection.getMetaData().getTables(null, null, "BUCKET", null);
+             final ResultSet rsLower = connection.getMetaData().getTables(null, null, "bucket", null)) {
+            return !rsUpper.next() && !rsLower.next();
+        } catch (SQLException e) {
+            LOGGER.error(e.getMessage(), e);
+            throw new FlywayException("Unable to obtain connection from Flyway DataSource", e);
+        }
+    }
+
+    /**
+     * Transfers all data from the source to the destination.
+     *
+     * @param source the legacy H2 DataSource
+     * @param dest the new destination DataSource
+     */
+    private void migrateData(final DataSource source, final DataSource dest) {
+        final LegacyDatabaseService legacyDatabaseService = new LegacyDatabaseService(source);
+
+        final JdbcTemplate destJdbcTemplate = new JdbcTemplate(dest);
+        final MetadataService destMetadataService = new DatabaseMetadataService(destJdbcTemplate);
+
+        LOGGER.info("Migrating data from legacy database to new new database...");
+
+        // Migrate buckets
+        final List<BucketEntityV1> sourceBuckets = legacyDatabaseService.getAllBuckets();
+        LOGGER.info("Migrating {} buckets..", new Object[]{sourceBuckets.size()});
+
+        sourceBuckets.stream()
+                .map(b -> LegacyEntityMapper.createBucketEntity(b))
+                .forEach(b -> destMetadataService.createBucket(b));
+
+        // Migrate flows
+        final List<FlowEntityV1> sourceFlows = legacyDatabaseService.getAllFlows();
+        LOGGER.info("Migrating {} flows..", new Object[]{sourceFlows.size()});
+
+        sourceFlows.stream()
+                .map(f -> LegacyEntityMapper.createFlowEntity(f))
+                .forEach(f -> destMetadataService.createFlow(f));
+
+        // Migrate flow snapshots
+        final List<FlowSnapshotEntityV1> sourceSnapshots = legacyDatabaseService.getAllFlowSnapshots();
+        LOGGER.info("Migrating {} flow snapshots..", new Object[]{sourceSnapshots.size()});
+
+        sourceSnapshots.stream()
+                .map(fs -> LegacyEntityMapper.createFlowSnapshotEntity(fs))
+                .forEach(fs -> destMetadataService.createFlowSnapshot(fs));
+
+        LOGGER.info("Data migration complete!");
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/6f26290d/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/db/DataSourceFactory.java
----------------------------------------------------------------------
diff --git a/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/db/DataSourceFactory.java b/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/db/DataSourceFactory.java
new file mode 100644
index 0000000..29c132e
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/db/DataSourceFactory.java
@@ -0,0 +1,97 @@
+/*
+ * 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.db;
+
+import com.zaxxer.hikari.HikariDataSource;
+import org.apache.commons.lang3.StringUtils;
+import org.apache.nifi.registry.properties.NiFiRegistryProperties;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.boot.jdbc.DataSourceBuilder;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.context.annotation.Primary;
+
+import javax.sql.DataSource;
+
+/**
+ * Overriding Spring Boot's normal automatic creation of a DataSource in order to use the properties
+ * from NiFiRegistryProperties rather than the standard application.properties/yaml.
+ */
+@Configuration
+public class DataSourceFactory {
+
+    private static final Logger LOGGER = LoggerFactory.getLogger(DataSourceFactory.class);
+
+    private final NiFiRegistryProperties properties;
+
+    private DataSource dataSource;
+
+    @Autowired
+    public DataSourceFactory(final NiFiRegistryProperties properties) {
+        this.properties = properties;
+    }
+
+    @Bean
+    @Primary
+    public DataSource getDataSource() {
+        if (dataSource == null) {
+            dataSource = createDataSource();
+        }
+
+        return dataSource;
+    }
+
+    private DataSource createDataSource() {
+        final String databaseUrl = properties.getDatabaseUrl();
+        if (StringUtils.isBlank(databaseUrl)) {
+            throw new IllegalStateException(NiFiRegistryProperties.DATABASE_URL + " is required");
+        }
+
+        final String databaseDriver = properties.getDatabaseDriverClassName();
+        if (StringUtils.isBlank(databaseDriver)) {
+            throw new IllegalStateException(NiFiRegistryProperties.DATABASE_DRIVER_CLASS_NAME + " is required");
+        }
+
+        final String databaseUsername = properties.getDatabaseUsername();
+        if (StringUtils.isBlank(databaseUsername)) {
+            throw new IllegalStateException(NiFiRegistryProperties.DATABASE_USERNAME + " is required");
+        }
+
+        String databasePassword = properties.getDatabasePassword();
+        if (StringUtils.isBlank(databasePassword)) {
+            throw new IllegalStateException(NiFiRegistryProperties.DATABASE_PASSWORD + " is required");
+        }
+
+        final DataSource dataSource = DataSourceBuilder
+                .create()
+                .url(databaseUrl)
+                .driverClassName(databaseDriver)
+                .username(databaseUsername)
+                .password(databasePassword)
+                .build();
+
+        if (dataSource instanceof HikariDataSource) {
+            LOGGER.info("Setting maximum pool size on HikariDataSource to {}", new Object[]{properties.getDatabaseMaxConnections()});
+            ((HikariDataSource)dataSource).setMaximumPoolSize(properties.getDatabaseMaxConnections());
+        }
+
+        return dataSource;
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/6f26290d/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/db/DatabaseKeyService.java
----------------------------------------------------------------------
diff --git a/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/db/DatabaseKeyService.java b/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/db/DatabaseKeyService.java
new file mode 100644
index 0000000..1d4f721
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/db/DatabaseKeyService.java
@@ -0,0 +1,136 @@
+/*
+ * 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.db;
+
+import org.apache.nifi.registry.db.entity.KeyEntity;
+import org.apache.nifi.registry.db.mapper.KeyEntityRowMapper;
+import org.apache.nifi.registry.security.key.Key;
+import org.apache.nifi.registry.security.key.KeyService;
+import org.apache.nifi.registry.service.DataModelMapper;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.dao.EmptyResultDataAccessException;
+import org.springframework.jdbc.core.JdbcTemplate;
+import org.springframework.stereotype.Service;
+
+import java.util.UUID;
+import java.util.concurrent.locks.Lock;
+import java.util.concurrent.locks.ReentrantReadWriteLock;
+
+@Service
+public class DatabaseKeyService implements KeyService {
+
+    private static final Logger logger = LoggerFactory.getLogger(DatabaseKeyService.class);
+
+    private final ReentrantReadWriteLock lock = new ReentrantReadWriteLock();
+    private final Lock readLock = lock.readLock();
+    private final Lock writeLock = lock.writeLock();
+
+    private JdbcTemplate jdbcTemplate;
+
+    @Autowired
+    public DatabaseKeyService(final JdbcTemplate jdbcTemplate) {
+        this.jdbcTemplate = jdbcTemplate;
+    }
+
+    @Override
+    public Key getKey(String id) {
+        if (id == null) {
+            throw new IllegalArgumentException("Id cannot be null");
+        }
+
+        Key key = null;
+        readLock.lock();
+        try {
+            final String sql = "SELECT * FROM signing_key WHERE id = ?";
+
+            KeyEntity keyEntity;
+            try {
+                keyEntity = jdbcTemplate.queryForObject(sql, new KeyEntityRowMapper(), id);
+            } catch (EmptyResultDataAccessException e) {
+                keyEntity = null;
+            }
+
+            if (keyEntity != null) {
+                key = DataModelMapper.map(keyEntity);
+            } else {
+                logger.debug("No signing key found with id='" + id + "'");
+            }
+        } finally {
+            readLock.unlock();
+        }
+        return key;
+    }
+
+    @Override
+    public Key getOrCreateKey(String tenantIdentity) {
+        if (tenantIdentity == null) {
+            throw new IllegalArgumentException("Identity cannot be null");
+        }
+
+        Key key;
+        writeLock.lock();
+        try {
+            final String selectSql = "SELECT * FROM signing_key WHERE tenant_identity = ?";
+
+            KeyEntity existingKeyEntity;
+            try {
+                existingKeyEntity = jdbcTemplate.queryForObject(selectSql, new KeyEntityRowMapper(), tenantIdentity);
+            } catch (EmptyResultDataAccessException e) {
+                existingKeyEntity = null;
+            }
+
+            if (existingKeyEntity == null) {
+                logger.debug("No key found with identity='" + tenantIdentity + "'. Creating new key.");
+
+                final KeyEntity newKeyEntity = new KeyEntity();
+                newKeyEntity.setId(UUID.randomUUID().toString());
+                newKeyEntity.setTenantIdentity(tenantIdentity);
+                newKeyEntity.setKeyValue(UUID.randomUUID().toString());
+
+                final String insertSql = "INSERT INTO signing_key (ID, TENANT_IDENTITY, KEY_VALUE) VALUES (?, ?, ?)";
+                jdbcTemplate.update(insertSql, newKeyEntity.getId(), newKeyEntity.getTenantIdentity(), newKeyEntity.getKeyValue());
+
+                key = DataModelMapper.map(newKeyEntity);
+            } else {
+                key = DataModelMapper.map(existingKeyEntity);
+            }
+        } finally {
+            writeLock.unlock();
+        }
+        return key;
+    }
+
+    @Override
+    public void deleteKey(String tenantIdentity) {
+        if (tenantIdentity == null) {
+            throw new IllegalArgumentException("Identity cannot be null");
+        }
+
+        writeLock.lock();
+        try {
+            logger.debug("Deleting key with identity='" + tenantIdentity + "'.");
+            final String deleteSql = "DELETE FROM signing_key WHERE tenant_identity = ?";
+            jdbcTemplate.update(deleteSql, tenantIdentity);
+        } finally {
+            writeLock.unlock();
+        }
+
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/6f26290d/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/db/DatabaseMetadataService.java
----------------------------------------------------------------------
diff --git a/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/db/DatabaseMetadataService.java b/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/db/DatabaseMetadataService.java
new file mode 100644
index 0000000..4d32790
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/db/DatabaseMetadataService.java
@@ -0,0 +1,441 @@
+/*
+ * 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.db;
+
+import org.apache.nifi.registry.db.entity.BucketEntity;
+import org.apache.nifi.registry.db.entity.BucketItemEntity;
+import org.apache.nifi.registry.db.entity.BucketItemEntityType;
+import org.apache.nifi.registry.db.entity.FlowEntity;
+import org.apache.nifi.registry.db.entity.FlowSnapshotEntity;
+import org.apache.nifi.registry.db.mapper.BucketEntityRowMapper;
+import org.apache.nifi.registry.db.mapper.BucketItemEntityRowMapper;
+import org.apache.nifi.registry.db.mapper.FlowEntityRowMapper;
+import org.apache.nifi.registry.db.mapper.FlowSnapshotEntityRowMapper;
+import org.apache.nifi.registry.service.MetadataService;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.dao.EmptyResultDataAccessException;
+import org.springframework.jdbc.core.JdbcTemplate;
+import org.springframework.stereotype.Repository;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Date;
+import java.util.HashMap;
+import java.util.LinkedHashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+@Repository
+public class DatabaseMetadataService implements MetadataService {
+
+    private final JdbcTemplate jdbcTemplate;
+
+    @Autowired
+    public DatabaseMetadataService(final JdbcTemplate jdbcTemplate) {
+        this.jdbcTemplate = jdbcTemplate;
+    }
+
+    //----------------- Buckets ---------------------------------
+
+    @Override
+    public BucketEntity createBucket(final BucketEntity b) {
+        final String sql = "INSERT INTO bucket (ID, NAME, DESCRIPTION, CREATED) VALUES (?, ?, ?, ?)";
+        jdbcTemplate.update(sql, b.getId(), b.getName(), b.getDescription(), b.getCreated());
+        return b;
+    }
+
+    @Override
+    public BucketEntity getBucketById(final String bucketIdentifier) {
+        final String sql = "SELECT * FROM bucket WHERE id = ?";
+        try {
+            return jdbcTemplate.queryForObject(sql, new BucketEntityRowMapper(), bucketIdentifier);
+        } catch (EmptyResultDataAccessException e) {
+            return null;
+        }
+    }
+
+    @Override
+    public List<BucketEntity> getBucketsByName(final String name) {
+        final String sql = "SELECT * FROM bucket WHERE name = ? ORDER BY name ASC";
+        return jdbcTemplate.query(sql, new Object[] {name} , new BucketEntityRowMapper());
+    }
+
+    @Override
+    public BucketEntity updateBucket(final BucketEntity bucket) {
+        final String sql = "UPDATE bucket SET name = ?, description = ? WHERE id = ?";
+        jdbcTemplate.update(sql, bucket.getName(), bucket.getDescription(), bucket.getId());
+        return bucket;
+    }
+
+    @Override
+    public void deleteBucket(final BucketEntity bucket) {
+        final String snapshotDeleteSql = "DELETE FROM flow_snapshot WHERE flow_id IN ( " +
+                    "SELECT f.id FROM flow f, bucket_item item WHERE f.id = item.id AND item.bucket_id = ?" +
+                ")";
+        jdbcTemplate.update(snapshotDeleteSql, bucket.getId());
+
+        final String flowDeleteSql = "DELETE FROM flow WHERE id IN ( " +
+                    "SELECT f.id FROM flow f, bucket_item item WHERE f.id = item.id AND item.bucket_id = ?" +
+                ")";
+        jdbcTemplate.update(flowDeleteSql, bucket.getId());
+
+        final String itemDeleteSql = "DELETE FROM bucket_item WHERE bucket_id = ?";
+        jdbcTemplate.update(itemDeleteSql, bucket.getId());
+
+        final String sql = "DELETE FROM bucket WHERE id = ?";
+        jdbcTemplate.update(sql, bucket.getId());
+    }
+
+    @Override
+    public List<BucketEntity> getBuckets(final Set<String> bucketIds) {
+        if (bucketIds == null || bucketIds.isEmpty()) {
+            return Collections.emptyList();
+        }
+
+        final StringBuilder sqlBuilder = new StringBuilder("SELECT * FROM bucket WHERE id IN (");
+        for (int i=0; i < bucketIds.size(); i++) {
+            if (i > 0) {
+                sqlBuilder.append(", ");
+            }
+            sqlBuilder.append("?");
+        }
+        sqlBuilder.append(") ");
+        sqlBuilder.append("ORDER BY name ASC");
+
+        return jdbcTemplate.query(sqlBuilder.toString(), bucketIds.toArray(), new BucketEntityRowMapper());
+    }
+
+    @Override
+    public List<BucketEntity> getAllBuckets() {
+        final String sql = "SELECT * FROM bucket ORDER BY name ASC";
+        return jdbcTemplate.query(sql, new BucketEntityRowMapper());
+    }
+
+    //----------------- BucketItems ---------------------------------
+
+    @Override
+    public List<BucketItemEntity> getBucketItems(final String bucketIdentifier) {
+        final String sql =
+                "SELECT " +
+                    "item.id as ID, " +
+                    "item.name as NAME, " +
+                    "item.description as DESCRIPTION, " +
+                    "item.created as CREATED, " +
+                    "item.modified as MODIFIED, " +
+                    "item.item_type as ITEM_TYPE, " +
+                    "b.id as BUCKET_ID, " +
+                    "b.name as BUCKET_NAME " +
+                "FROM " +
+                        "bucket_item item, bucket b " +
+                "WHERE " +
+                        "item.bucket_id = b.id " +
+                "AND " +
+                        "item.bucket_id = ?";
+
+        final List<BucketItemEntity> items = jdbcTemplate.query(sql, new Object[] { bucketIdentifier }, new BucketItemEntityRowMapper());
+        return getItemsWithCounts(items);
+    }
+
+    @Override
+    public List<BucketItemEntity> getBucketItems(final Set<String> bucketIds) {
+        if (bucketIds == null || bucketIds.isEmpty()) {
+            return Collections.emptyList();
+        }
+
+        final StringBuilder sqlBuilder = new StringBuilder(
+                "SELECT " +
+                        "item.id as ID, " +
+                        "item.name as NAME, " +
+                        "item.description as DESCRIPTION, " +
+                        "item.created as CREATED, " +
+                        "item.modified as MODIFIED, " +
+                        "item.item_type as ITEM_TYPE, " +
+                        "b.id as BUCKET_ID, " +
+                        "b.name as BUCKET_NAME " +
+                "FROM " +
+                        "bucket_item item, bucket b " +
+                "WHERE " +
+                        "item.bucket_id = b.id " +
+                "AND " +
+                        "item.bucket_id IN (");
+
+        for (int i=0; i < bucketIds.size(); i++) {
+            if (i > 0) {
+                sqlBuilder.append(", ");
+            }
+            sqlBuilder.append("?");
+        }
+        sqlBuilder.append(")");
+
+        final List<BucketItemEntity> items = jdbcTemplate.query(sqlBuilder.toString(), bucketIds.toArray(), new BucketItemEntityRowMapper());
+        return getItemsWithCounts(items);
+    }
+
+    private List<BucketItemEntity> getItemsWithCounts(final Iterable<BucketItemEntity> items) {
+        final Map<String,Long> snapshotCounts = getFlowSnapshotCounts();
+
+        final List<BucketItemEntity> itemWithCounts = new ArrayList<>();
+        for (final BucketItemEntity item : items) {
+            if (item.getType() == BucketItemEntityType.FLOW) {
+                final Long snapshotCount = snapshotCounts.get(item.getId());
+                if (snapshotCount != null) {
+                    final FlowEntity flowEntity = (FlowEntity) item;
+                    flowEntity.setSnapshotCount(snapshotCount);
+                }
+            }
+
+            itemWithCounts.add(item);
+        }
+
+        return itemWithCounts;
+    }
+
+    private Map<String,Long> getFlowSnapshotCounts() {
+        final String sql = "SELECT flow_id, count(*) FROM flow_snapshot GROUP BY flow_id";
+
+        final Map<String,Long> results = new HashMap<>();
+        jdbcTemplate.query(sql, (rs) -> {
+            results.put(rs.getString(1), rs.getLong(2));
+        });
+        return results;
+    }
+
+    private Long getFlowSnapshotCount(final String flowIdentifier) {
+        final String sql = "SELECT count(*) FROM flow_snapshot WHERE flow_id = ?";
+
+        return jdbcTemplate.queryForObject(sql, new Object[] {flowIdentifier}, (rs, num) -> {
+            return rs.getLong(1);
+        });
+    }
+
+    //----------------- Flows ---------------------------------
+
+    @Override
+    public FlowEntity createFlow(final FlowEntity flow) {
+        final String itemSql = "INSERT INTO bucket_item (ID, NAME, DESCRIPTION, CREATED, MODIFIED, ITEM_TYPE, BUCKET_ID) VALUES (?, ?, ?, ?, ?, ?, ?)";
+
+        jdbcTemplate.update(itemSql,
+                flow.getId(),
+                flow.getName(),
+                flow.getDescription(),
+                flow.getCreated(),
+                flow.getModified(),
+                flow.getType().toString(),
+                flow.getBucketId());
+
+        final String flowSql = "INSERT INTO flow (ID) VALUES (?)";
+
+        jdbcTemplate.update(flowSql, flow.getId());
+
+        return flow;
+    }
+
+    @Override
+    public FlowEntity getFlowById(final String flowIdentifier) {
+        final String sql = "SELECT * FROM flow f, bucket_item item WHERE f.id = ? AND item.id = f.id";
+        try {
+            return jdbcTemplate.queryForObject(sql, new FlowEntityRowMapper(), flowIdentifier);
+        } catch (EmptyResultDataAccessException e) {
+            return null;
+        }
+    }
+
+    @Override
+    public FlowEntity getFlowByIdWithSnapshotCounts(final String flowIdentifier) {
+        final FlowEntity flowEntity = getFlowById(flowIdentifier);
+        if (flowEntity == null) {
+            return flowEntity;
+        }
+
+        final Long snapshotCount = getFlowSnapshotCount(flowIdentifier);
+        if (snapshotCount != null) {
+            flowEntity.setSnapshotCount(snapshotCount);
+        }
+
+        return flowEntity;
+    }
+
+    @Override
+    public List<FlowEntity> getFlowsByName(final String name) {
+        final String sql = "SELECT * FROM flow f, bucket_item item WHERE item.name = ? AND item.id = f.id";
+        return jdbcTemplate.query(sql, new Object[] {name}, new FlowEntityRowMapper());
+    }
+
+    @Override
+    public List<FlowEntity> getFlowsByName(final String bucketIdentifier, final String name) {
+        final String sql = "SELECT * FROM flow f, bucket_item item WHERE item.name = ? AND item.id = f.id AND item.bucket_id = ?";
+        return jdbcTemplate.query(sql, new Object[] {name, bucketIdentifier}, new FlowEntityRowMapper());
+    }
+
+    @Override
+    public List<FlowEntity> getFlowsByBucket(final String bucketIdentifier) {
+        final String sql = "SELECT * FROM flow f, bucket_item item WHERE item.bucket_id = ? AND item.id = f.id";
+        final List<FlowEntity> flows = jdbcTemplate.query(sql, new Object[] {bucketIdentifier}, new FlowEntityRowMapper());
+
+        final Map<String,Long> snapshotCounts = getFlowSnapshotCounts();
+        for (final FlowEntity flowEntity : flows) {
+            final Long snapshotCount = snapshotCounts.get(flowEntity.getId());
+            if (snapshotCount != null) {
+                flowEntity.setSnapshotCount(snapshotCount);
+            }
+        }
+
+        return flows;
+    }
+
+    @Override
+    public FlowEntity updateFlow(final FlowEntity flow) {
+        flow.setModified(new Date());
+
+        final String sql = "UPDATE bucket_item SET name = ?, description = ?, modified = ? WHERE id = ?";
+        jdbcTemplate.update(sql, flow.getName(), flow.getDescription(), flow.getModified(), flow.getId());
+        return flow;
+    }
+
+    @Override
+    public void deleteFlow(final FlowEntity flow) {
+        final String snapshotDeleteSql = "DELETE FROM flow_snapshot WHERE flow_id = ?";
+        jdbcTemplate.update(snapshotDeleteSql, flow.getId());
+
+        final String flowDeleteSql = "DELETE FROM flow WHERE id = ?";
+        jdbcTemplate.update(flowDeleteSql, flow.getId());
+
+        final String itemDeleteSql = "DELETE FROM bucket_item WHERE id = ?";
+        jdbcTemplate.update(itemDeleteSql, flow.getId());
+    }
+
+    //----------------- Flow Snapshots ---------------------------------
+
+    @Override
+    public FlowSnapshotEntity createFlowSnapshot(final FlowSnapshotEntity flowSnapshot) {
+        final String sql = "INSERT INTO flow_snapshot (FLOW_ID, VERSION, CREATED, CREATED_BY, COMMENTS) VALUES (?, ?, ?, ?, ?)";
+
+        jdbcTemplate.update(sql,
+                flowSnapshot.getFlowId(),
+                flowSnapshot.getVersion(),
+                flowSnapshot.getCreated(),
+                flowSnapshot.getCreatedBy(),
+                flowSnapshot.getComments());
+
+        return flowSnapshot;
+    }
+
+    @Override
+    public FlowSnapshotEntity getFlowSnapshot(final String flowIdentifier, final Integer version) {
+        final String sql =
+                "SELECT " +
+                        "fs.flow_id, " +
+                        "fs.version, " +
+                        "fs.created, " +
+                        "fs.created_by, " +
+                        "fs.comments " +
+                "FROM " +
+                        "flow_snapshot fs, " +
+                        "flow f, " +
+                        "bucket_item item " +
+                "WHERE " +
+                        "item.id = f.id AND " +
+                        "f.id = ? AND " +
+                        "f.id = fs.flow_id AND " +
+                        "fs.version = ?";
+
+        try {
+            return jdbcTemplate.queryForObject(sql, new FlowSnapshotEntityRowMapper(),
+                    flowIdentifier, version);
+        } catch (EmptyResultDataAccessException e) {
+            return null;
+        }
+    }
+
+    @Override
+    public FlowSnapshotEntity getLatestSnapshot(final String flowIdentifier) {
+        final String sql = "SELECT * FROM flow_snapshot WHERE flow_id = ? ORDER BY version DESC LIMIT 1";
+
+        try {
+            return jdbcTemplate.queryForObject(sql, new FlowSnapshotEntityRowMapper(), flowIdentifier);
+        } catch (EmptyResultDataAccessException e) {
+            return null;
+        }
+    }
+
+    @Override
+    public List<FlowSnapshotEntity> getSnapshots(final String flowIdentifier) {
+        final String sql =
+                "SELECT " +
+                        "fs.flow_id, " +
+                        "fs.version, " +
+                        "fs.created, " +
+                        "fs.created_by, " +
+                        "fs.comments " +
+                "FROM " +
+                        "flow_snapshot fs, " +
+                        "flow f, " +
+                        "bucket_item item " +
+                "WHERE " +
+                        "item.id = f.id AND " +
+                        "f.id = ? AND " +
+                        "f.id = fs.flow_id";
+
+        final Object[] args = new Object[] { flowIdentifier };
+        return jdbcTemplate.query(sql, args, new FlowSnapshotEntityRowMapper());
+    }
+
+    @Override
+    public void deleteFlowSnapshot(final FlowSnapshotEntity flowSnapshot) {
+        final String sql = "DELETE FROM flow_snapshot WHERE flow_id = ? AND version = ?";
+        jdbcTemplate.update(sql, flowSnapshot.getFlowId(), flowSnapshot.getVersion());
+    }
+
+    //----------------- BucketItems ---------------------------------
+
+    @Override
+    public Set<String> getBucketFields() {
+        final Set<String> fields = new LinkedHashSet<>();
+        fields.add("ID");
+        fields.add("NAME");
+        fields.add("DESCRIPTION");
+        fields.add("CREATED");
+        return fields;
+    }
+
+    @Override
+    public Set<String> getBucketItemFields() {
+        final Set<String> fields = new LinkedHashSet<>();
+        fields.add("ID");
+        fields.add("NAME");
+        fields.add("DESCRIPTION");
+        fields.add("CREATED");
+        fields.add("MODIFIED");
+        fields.add("ITEM_TYPE");
+        fields.add("BUCKET_ID");
+        return fields;
+    }
+
+    @Override
+    public Set<String> getFlowFields() {
+        final Set<String> fields = new LinkedHashSet<>();
+        fields.add("ID");
+        fields.add("NAME");
+        fields.add("DESCRIPTION");
+        fields.add("CREATED");
+        fields.add("MODIFIED");
+        fields.add("ITEM_TYPE");
+        fields.add("BUCKET_ID");
+        return fields;
+    }
+}

http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/6f26290d/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/db/entity/BucketEntity.java
----------------------------------------------------------------------
diff --git a/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/db/entity/BucketEntity.java b/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/db/entity/BucketEntity.java
new file mode 100644
index 0000000..71d5a92
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/db/entity/BucketEntity.java
@@ -0,0 +1,83 @@
+/*
+ * 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.db.entity;
+
+import java.util.Date;
+import java.util.Objects;
+
+public class BucketEntity {
+
+    private String id;
+
+    private String name;
+
+    private String description;
+
+    private Date created;
+
+
+    public String getId() {
+        return id;
+    }
+
+    public void setId(String id) {
+        this.id = id;
+    }
+
+    public String getName() {
+        return name;
+    }
+
+    public void setName(String name) {
+        this.name = name;
+    }
+
+    public Date getCreated() {
+        return created;
+    }
+
+    public void setCreated(Date created) {
+        this.created = created;
+    }
+
+    public String getDescription() {
+        return description;
+    }
+
+    public void setDescription(String description) {
+        this.description = description;
+    }
+
+    @Override
+    public int hashCode() {
+        return Objects.hashCode(this.id);
+    }
+
+    @Override
+    public boolean equals(final Object obj) {
+        if (obj == null) {
+            return false;
+        }
+        if (getClass() != obj.getClass()) {
+            return false;
+        }
+
+        final BucketEntity other = (BucketEntity) obj;
+        return Objects.equals(this.id, other.id);
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/6f26290d/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/db/entity/BucketItemEntity.java
----------------------------------------------------------------------
diff --git a/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/db/entity/BucketItemEntity.java b/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/db/entity/BucketItemEntity.java
new file mode 100644
index 0000000..cdfa963
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/db/entity/BucketItemEntity.java
@@ -0,0 +1,123 @@
+/*
+ * 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.db.entity;
+
+import java.util.Date;
+import java.util.Objects;
+
+public class BucketItemEntity {
+
+    private String id;
+
+    private String name;
+
+    private String description;
+
+    private Date created;
+
+    private Date modified;
+
+    // NOTE: sub-classes should ensure that the type is set appropriately by overriding the getter/setter
+    private BucketItemEntityType type;
+
+    private String bucketId;
+
+    private String bucketName;
+
+
+    public String getId() {
+        return id;
+    }
+
+    public void setId(String id) {
+        this.id = id;
+    }
+
+    public String getName() {
+        return name;
+    }
+
+    public void setName(String name) {
+        this.name = name;
+    }
+
+    public String getDescription() {
+        return description;
+    }
+
+    public void setDescription(String description) {
+        this.description = description;
+    }
+
+    public Date getCreated() {
+        return created;
+    }
+
+    public void setCreated(Date created) {
+        this.created = created;
+    }
+
+    public Date getModified() {
+        return modified;
+    }
+
+    public void setModified(Date modified) {
+        this.modified = modified;
+    }
+
+    public BucketItemEntityType getType() {
+        return type;
+    }
+
+    public void setType(BucketItemEntityType type) {
+        this.type = type;
+    }
+
+    public String getBucketId() {
+        return bucketId;
+    }
+
+    public void setBucketId(String bucketId) {
+        this.bucketId = bucketId;
+    }
+
+    public String getBucketName() {
+        return bucketName;
+    }
+
+    public void setBucketName(String bucketName) {
+        this.bucketName = bucketName;
+    }
+
+    @Override
+    public int hashCode() {
+        return Objects.hashCode(this.id);
+    }
+
+    @Override
+    public boolean equals(final Object obj) {
+        if (obj == null) {
+            return false;
+        }
+        if (getClass() != obj.getClass()) {
+            return false;
+        }
+
+        final BucketItemEntity other = (BucketItemEntity) obj;
+        return Objects.equals(this.id, other.id);
+    }
+}

http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/6f26290d/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/db/entity/BucketItemEntityType.java
----------------------------------------------------------------------
diff --git a/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/db/entity/BucketItemEntityType.java b/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/db/entity/BucketItemEntityType.java
new file mode 100644
index 0000000..e78b2b1
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/db/entity/BucketItemEntityType.java
@@ -0,0 +1,42 @@
+/*
+ * 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.db.entity;
+
+/**
+ * Possible types of BucketItemEntity.
+ */
+public enum BucketItemEntityType {
+
+    FLOW(Values.FLOW);
+
+    private final String value;
+
+    BucketItemEntityType(final String value) {
+        this.value = value;
+    }
+
+    @Override
+    public String toString() {
+        return value;
+    }
+
+    // need these constants to reference from @DiscriminatorValue
+    public static class Values {
+        public static final String FLOW = "FLOW";
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/6f26290d/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/db/entity/FlowEntity.java
----------------------------------------------------------------------
diff --git a/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/db/entity/FlowEntity.java b/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/db/entity/FlowEntity.java
new file mode 100644
index 0000000..b978168
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/db/entity/FlowEntity.java
@@ -0,0 +1,42 @@
+/*
+ * 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.db.entity;
+
+public class FlowEntity extends BucketItemEntity {
+
+    private long snapshotCount;
+
+    public FlowEntity() {
+        setType(BucketItemEntityType.FLOW);
+    }
+
+    public long getSnapshotCount() {
+        return snapshotCount;
+    }
+
+    public void setSnapshotCount(long snapshotCount) {
+        this.snapshotCount = snapshotCount;
+    }
+
+    @Override
+    public void setType(BucketItemEntityType type) {
+        if (BucketItemEntityType.FLOW != type) {
+            throw new IllegalStateException("Must set type to FLOW");
+        }
+        super.setType(type);
+    }
+}

http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/6f26290d/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/db/entity/FlowSnapshotEntity.java
----------------------------------------------------------------------
diff --git a/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/db/entity/FlowSnapshotEntity.java b/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/db/entity/FlowSnapshotEntity.java
new file mode 100644
index 0000000..3143a6e
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/db/entity/FlowSnapshotEntity.java
@@ -0,0 +1,93 @@
+/*
+ * 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.db.entity;
+
+import java.util.Date;
+import java.util.Objects;
+
+public class FlowSnapshotEntity {
+
+    private String flowId;
+
+    private Integer version;
+
+    private Date created;
+
+    private String createdBy;
+
+    private String comments;
+
+    public String getFlowId() {
+        return flowId;
+    }
+
+    public void setFlowId(String flowId) {
+        this.flowId = flowId;
+    }
+
+    public Integer getVersion() {
+        return version;
+    }
+
+    public void setVersion(Integer version) {
+        this.version = version;
+    }
+
+    public Date getCreated() {
+        return created;
+    }
+
+    public void setCreated(Date created) {
+        this.created = created;
+    }
+
+    public String getCreatedBy() {
+        return createdBy;
+    }
+
+    public void setCreatedBy(String createdBy) {
+        this.createdBy = createdBy;
+    }
+
+    public String getComments() {
+        return comments;
+    }
+
+    public void setComments(String comments) {
+        this.comments = comments;
+    }
+
+    @Override
+    public int hashCode() {
+        return Objects.hash(this.flowId, this.version);
+    }
+
+    @Override
+    public boolean equals(Object obj) {
+        if (obj == null) {
+            return false;
+        }
+
+        if (!(obj instanceof FlowSnapshotEntity)) {
+            return false;
+        }
+
+        final FlowSnapshotEntity other = (FlowSnapshotEntity) obj;
+        return Objects.equals(this.flowId, other.flowId) && Objects.equals(this.version, other.version);
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/6f26290d/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/db/entity/KeyEntity.java
----------------------------------------------------------------------
diff --git a/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/db/entity/KeyEntity.java b/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/db/entity/KeyEntity.java
new file mode 100644
index 0000000..494867f
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/db/entity/KeyEntity.java
@@ -0,0 +1,51 @@
+/*
+ * 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.db.entity;
+
+public class KeyEntity {
+
+    private String id;
+
+    private String tenantIdentity;
+
+    private String keyValue;
+
+    public String getId() {
+        return id;
+    }
+
+    public void setId(String id) {
+        this.id = id;
+    }
+
+    public String getTenantIdentity() {
+        return tenantIdentity;
+    }
+
+    public void setTenantIdentity(String tenantIdentity) {
+        this.tenantIdentity = tenantIdentity;
+    }
+
+    public String getKeyValue() {
+        return keyValue;
+    }
+
+    public void setKeyValue(String keyValue) {
+        this.keyValue = keyValue;
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/6f26290d/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/db/mapper/BucketEntityRowMapper.java
----------------------------------------------------------------------
diff --git a/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/db/mapper/BucketEntityRowMapper.java b/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/db/mapper/BucketEntityRowMapper.java
new file mode 100644
index 0000000..6c5bc2e
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/db/mapper/BucketEntityRowMapper.java
@@ -0,0 +1,39 @@
+/*
+ * 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.db.mapper;
+
+import org.apache.nifi.registry.db.entity.BucketEntity;
+import org.springframework.jdbc.core.RowMapper;
+import org.springframework.lang.Nullable;
+
+import java.sql.ResultSet;
+import java.sql.SQLException;
+
+public class BucketEntityRowMapper implements RowMapper<BucketEntity> {
+
+    @Nullable
+    @Override
+    public BucketEntity mapRow(ResultSet rs, int rowNum) throws SQLException {
+        final BucketEntity b = new BucketEntity();
+        b.setId(rs.getString("ID"));
+        b.setName(rs.getString("NAME"));
+        b.setDescription(rs.getString("DESCRIPTION"));
+        b.setCreated(rs.getTimestamp("CREATED"));
+        return b;
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/6f26290d/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/db/mapper/BucketItemEntityRowMapper.java
----------------------------------------------------------------------
diff --git a/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/db/mapper/BucketItemEntityRowMapper.java b/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/db/mapper/BucketItemEntityRowMapper.java
new file mode 100644
index 0000000..7b3df05
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/db/mapper/BucketItemEntityRowMapper.java
@@ -0,0 +1,58 @@
+/*
+ * 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.db.mapper;
+
+import org.apache.nifi.registry.db.entity.BucketItemEntity;
+import org.apache.nifi.registry.db.entity.BucketItemEntityType;
+import org.apache.nifi.registry.db.entity.FlowEntity;
+import org.springframework.jdbc.core.RowMapper;
+import org.springframework.lang.Nullable;
+
+import java.sql.ResultSet;
+import java.sql.SQLException;
+
+public class BucketItemEntityRowMapper implements RowMapper<BucketItemEntity> {
+
+    @Nullable
+    @Override
+    public BucketItemEntity mapRow(ResultSet rs, int rowNum) throws SQLException {
+        final BucketItemEntityType type = BucketItemEntityType.valueOf(rs.getString("ITEM_TYPE"));
+
+        // Create the appropriate type of sub-class, eventually populate specific data for each type
+        final BucketItemEntity item;
+        switch (type) {
+            case FLOW:
+                item = new FlowEntity();
+                break;
+            default:
+                // should never happen
+                item = new BucketItemEntity();
+                break;
+        }
+
+        // populate fields common to all bucket items
+        item.setId(rs.getString("ID"));
+        item.setName(rs.getString("NAME"));
+        item.setDescription(rs.getString("DESCRIPTION"));
+        item.setCreated(rs.getTimestamp("CREATED"));
+        item.setModified(rs.getTimestamp("MODIFIED"));
+        item.setBucketId(rs.getString("BUCKET_ID"));
+        item.setBucketName(rs.getString("BUCKET_NAME"));
+        item.setType(type);
+        return item;
+    }
+}

http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/6f26290d/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/db/mapper/FlowEntityRowMapper.java
----------------------------------------------------------------------
diff --git a/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/db/mapper/FlowEntityRowMapper.java b/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/db/mapper/FlowEntityRowMapper.java
new file mode 100644
index 0000000..acaf343
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/db/mapper/FlowEntityRowMapper.java
@@ -0,0 +1,43 @@
+/*
+ * 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.db.mapper;
+
+import org.apache.nifi.registry.db.entity.BucketItemEntityType;
+import org.apache.nifi.registry.db.entity.FlowEntity;
+import org.springframework.jdbc.core.RowMapper;
+import org.springframework.lang.Nullable;
+
+import java.sql.ResultSet;
+import java.sql.SQLException;
+
+public class FlowEntityRowMapper implements RowMapper<FlowEntity> {
+
+    @Nullable
+    @Override
+    public FlowEntity mapRow(ResultSet rs, int rowNum) throws SQLException {
+        final FlowEntity flowEntity = new FlowEntity();
+        flowEntity.setId(rs.getString("ID"));
+        flowEntity.setName(rs.getString("NAME"));
+        flowEntity.setDescription(rs.getString("DESCRIPTION"));
+        flowEntity.setCreated(rs.getTimestamp("CREATED"));
+        flowEntity.setModified(rs.getTimestamp("MODIFIED"));
+        flowEntity.setBucketId(rs.getString("BUCKET_ID"));
+        flowEntity.setType(BucketItemEntityType.FLOW);
+        return flowEntity;
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/6f26290d/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/db/mapper/FlowSnapshotEntityRowMapper.java
----------------------------------------------------------------------
diff --git a/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/db/mapper/FlowSnapshotEntityRowMapper.java b/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/db/mapper/FlowSnapshotEntityRowMapper.java
new file mode 100644
index 0000000..07a59b3
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/db/mapper/FlowSnapshotEntityRowMapper.java
@@ -0,0 +1,39 @@
+/*
+ * 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.db.mapper;
+
+import org.apache.nifi.registry.db.entity.FlowSnapshotEntity;
+import org.springframework.jdbc.core.RowMapper;
+import org.springframework.lang.Nullable;
+
+import java.sql.ResultSet;
+import java.sql.SQLException;
+
+public class FlowSnapshotEntityRowMapper implements RowMapper<FlowSnapshotEntity> {
+
+    @Nullable
+    @Override
+    public FlowSnapshotEntity mapRow(ResultSet rs, int rowNum) throws SQLException {
+        final FlowSnapshotEntity entity = new FlowSnapshotEntity();
+        entity.setFlowId(rs.getString("FLOW_ID"));
+        entity.setVersion(rs.getInt("VERSION"));
+        entity.setCreated(rs.getTimestamp("CREATED"));
+        entity.setCreatedBy(rs.getString("CREATED_BY"));
+        entity.setComments(rs.getString("COMMENTS"));
+        return entity;
+    }
+}

http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/6f26290d/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/db/mapper/KeyEntityRowMapper.java
----------------------------------------------------------------------
diff --git a/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/db/mapper/KeyEntityRowMapper.java b/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/db/mapper/KeyEntityRowMapper.java
new file mode 100644
index 0000000..6e190a5
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/db/mapper/KeyEntityRowMapper.java
@@ -0,0 +1,37 @@
+/*
+ * 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.db.mapper;
+
+import org.apache.nifi.registry.db.entity.KeyEntity;
+import org.springframework.jdbc.core.RowMapper;
+import org.springframework.lang.Nullable;
+
+import java.sql.ResultSet;
+import java.sql.SQLException;
+
+public class KeyEntityRowMapper implements RowMapper<KeyEntity> {
+
+    @Nullable
+    @Override
+    public KeyEntity mapRow(ResultSet rs, int rowNum) throws SQLException {
+        final KeyEntity keyEntity = new KeyEntity();
+        keyEntity.setId(rs.getString("ID"));
+        keyEntity.setTenantIdentity(rs.getString("TENANT_IDENTITY"));
+        keyEntity.setKeyValue(rs.getString("KEY_VALUE"));
+        return keyEntity;
+    }
+}

http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/6f26290d/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/db/migration/BucketEntityV1.java
----------------------------------------------------------------------
diff --git a/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/db/migration/BucketEntityV1.java b/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/db/migration/BucketEntityV1.java
new file mode 100644
index 0000000..94000a5
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/db/migration/BucketEntityV1.java
@@ -0,0 +1,86 @@
+/*
+ * 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.db.migration;
+
+import java.util.Date;
+import java.util.Objects;
+
+/**
+ * Bucket DB entity from the original database schema in 0.1.0, used for migration purposes.
+ */
+public class BucketEntityV1 {
+
+    private String id;
+
+    private String name;
+
+    private String description;
+
+    private Date created;
+
+
+    public String getId() {
+        return id;
+    }
+
+    public void setId(String id) {
+        this.id = id;
+    }
+
+    public String getName() {
+        return name;
+    }
+
+    public void setName(String name) {
+        this.name = name;
+    }
+
+    public Date getCreated() {
+        return created;
+    }
+
+    public void setCreated(Date created) {
+        this.created = created;
+    }
+
+    public String getDescription() {
+        return description;
+    }
+
+    public void setDescription(String description) {
+        this.description = description;
+    }
+
+    @Override
+    public int hashCode() {
+        return Objects.hashCode(this.id);
+    }
+
+    @Override
+    public boolean equals(final Object obj) {
+        if (obj == null) {
+            return false;
+        }
+        if (getClass() != obj.getClass()) {
+            return false;
+        }
+
+        final BucketEntityV1 other = (BucketEntityV1) obj;
+        return Objects.equals(this.id, other.id);
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/6f26290d/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/db/migration/FlowEntityV1.java
----------------------------------------------------------------------
diff --git a/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/db/migration/FlowEntityV1.java b/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/db/migration/FlowEntityV1.java
new file mode 100644
index 0000000..961c3bd
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/db/migration/FlowEntityV1.java
@@ -0,0 +1,106 @@
+/*
+ * 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.db.migration;
+
+import java.util.Date;
+import java.util.Objects;
+
+/**
+ * Flow DB entity from the original database schema in 0.1.0, used for migration purposes.
+ */
+public class FlowEntityV1 {
+
+    private String id;
+
+    private String name;
+
+    private String description;
+
+    private Date created;
+
+    private Date modified;
+
+    private String bucketId;
+
+
+    public String getId() {
+        return id;
+    }
+
+    public void setId(String id) {
+        this.id = id;
+    }
+
+    public String getName() {
+        return name;
+    }
+
+    public void setName(String name) {
+        this.name = name;
+    }
+
+    public String getDescription() {
+        return description;
+    }
+
+    public void setDescription(String description) {
+        this.description = description;
+    }
+
+    public Date getCreated() {
+        return created;
+    }
+
+    public void setCreated(Date created) {
+        this.created = created;
+    }
+
+    public Date getModified() {
+        return modified;
+    }
+
+    public void setModified(Date modified) {
+        this.modified = modified;
+    }
+
+    public String getBucketId() {
+        return bucketId;
+    }
+
+    public void setBucketId(String bucketId) {
+        this.bucketId = bucketId;
+    }
+
+    @Override
+    public int hashCode() {
+        return Objects.hashCode(this.id);
+    }
+
+    @Override
+    public boolean equals(final Object obj) {
+        if (obj == null) {
+            return false;
+        }
+        if (getClass() != obj.getClass()) {
+            return false;
+        }
+
+        final FlowEntityV1 other = (FlowEntityV1) obj;
+        return Objects.equals(this.id, other.id);
+    }
+
+}


[02/51] [partial] nifi-registry git commit: NIFIREG-201 Refactoring project structure to better isolate extensions

Posted by kd...@apache.org.
http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/6f26290d/nifi-registry-core/nifi-registry-web-ui/src/main/webapp/components/administration/users/sidenav/manage-group/nf-registry-manage-group.spec.js
----------------------------------------------------------------------
diff --git a/nifi-registry-core/nifi-registry-web-ui/src/main/webapp/components/administration/users/sidenav/manage-group/nf-registry-manage-group.spec.js b/nifi-registry-core/nifi-registry-web-ui/src/main/webapp/components/administration/users/sidenav/manage-group/nf-registry-manage-group.spec.js
new file mode 100644
index 0000000..624447a
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-web-ui/src/main/webapp/components/administration/users/sidenav/manage-group/nf-registry-manage-group.spec.js
@@ -0,0 +1,2517 @@
+/*
+ * 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.
+ */
+
+var NfRegistryRoutes = require('nifi-registry/nf-registry.routes.js');
+var ngCoreTesting = require('@angular/core/testing');
+var ngCommon = require('@angular/common');
+var ngRouter = require('@angular/router');
+var NfRegistry = require('nifi-registry/nf-registry.js');
+var NfRegistryApi = require('nifi-registry/services/nf-registry.api.js');
+var NfRegistryService = require('nifi-registry/services/nf-registry.service.js');
+var NfPageNotFoundComponent = require('nifi-registry/components/page-not-found/nf-registry-page-not-found.js');
+var NfRegistryExplorer = require('nifi-registry/components/explorer/nf-registry-explorer.js');
+var NfRegistryAdministration = require('nifi-registry/components/administration/nf-registry-administration.js');
+var NfRegistryUsersAdministration = require('nifi-registry/components/administration/users/nf-registry-users-administration.js');
+var NfRegistryAddUser = require('nifi-registry/components/administration/users/dialogs/add-user/nf-registry-add-user.js');
+var NfRegistryCreateNewGroup = require('nifi-registry/components/administration/users/dialogs/create-new-group/nf-registry-create-new-group.js');
+var NfRegistryEditBucketPolicy = require('nifi-registry/components/administration/workflow/dialogs/edit-bucket-policy/nf-registry-edit-bucket-policy.js');
+var NfRegistryAddPolicyToBucket = require('nifi-registry/components/administration/workflow/dialogs/add-policy-to-bucket/nf-registry-add-policy-to-bucket.js');
+var NfRegistryAddUserToGroups = require('nifi-registry/components/administration/users/dialogs/add-user-to-groups/nf-registry-add-user-to-groups.js');
+var NfRegistryAddUsersToGroup = require('nifi-registry/components/administration/users/dialogs/add-users-to-group/nf-registry-add-users-to-group.js');
+var NfRegistryManageUser = require('nifi-registry/components/administration/users/sidenav/manage-user/nf-registry-manage-user.js');
+var NfRegistryManageGroup = require('nifi-registry/components/administration/users/sidenav/manage-group/nf-registry-manage-group.js');
+var NfRegistryManageBucket = require('nifi-registry/components/administration/workflow/sidenav/manage-bucket/nf-registry-manage-bucket.js');
+var NfRegistryWorkflowAdministration = require('nifi-registry/components/administration/workflow/nf-registry-workflow-administration.js');
+var NfRegistryCreateBucket = require('nifi-registry/components/administration/workflow/dialogs/create-bucket/nf-registry-create-bucket.js');
+var NfRegistryGridListViewer = require('nifi-registry/components/explorer/grid-list/registry/nf-registry-grid-list-viewer.js');
+var NfRegistryBucketGridListViewer = require('nifi-registry/components/explorer/grid-list/registry/nf-registry-bucket-grid-list-viewer.js');
+var NfRegistryDropletGridListViewer = require('nifi-registry/components/explorer/grid-list/registry/nf-registry-droplet-grid-list-viewer.js');
+var fdsCore = require('@flow-design-system/core');
+var ngMoment = require('angular2-moment');
+var rxjs = require('rxjs/Rx');
+var ngCommonHttp = require('@angular/common/http');
+var NfRegistryTokenInterceptor = require('nifi-registry/services/nf-registry.token.interceptor.js');
+var NfStorage = require('nifi-registry/services/nf-storage.service.js');
+var NfLoginComponent = require('nifi-registry/components/login/nf-registry-login.js');
+var NfUserLoginComponent = require('nifi-registry/components/login/dialogs/nf-registry-user-login.js');
+
+describe('NfRegistryManageGroup Component', function () {
+    var comp;
+    var fixture;
+    var nfRegistryService;
+    var nfRegistryApi;
+
+    beforeEach(function () {
+        ngCoreTesting.TestBed.configureTestingModule({
+            imports: [
+                ngMoment.MomentModule,
+                ngCommonHttp.HttpClientModule,
+                fdsCore,
+                NfRegistryRoutes
+            ],
+            declarations: [
+                NfRegistry,
+                NfRegistryExplorer,
+                NfRegistryAdministration,
+                NfRegistryUsersAdministration,
+                NfRegistryManageUser,
+                NfRegistryManageGroup,
+                NfRegistryManageBucket,
+                NfRegistryWorkflowAdministration,
+                NfRegistryAddUser,
+                NfRegistryCreateBucket,
+                NfRegistryCreateNewGroup,
+                NfRegistryAddUserToGroups,
+                NfRegistryAddUsersToGroup,
+                NfRegistryAddPolicyToBucket,
+                NfRegistryEditBucketPolicy,
+                NfRegistryGridListViewer,
+                NfRegistryBucketGridListViewer,
+                NfRegistryDropletGridListViewer,
+                NfPageNotFoundComponent,
+                NfLoginComponent,
+                NfUserLoginComponent
+            ],
+            entryComponents: [
+                NfRegistryAddUser,
+                NfRegistryCreateBucket,
+                NfRegistryCreateNewGroup,
+                NfRegistryAddUserToGroups,
+                NfRegistryAddUsersToGroup,
+                NfRegistryAddPolicyToBucket,
+                NfRegistryEditBucketPolicy,
+                NfUserLoginComponent
+            ],
+            providers: [
+                NfRegistryService,
+                NfRegistryApi,
+                NfStorage,
+                {
+                    provide: ngCommonHttp.HTTP_INTERCEPTORS,
+                    useClass: NfRegistryTokenInterceptor,
+                    multi: true
+                },
+                {
+                    provide: ngCommon.APP_BASE_HREF,
+                    useValue: '/'
+                },
+                {
+                    provide: ngRouter.ActivatedRoute,
+                    useValue: {
+                        params: rxjs.Observable.of({groupId: '123'})
+                    }
+                }
+            ]
+        });
+        fixture = ngCoreTesting.TestBed.createComponent(NfRegistryManageGroup);
+
+        // test instance
+        comp = fixture.componentInstance;
+
+        // from the root injector
+        nfRegistryService = ngCoreTesting.TestBed.get(NfRegistryService);
+        nfRegistryApi = ngCoreTesting.TestBed.get(NfRegistryApi);
+
+        // because the NfRegistryManageGroup component is a nested route component we need to set up the nfRegistryService service manually
+        nfRegistryService.sidenav = {
+            open: function () {
+            },
+            close: function () {
+            }
+        };
+        nfRegistryService.group = {
+            identifier: 999,
+            identity: 'Group #1',
+            users: [{
+                identifier: '123',
+                identity: 'Group #1',
+                resourcePermissions: {
+                    anyTopLevelResource: {
+                        canRead: false,
+                        canWrite: false,
+                        canDelete: false
+                    },
+                    buckets: {
+                        canRead: false,
+                        canWrite: false,
+                        canDelete: false
+                    },
+                    tenants: {
+                        canRead: false,
+                        canWrite: false,
+                        canDelete: false
+                    },
+                    policies: {
+                        canRead: false,
+                        canWrite: false,
+                        canDelete: false
+                    },
+                    proxy: {
+                        canRead: false,
+                        canWrite: false,
+                        canDelete: false
+                    }
+                }
+            }],
+            resourcePermissions: {
+                anyTopLevelResource: {
+                    canRead: false,
+                    canWrite: false,
+                    canDelete: false
+                },
+                buckets: {
+                    canRead: false,
+                    canWrite: false,
+                    canDelete: false
+                },
+                tenants: {
+                    canRead: false,
+                    canWrite: false,
+                    canDelete: false
+                },
+                policies: {
+                    canRead: false,
+                    canWrite: false,
+                    canDelete: false
+                },
+                proxy: {
+                    canRead: false,
+                    canWrite: false,
+                    canDelete: false
+                }
+            }
+        };
+        nfRegistryService.groups = [nfRegistryService.group];
+
+        //Spy
+        spyOn(nfRegistryApi, 'ticketExchange').and.callFake(function () {
+        }).and.returnValue(rxjs.Observable.of({}));
+        spyOn(nfRegistryApi, 'loadCurrentUser').and.callFake(function () {
+        }).and.returnValue(rxjs.Observable.of({}));
+    });
+
+    it('should have a defined component', ngCoreTesting.fakeAsync(function () {
+        spyOn(nfRegistryApi, 'getUserGroup').and.callFake(function () {
+        }).and.returnValue(rxjs.Observable.of({
+            identifier: '123',
+            identity: 'Group #1',
+            resourcePermissions: {
+                anyTopLevelResource: {
+                    canRead: false,
+                    canWrite: false,
+                    canDelete: false
+                },
+                buckets: {
+                    canRead: false,
+                    canWrite: false,
+                    canDelete: false
+                },
+                tenants: {
+                    canRead: false,
+                    canWrite: false,
+                    canDelete: false
+                },
+                policies: {
+                    canRead: false,
+                    canWrite: false,
+                    canDelete: false
+                },
+                proxy: {
+                    canRead: false,
+                    canWrite: false,
+                    canDelete: false
+                }
+            },
+            users: [{
+                identifier: '123',
+                identity: 'User #1',
+                resourcePermissions: {
+                    anyTopLevelResource: {
+                        canRead: false,
+                        canWrite: false,
+                        canDelete: false
+                    },
+                    buckets: {
+                        canRead: false,
+                        canWrite: false,
+                        canDelete: false
+                    },
+                    tenants: {
+                        canRead: false,
+                        canWrite: false,
+                        canDelete: false
+                    },
+                    policies: {
+                        canRead: false,
+                        canWrite: false,
+                        canDelete: false
+                    },
+                    proxy: {
+                        canRead: false,
+                        canWrite: false,
+                        canDelete: false
+                    }
+                }
+            }]
+        }));
+        // 1st change detection triggers ngOnInit
+        fixture.detectChanges();
+        // wait for async calls
+        ngCoreTesting.tick();
+        // 2nd change detection completes after the getUserGroup calls
+        fixture.detectChanges();
+
+        //assertions
+        expect(comp).toBeDefined();
+        expect(nfRegistryService.group.identifier).toEqual('123');
+
+        var getUserGroupCall = nfRegistryApi.getUserGroup.calls.first()
+        expect(getUserGroupCall.args[0]).toBe('123');
+    }));
+
+    it('should FAIL to get user by id and redirect to admin users perspective', ngCoreTesting.fakeAsync(function () {
+        spyOn(nfRegistryApi, 'getUserGroup').and.callFake(function () {
+        }).and.returnValue(rxjs.Observable.of({
+            status: 404
+        }));
+        spyOn(comp.router, 'navigateByUrl').and.callFake(function () {
+        });
+        // 1st change detection triggers ngOnInit
+        fixture.detectChanges();
+        // wait for async calls
+        ngCoreTesting.tick();
+        // 2nd change detection completes after the getUserGroup calls
+        fixture.detectChanges();
+
+        //assertions
+        var routerCall = comp.router.navigateByUrl.calls.first();
+        expect(routerCall.args[0]).toBe('/nifi-registry/administration/users');
+        expect(comp.router.navigateByUrl.calls.count()).toBe(1);
+    }));
+
+    it('should FAIL to get user by id and redirect to workflow perspective', ngCoreTesting.fakeAsync(function () {
+        spyOn(nfRegistryApi, 'getUserGroup').and.callFake(function () {
+        }).and.returnValue(rxjs.Observable.of({
+            status: 409
+        }));
+        spyOn(comp.router, 'navigateByUrl').and.callFake(function () {
+        });
+        // 1st change detection triggers ngOnInit
+        fixture.detectChanges();
+        // wait for async calls
+        ngCoreTesting.tick();
+        // 2nd change detection completes after the getUserGroup calls
+        fixture.detectChanges();
+
+        //assertions
+        var routerCall = comp.router.navigateByUrl.calls.first();
+        expect(routerCall.args[0]).toBe('/nifi-registry/administration/workflow');
+        expect(comp.router.navigateByUrl.calls.count()).toBe(1);
+    }));
+
+    it('should redirect to users perspective', ngCoreTesting.fakeAsync(function () {
+        // Spy
+        spyOn(nfRegistryApi, 'getUserGroup').and.callFake(function () {
+        }).and.returnValue(rxjs.Observable.of({
+            identifier: '123',
+            identity: 'Group #1',
+            resourcePermissions: {
+                anyTopLevelResource: {
+                    canRead: false,
+                    canWrite: false,
+                    canDelete: false
+                },
+                buckets: {
+                    canRead: false,
+                    canWrite: false,
+                    canDelete: false
+                },
+                tenants: {
+                    canRead: false,
+                    canWrite: false,
+                    canDelete: false
+                },
+                policies: {
+                    canRead: false,
+                    canWrite: false,
+                    canDelete: false
+                },
+                proxy: {
+                    canRead: false,
+                    canWrite: false,
+                    canDelete: false
+                }
+            },
+            users: [{
+                identifier: '123',
+                identity: 'User #1',
+                resourcePermissions: {
+                    anyTopLevelResource: {
+                        canRead: false,
+                        canWrite: false,
+                        canDelete: false
+                    },
+                    buckets: {
+                        canRead: false,
+                        canWrite: false,
+                        canDelete: false
+                    },
+                    tenants: {
+                        canRead: false,
+                        canWrite: false,
+                        canDelete: false
+                    },
+                    policies: {
+                        canRead: false,
+                        canWrite: false,
+                        canDelete: false
+                    },
+                    proxy: {
+                        canRead: false,
+                        canWrite: false,
+                        canDelete: false
+                    }
+                }
+            }]
+        }));
+        spyOn(comp.router, 'navigateByUrl').and.callFake(function () {
+        });
+        // 1st change detection triggers ngOnInit
+        fixture.detectChanges();
+        // wait for async calls
+        ngCoreTesting.tick();
+        // 2nd change detection completes after the getUserGroup calls
+        fixture.detectChanges();
+
+        // the function to test
+        comp.closeSideNav();
+
+        //assertions
+        var routerCall = comp.router.navigateByUrl.calls.first();
+        expect(routerCall.args[0]).toBe('/nifi-registry/administration/users');
+        expect(comp.router.navigateByUrl.calls.count()).toBe(1);
+    }));
+
+    it('should toggle to create the manage bucket privileges for the current group', ngCoreTesting.fakeAsync(function () {
+        // Spy
+        spyOn(nfRegistryApi, 'getPolicyActionResource').and.callFake(function () {
+        }).and.returnValue(rxjs.Observable.of({
+            status: 404,
+            userGroups: []
+        }));
+        spyOn(nfRegistryApi, 'postPolicyActionResource').and.callFake(function () {
+        }).and.returnValue(rxjs.Observable.of({
+            userGroups: [
+                {
+                    identifier: '123',
+                    identity: 'Group #1',
+                    resourcePermissions: {
+                        anyTopLevelResource: {
+                            canRead: false,
+                            canWrite: false,
+                            canDelete: false
+                        },
+                        buckets: {
+                            canRead: false,
+                            canWrite: false,
+                            canDelete: false
+                        },
+                        tenants: {
+                            canRead: false,
+                            canWrite: false,
+                            canDelete: false
+                        },
+                        policies: {
+                            canRead: false,
+                            canWrite: false,
+                            canDelete: false
+                        },
+                        proxy: {
+                            canRead: false,
+                            canWrite: false,
+                            canDelete: false
+                        }
+                    }
+                }
+            ]
+        }));
+        spyOn(nfRegistryApi, 'getUserGroup').and.callFake(function () {
+        }).and.returnValue(rxjs.Observable.of({
+            identifier: '123',
+            identity: 'Group #1',
+            resourcePermissions: {
+                anyTopLevelResource: {
+                    canRead: false,
+                    canWrite: false,
+                    canDelete: false
+                },
+                buckets: {
+                    canRead: false,
+                    canWrite: false,
+                    canDelete: false
+                },
+                tenants: {
+                    canRead: false,
+                    canWrite: false,
+                    canDelete: false
+                },
+                policies: {
+                    canRead: false,
+                    canWrite: false,
+                    canDelete: false
+                },
+                proxy: {
+                    canRead: false,
+                    canWrite: false,
+                    canDelete: false
+                }
+            },
+            users: [{
+                identifier: '123',
+                identity: 'User #1',
+                resourcePermissions: {
+                    anyTopLevelResource: {
+                        canRead: false,
+                        canWrite: false,
+                        canDelete: false
+                    },
+                    buckets: {
+                        canRead: false,
+                        canWrite: false,
+                        canDelete: false
+                    },
+                    tenants: {
+                        canRead: false,
+                        canWrite: false,
+                        canDelete: false
+                    },
+                    policies: {
+                        canRead: false,
+                        canWrite: false,
+                        canDelete: false
+                    },
+                    proxy: {
+                        canRead: false,
+                        canWrite: false,
+                        canDelete: false
+                    }
+                }
+            }]
+        }));
+
+        // 1st change detection triggers ngOnInit
+        fixture.detectChanges();
+        // wait for async calls
+        ngCoreTesting.tick();
+        // 2nd change detection completes after the getUserGroup calls
+        fixture.detectChanges();
+
+        //assertions
+        var getUserGroupCall = nfRegistryApi.getUserGroup.calls.first();
+        expect(getUserGroupCall.args[0]).toBe('123');
+        expect(nfRegistryApi.getUserGroup.calls.count()).toBe(1);
+
+        // the function to test
+        comp.toggleGroupManageBucketsPrivileges({
+            checked: true
+        }, 'read');
+
+        //assertions
+        expect(nfRegistryApi.getPolicyActionResource.calls.count()).toBe(1);
+        expect(nfRegistryApi.postPolicyActionResource.calls.count()).toBe(1);
+        expect(nfRegistryApi.getUserGroup.calls.count()).toBe(2);
+    }));
+
+    it('should toggle to update the manage bucket privileges for the current group', ngCoreTesting.fakeAsync(function () {
+        // Spy
+        spyOn(nfRegistryApi, 'getPolicyActionResource').and.callFake(function () {
+        }).and.returnValue(rxjs.Observable.of({
+            userGroups: []
+        }));
+        spyOn(nfRegistryApi, 'putPolicyActionResource').and.callFake(function () {
+        }).and.returnValue(rxjs.Observable.of({
+            userGroups: [
+                {
+                    identifier: '123',
+                    identity: 'Group #1',
+                    resourcePermissions: {
+                        anyTopLevelResource: {
+                            canRead: false,
+                            canWrite: false,
+                            canDelete: false
+                        },
+                        buckets: {
+                            canRead: false,
+                            canWrite: false,
+                            canDelete: false
+                        },
+                        tenants: {
+                            canRead: false,
+                            canWrite: false,
+                            canDelete: false
+                        },
+                        policies: {
+                            canRead: false,
+                            canWrite: false,
+                            canDelete: false
+                        },
+                        proxy: {
+                            canRead: false,
+                            canWrite: false,
+                            canDelete: false
+                        }
+                    }
+                }
+            ]
+        }));
+        spyOn(nfRegistryApi, 'getUserGroup').and.callFake(function () {
+        }).and.returnValue(rxjs.Observable.of({
+            identifier: '123',
+            identity: 'Group #1',
+            resourcePermissions: {
+                anyTopLevelResource: {
+                    canRead: false,
+                    canWrite: false,
+                    canDelete: false
+                },
+                buckets: {
+                    canRead: false,
+                    canWrite: false,
+                    canDelete: false
+                },
+                tenants: {
+                    canRead: false,
+                    canWrite: false,
+                    canDelete: false
+                },
+                policies: {
+                    canRead: false,
+                    canWrite: false,
+                    canDelete: false
+                },
+                proxy: {
+                    canRead: false,
+                    canWrite: false,
+                    canDelete: false
+                }
+            },
+            users: [{
+                identifier: '123',
+                identity: 'User #1',
+                resourcePermissions: {
+                    anyTopLevelResource: {
+                        canRead: false,
+                        canWrite: false,
+                        canDelete: false
+                    },
+                    buckets: {
+                        canRead: false,
+                        canWrite: false,
+                        canDelete: false
+                    },
+                    tenants: {
+                        canRead: false,
+                        canWrite: false,
+                        canDelete: false
+                    },
+                    policies: {
+                        canRead: false,
+                        canWrite: false,
+                        canDelete: false
+                    },
+                    proxy: {
+                        canRead: false,
+                        canWrite: false,
+                        canDelete: false
+                    }
+                }
+            }]
+        }));
+
+        // 1st change detection triggers ngOnInit
+        fixture.detectChanges();
+        // wait for async calls
+        ngCoreTesting.tick();
+        // 2nd change detection completes after the getUserGroup calls
+        fixture.detectChanges();
+
+        //assertions
+        var getUserGroupCall = nfRegistryApi.getUserGroup.calls.first();
+        expect(getUserGroupCall.args[0]).toBe('123');
+        expect(nfRegistryApi.getUserGroup.calls.count()).toBe(1);
+
+        // the function to test
+        comp.toggleGroupManageBucketsPrivileges({
+            checked: true
+        }, 'read');
+
+        //assertions
+        expect(nfRegistryApi.getPolicyActionResource.calls.count()).toBe(1);
+        expect(nfRegistryApi.putPolicyActionResource.calls.count()).toBe(1);
+        expect(nfRegistryApi.getUserGroup.calls.count()).toBe(2);
+    }));
+
+    it('should toggle to remove the manage bucket privileges for the current group', ngCoreTesting.fakeAsync(function () {
+        // Spy
+        spyOn(nfRegistryApi, 'getPolicyActionResource').and.callFake(function () {
+        }).and.returnValue(rxjs.Observable.of({
+            status: 400,
+            userGroups: [
+                {
+                    identifier: '123',
+                    identity: 'Group #1',
+                    resourcePermissions: {
+                        anyTopLevelResource: {
+                            canRead: false,
+                            canWrite: false,
+                            canDelete: false
+                        },
+                        buckets: {
+                            canRead: false,
+                            canWrite: false,
+                            canDelete: false
+                        },
+                        tenants: {
+                            canRead: false,
+                            canWrite: false,
+                            canDelete: false
+                        },
+                        policies: {
+                            canRead: false,
+                            canWrite: false,
+                            canDelete: false
+                        },
+                        proxy: {
+                            canRead: false,
+                            canWrite: false,
+                            canDelete: false
+                        }
+                    }
+                }
+            ]
+        }));
+        spyOn(nfRegistryApi, 'putPolicyActionResource').and.callFake(function () {
+        }).and.returnValue(rxjs.Observable.of({}));
+        spyOn(nfRegistryApi, 'getUserGroup').and.callFake(function () {
+        }).and.returnValue(rxjs.Observable.of({
+            identifier: '123',
+            identity: 'Group #1',
+            resourcePermissions: {
+                anyTopLevelResource: {
+                    canRead: false,
+                    canWrite: false,
+                    canDelete: false
+                },
+                buckets: {
+                    canRead: false,
+                    canWrite: false,
+                    canDelete: false
+                },
+                tenants: {
+                    canRead: false,
+                    canWrite: false,
+                    canDelete: false
+                },
+                policies: {
+                    canRead: false,
+                    canWrite: false,
+                    canDelete: false
+                },
+                proxy: {
+                    canRead: false,
+                    canWrite: false,
+                    canDelete: false
+                }
+            },
+            users: [{
+                identifier: '123',
+                identity: 'User #1',
+                resourcePermissions: {
+                    anyTopLevelResource: {
+                        canRead: false,
+                        canWrite: false,
+                        canDelete: false
+                    },
+                    buckets: {
+                        canRead: false,
+                        canWrite: false,
+                        canDelete: false
+                    },
+                    tenants: {
+                        canRead: false,
+                        canWrite: false,
+                        canDelete: false
+                    },
+                    policies: {
+                        canRead: false,
+                        canWrite: false,
+                        canDelete: false
+                    },
+                    proxy: {
+                        canRead: false,
+                        canWrite: false,
+                        canDelete: false
+                    }
+                }
+            }]
+        }));
+
+        // 1st change detection triggers ngOnInit
+        fixture.detectChanges();
+        // wait for async calls
+        ngCoreTesting.tick();
+        // 2nd change detection completes after the getUserGroup calls
+        fixture.detectChanges();
+
+        //assertions
+        var getUserGroupCall = nfRegistryApi.getUserGroup.calls.first();
+        expect(getUserGroupCall.args[0]).toBe('123');
+        expect(nfRegistryApi.getUserGroup.calls.count()).toBe(1);
+
+        // the function to test
+        comp.toggleGroupManageBucketsPrivileges({
+            checked: false
+        }, 'read');
+
+        //assertions
+        expect(nfRegistryApi.getPolicyActionResource.calls.count()).toBe(1);
+        expect(nfRegistryApi.putPolicyActionResource.calls.count()).toBe(1);
+        expect(nfRegistryApi.getUserGroup.calls.count()).toBe(2);
+    }));
+
+    it('should toggle to create the manage proxy privileges for the current group', ngCoreTesting.fakeAsync(function () {
+        // Spy
+        spyOn(nfRegistryApi, 'getPolicyActionResource').and.callFake(function () {
+        }).and.returnValue(rxjs.Observable.of({
+            status: 404,
+            userGroups: []
+        }));
+        spyOn(nfRegistryApi, 'postPolicyActionResource').and.callFake(function () {
+        }).and.returnValue(rxjs.Observable.of({
+            userGroups: [
+                {
+                    identifier: '123',
+                    identity: 'Group #1',
+                    resourcePermissions: {
+                        anyTopLevelResource: {
+                            canRead: false,
+                            canWrite: false,
+                            canDelete: false
+                        },
+                        buckets: {
+                            canRead: false,
+                            canWrite: false,
+                            canDelete: false
+                        },
+                        tenants: {
+                            canRead: false,
+                            canWrite: false,
+                            canDelete: false
+                        },
+                        policies: {
+                            canRead: false,
+                            canWrite: false,
+                            canDelete: false
+                        },
+                        proxy: {
+                            canRead: false,
+                            canWrite: false,
+                            canDelete: false
+                        }
+                    }
+                }
+            ]
+        }));
+        spyOn(nfRegistryApi, 'getUserGroup').and.callFake(function () {
+        }).and.returnValue(rxjs.Observable.of({
+            identifier: '123',
+            identity: 'Group #1',
+            resourcePermissions: {
+                anyTopLevelResource: {
+                    canRead: false,
+                    canWrite: false,
+                    canDelete: false
+                },
+                buckets: {
+                    canRead: false,
+                    canWrite: false,
+                    canDelete: false
+                },
+                tenants: {
+                    canRead: false,
+                    canWrite: false,
+                    canDelete: false
+                },
+                policies: {
+                    canRead: false,
+                    canWrite: false,
+                    canDelete: false
+                },
+                proxy: {
+                    canRead: false,
+                    canWrite: false,
+                    canDelete: false
+                }
+            },
+            users: [{
+                identifier: '123',
+                identity: 'User #1',
+                resourcePermissions: {
+                    anyTopLevelResource: {
+                        canRead: false,
+                        canWrite: false,
+                        canDelete: false
+                    },
+                    buckets: {
+                        canRead: false,
+                        canWrite: false,
+                        canDelete: false
+                    },
+                    tenants: {
+                        canRead: false,
+                        canWrite: false,
+                        canDelete: false
+                    },
+                    policies: {
+                        canRead: false,
+                        canWrite: false,
+                        canDelete: false
+                    },
+                    proxy: {
+                        canRead: false,
+                        canWrite: false,
+                        canDelete: false
+                    }
+                }
+            }]
+        }));
+
+        // 1st change detection triggers ngOnInit
+        fixture.detectChanges();
+        // wait for async calls
+        ngCoreTesting.tick();
+        // 2nd change detection completes after the getUserGroup calls
+        fixture.detectChanges();
+
+        //assertions
+        var getUserGroupCall = nfRegistryApi.getUserGroup.calls.first();
+        expect(getUserGroupCall.args[0]).toBe('123');
+        expect(nfRegistryApi.getUserGroup.calls.count()).toBe(1);
+
+        // the function to test
+        comp.toggleGroupManageProxyPrivileges({
+            checked: true
+        }, 'write');
+
+        //assertions
+        expect(nfRegistryApi.getPolicyActionResource.calls.count()).toBe(1);
+        expect(nfRegistryApi.postPolicyActionResource.calls.count()).toBe(1);
+        expect(nfRegistryApi.getUserGroup.calls.count()).toBe(2);
+    }));
+
+    it('should toggle to update the manage proxy privileges for the current group', ngCoreTesting.fakeAsync(function () {
+        // Spy
+        spyOn(nfRegistryApi, 'getPolicyActionResource').and.callFake(function () {
+        }).and.returnValue(rxjs.Observable.of({
+            userGroups: []
+        }));
+        spyOn(nfRegistryApi, 'putPolicyActionResource').and.callFake(function () {
+        }).and.returnValue(rxjs.Observable.of({
+            userGroups: [
+                {
+                    identifier: '123',
+                    identity: 'Group #1',
+                    resourcePermissions: {
+                        anyTopLevelResource: {
+                            canRead: false,
+                            canWrite: false,
+                            canDelete: false
+                        },
+                        buckets: {
+                            canRead: false,
+                            canWrite: false,
+                            canDelete: false
+                        },
+                        tenants: {
+                            canRead: false,
+                            canWrite: false,
+                            canDelete: false
+                        },
+                        policies: {
+                            canRead: false,
+                            canWrite: false,
+                            canDelete: false
+                        },
+                        proxy: {
+                            canRead: false,
+                            canWrite: false,
+                            canDelete: false
+                        }
+                    }
+                }
+            ]
+        }));
+        spyOn(nfRegistryApi, 'getUserGroup').and.callFake(function () {
+        }).and.returnValue(rxjs.Observable.of({
+            identifier: '123',
+            identity: 'Group #1',
+            resourcePermissions: {
+                anyTopLevelResource: {
+                    canRead: false,
+                    canWrite: false,
+                    canDelete: false
+                },
+                buckets: {
+                    canRead: false,
+                    canWrite: false,
+                    canDelete: false
+                },
+                tenants: {
+                    canRead: false,
+                    canWrite: false,
+                    canDelete: false
+                },
+                policies: {
+                    canRead: false,
+                    canWrite: false,
+                    canDelete: false
+                },
+                proxy: {
+                    canRead: false,
+                    canWrite: false,
+                    canDelete: false
+                }
+            },
+            users: [{
+                identifier: '123',
+                identity: 'User #1',
+                resourcePermissions: {
+                    anyTopLevelResource: {
+                        canRead: false,
+                        canWrite: false,
+                        canDelete: false
+                    },
+                    buckets: {
+                        canRead: false,
+                        canWrite: false,
+                        canDelete: false
+                    },
+                    tenants: {
+                        canRead: false,
+                        canWrite: false,
+                        canDelete: false
+                    },
+                    policies: {
+                        canRead: false,
+                        canWrite: false,
+                        canDelete: false
+                    },
+                    proxy: {
+                        canRead: false,
+                        canWrite: false,
+                        canDelete: false
+                    }
+                }
+            }]
+        }));
+
+        // 1st change detection triggers ngOnInit
+        fixture.detectChanges();
+        // wait for async calls
+        ngCoreTesting.tick();
+        // 2nd change detection completes after the getUserGroup calls
+        fixture.detectChanges();
+
+        //assertions
+        var getUserGroupCall = nfRegistryApi.getUserGroup.calls.first();
+        expect(getUserGroupCall.args[0]).toBe('123');
+        expect(nfRegistryApi.getUserGroup.calls.count()).toBe(1);
+
+        // the function to test
+        comp.toggleGroupManageProxyPrivileges({
+            checked: true
+        }, 'write');
+
+        //assertions
+        expect(nfRegistryApi.getPolicyActionResource.calls.count()).toBe(1);
+        expect(nfRegistryApi.putPolicyActionResource.calls.count()).toBe(1);
+        expect(nfRegistryApi.getUserGroup.calls.count()).toBe(2);
+    }));
+
+    it('should toggle to remove the manage proxy privileges for the current group', ngCoreTesting.fakeAsync(function () {
+        // Spy
+        spyOn(nfRegistryApi, 'getPolicyActionResource').and.callFake(function () {
+        }).and.returnValue(rxjs.Observable.of({
+            status: 400,
+            userGroups: [
+                {
+                    identifier: '123',
+                    identity: 'Group #1',
+                    resourcePermissions: {
+                        anyTopLevelResource: {
+                            canRead: false,
+                            canWrite: false,
+                            canDelete: false
+                        },
+                        buckets: {
+                            canRead: false,
+                            canWrite: false,
+                            canDelete: false
+                        },
+                        tenants: {
+                            canRead: false,
+                            canWrite: false,
+                            canDelete: false
+                        },
+                        policies: {
+                            canRead: false,
+                            canWrite: false,
+                            canDelete: false
+                        },
+                        proxy: {
+                            canRead: false,
+                            canWrite: false,
+                            canDelete: false
+                        }
+                    }
+                }
+            ]
+        }));
+        spyOn(nfRegistryApi, 'putPolicyActionResource').and.callFake(function () {
+        }).and.returnValue(rxjs.Observable.of({}));
+        spyOn(nfRegistryApi, 'getUserGroup').and.callFake(function () {
+        }).and.returnValue(rxjs.Observable.of({
+            identifier: '123',
+            identity: 'Group #1',
+            resourcePermissions: {
+                anyTopLevelResource: {
+                    canRead: false,
+                    canWrite: false,
+                    canDelete: false
+                },
+                buckets: {
+                    canRead: false,
+                    canWrite: false,
+                    canDelete: false
+                },
+                tenants: {
+                    canRead: false,
+                    canWrite: false,
+                    canDelete: false
+                },
+                policies: {
+                    canRead: false,
+                    canWrite: false,
+                    canDelete: false
+                },
+                proxy: {
+                    canRead: false,
+                    canWrite: false,
+                    canDelete: false
+                }
+            },
+            users: [{
+                identifier: '123',
+                identity: 'User #1',
+                resourcePermissions: {
+                    anyTopLevelResource: {
+                        canRead: false,
+                        canWrite: false,
+                        canDelete: false
+                    },
+                    buckets: {
+                        canRead: false,
+                        canWrite: false,
+                        canDelete: false
+                    },
+                    tenants: {
+                        canRead: false,
+                        canWrite: false,
+                        canDelete: false
+                    },
+                    policies: {
+                        canRead: false,
+                        canWrite: false,
+                        canDelete: false
+                    },
+                    proxy: {
+                        canRead: false,
+                        canWrite: false,
+                        canDelete: false
+                    }
+                }
+            }]
+        }));
+
+        // 1st change detection triggers ngOnInit
+        fixture.detectChanges();
+        // wait for async calls
+        ngCoreTesting.tick();
+        // 2nd change detection completes after the getUserGroup calls
+        fixture.detectChanges();
+
+        //assertions
+        var getUserGroupCall = nfRegistryApi.getUserGroup.calls.first();
+        expect(getUserGroupCall.args[0]).toBe('123');
+        expect(nfRegistryApi.getUserGroup.calls.count()).toBe(1);
+
+        // the function to test
+        comp.toggleGroupManageProxyPrivileges({
+            checked: false
+        }, 'write');
+
+        //assertions
+        expect(nfRegistryApi.getPolicyActionResource.calls.count()).toBe(1);
+        expect(nfRegistryApi.putPolicyActionResource.calls.count()).toBe(1);
+        expect(nfRegistryApi.getUserGroup.calls.count()).toBe(2);
+    }));
+
+    it('should toggle to create the manage policies privileges for the current group', ngCoreTesting.fakeAsync(function () {
+        // Spy
+        spyOn(nfRegistryApi, 'getPolicyActionResource').and.callFake(function () {
+        }).and.returnValue(rxjs.Observable.of({
+            status: 404,
+            userGroups: []
+        }));
+        spyOn(nfRegistryApi, 'postPolicyActionResource').and.callFake(function () {
+        }).and.returnValue(rxjs.Observable.of({
+            userGroups: [
+                {
+                    identifier: '123',
+                    identity: 'Group #1',
+                    resourcePermissions: {
+                        anyTopLevelResource: {
+                            canRead: false,
+                            canWrite: false,
+                            canDelete: false
+                        },
+                        buckets: {
+                            canRead: false,
+                            canWrite: false,
+                            canDelete: false
+                        },
+                        tenants: {
+                            canRead: false,
+                            canWrite: false,
+                            canDelete: false
+                        },
+                        policies: {
+                            canRead: false,
+                            canWrite: false,
+                            canDelete: false
+                        },
+                        proxy: {
+                            canRead: false,
+                            canWrite: false,
+                            canDelete: false
+                        }
+                    }
+                }
+            ]
+        }));
+        spyOn(nfRegistryApi, 'getUserGroup').and.callFake(function () {
+        }).and.returnValue(rxjs.Observable.of({
+            identifier: '123',
+            identity: 'Group #1',
+            resourcePermissions: {
+                anyTopLevelResource: {
+                    canRead: false,
+                    canWrite: false,
+                    canDelete: false
+                },
+                buckets: {
+                    canRead: false,
+                    canWrite: false,
+                    canDelete: false
+                },
+                tenants: {
+                    canRead: false,
+                    canWrite: false,
+                    canDelete: false
+                },
+                policies: {
+                    canRead: false,
+                    canWrite: false,
+                    canDelete: false
+                },
+                proxy: {
+                    canRead: false,
+                    canWrite: false,
+                    canDelete: false
+                }
+            },
+            users: [{
+                identifier: '123',
+                identity: 'User #1',
+                resourcePermissions: {
+                    anyTopLevelResource: {
+                        canRead: false,
+                        canWrite: false,
+                        canDelete: false
+                    },
+                    buckets: {
+                        canRead: false,
+                        canWrite: false,
+                        canDelete: false
+                    },
+                    tenants: {
+                        canRead: false,
+                        canWrite: false,
+                        canDelete: false
+                    },
+                    policies: {
+                        canRead: false,
+                        canWrite: false,
+                        canDelete: false
+                    },
+                    proxy: {
+                        canRead: false,
+                        canWrite: false,
+                        canDelete: false
+                    }
+                }
+            }]
+        }));
+
+        // 1st change detection triggers ngOnInit
+        fixture.detectChanges();
+        // wait for async calls
+        ngCoreTesting.tick();
+        // 2nd change detection completes after the getUserGroup calls
+        fixture.detectChanges();
+
+        //assertions
+        var getUserGroupCall = nfRegistryApi.getUserGroup.calls.first();
+        expect(getUserGroupCall.args[0]).toBe('123');
+        expect(nfRegistryApi.getUserGroup.calls.count()).toBe(1);
+
+        // the function to test
+        comp.toggleGroupManagePoliciesPrivileges({
+            checked: true
+        }, 'read');
+
+        //assertions
+        expect(nfRegistryApi.getPolicyActionResource.calls.count()).toBe(1);
+        expect(nfRegistryApi.postPolicyActionResource.calls.count()).toBe(1);
+        expect(nfRegistryApi.getUserGroup.calls.count()).toBe(2);
+    }));
+
+    it('should toggle to update the manage policies privileges for the current group', ngCoreTesting.fakeAsync(function () {
+        // Spy
+        spyOn(nfRegistryApi, 'getPolicyActionResource').and.callFake(function () {
+        }).and.returnValue(rxjs.Observable.of({
+            userGroups: []
+        }));
+        spyOn(nfRegistryApi, 'putPolicyActionResource').and.callFake(function () {
+        }).and.returnValue(rxjs.Observable.of({
+            userGroups: [
+                {
+                    identifier: '123',
+                    identity: 'Group #1',
+                    resourcePermissions: {
+                        anyTopLevelResource: {
+                            canRead: false,
+                            canWrite: false,
+                            canDelete: false
+                        },
+                        buckets: {
+                            canRead: false,
+                            canWrite: false,
+                            canDelete: false
+                        },
+                        tenants: {
+                            canRead: false,
+                            canWrite: false,
+                            canDelete: false
+                        },
+                        policies: {
+                            canRead: false,
+                            canWrite: false,
+                            canDelete: false
+                        },
+                        proxy: {
+                            canRead: false,
+                            canWrite: false,
+                            canDelete: false
+                        }
+                    }
+                }
+            ]
+        }));
+        spyOn(nfRegistryApi, 'getUserGroup').and.callFake(function () {
+        }).and.returnValue(rxjs.Observable.of({
+            identifier: '123',
+            identity: 'Group #1',
+            resourcePermissions: {
+                anyTopLevelResource: {
+                    canRead: false,
+                    canWrite: false,
+                    canDelete: false
+                },
+                buckets: {
+                    canRead: false,
+                    canWrite: false,
+                    canDelete: false
+                },
+                tenants: {
+                    canRead: false,
+                    canWrite: false,
+                    canDelete: false
+                },
+                policies: {
+                    canRead: false,
+                    canWrite: false,
+                    canDelete: false
+                },
+                proxy: {
+                    canRead: false,
+                    canWrite: false,
+                    canDelete: false
+                }
+            },
+            users: [{
+                identifier: '123',
+                identity: 'User #1',
+                resourcePermissions: {
+                    anyTopLevelResource: {
+                        canRead: false,
+                        canWrite: false,
+                        canDelete: false
+                    },
+                    buckets: {
+                        canRead: false,
+                        canWrite: false,
+                        canDelete: false
+                    },
+                    tenants: {
+                        canRead: false,
+                        canWrite: false,
+                        canDelete: false
+                    },
+                    policies: {
+                        canRead: false,
+                        canWrite: false,
+                        canDelete: false
+                    },
+                    proxy: {
+                        canRead: false,
+                        canWrite: false,
+                        canDelete: false
+                    }
+                }
+            }]
+        }));
+
+        // 1st change detection triggers ngOnInit
+        fixture.detectChanges();
+        // wait for async calls
+        ngCoreTesting.tick();
+        // 2nd change detection completes after the getUserGroup calls
+        fixture.detectChanges();
+
+        //assertions
+        var getUserGroupCall = nfRegistryApi.getUserGroup.calls.first();
+        expect(getUserGroupCall.args[0]).toBe('123');
+        expect(nfRegistryApi.getUserGroup.calls.count()).toBe(1);
+
+        // the function to test
+        comp.toggleGroupManagePoliciesPrivileges({
+            checked: true
+        }, 'read');
+
+        //assertions
+        expect(nfRegistryApi.getPolicyActionResource.calls.count()).toBe(1);
+        expect(nfRegistryApi.putPolicyActionResource.calls.count()).toBe(1);
+        expect(nfRegistryApi.getUserGroup.calls.count()).toBe(2);
+    }));
+
+    it('should toggle to remove the manage policies privileges for the current group', ngCoreTesting.fakeAsync(function () {
+        // Spy
+        spyOn(nfRegistryApi, 'getPolicyActionResource').and.callFake(function () {
+        }).and.returnValue(rxjs.Observable.of({
+            status: 400,
+            userGroups: [
+                {
+                    identifier: '123',
+                    identity: 'Group #1',
+                    resourcePermissions: {
+                        anyTopLevelResource: {
+                            canRead: false,
+                            canWrite: false,
+                            canDelete: false
+                        },
+                        buckets: {
+                            canRead: false,
+                            canWrite: false,
+                            canDelete: false
+                        },
+                        tenants: {
+                            canRead: false,
+                            canWrite: false,
+                            canDelete: false
+                        },
+                        policies: {
+                            canRead: false,
+                            canWrite: false,
+                            canDelete: false
+                        },
+                        proxy: {
+                            canRead: false,
+                            canWrite: false,
+                            canDelete: false
+                        }
+                    }
+                }
+            ]
+        }));
+        spyOn(nfRegistryApi, 'putPolicyActionResource').and.callFake(function () {
+        }).and.returnValue(rxjs.Observable.of({}));
+        spyOn(nfRegistryApi, 'getUserGroup').and.callFake(function () {
+        }).and.returnValue(rxjs.Observable.of({
+            identifier: '123',
+            identity: 'Group #1',
+            resourcePermissions: {
+                anyTopLevelResource: {
+                    canRead: false,
+                    canWrite: false,
+                    canDelete: false
+                },
+                buckets: {
+                    canRead: false,
+                    canWrite: false,
+                    canDelete: false
+                },
+                tenants: {
+                    canRead: false,
+                    canWrite: false,
+                    canDelete: false
+                },
+                policies: {
+                    canRead: false,
+                    canWrite: false,
+                    canDelete: false
+                },
+                proxy: {
+                    canRead: false,
+                    canWrite: false,
+                    canDelete: false
+                }
+            },
+            users: [{
+                identifier: '123',
+                identity: 'User #1',
+                resourcePermissions: {
+                    anyTopLevelResource: {
+                        canRead: false,
+                        canWrite: false,
+                        canDelete: false
+                    },
+                    buckets: {
+                        canRead: false,
+                        canWrite: false,
+                        canDelete: false
+                    },
+                    tenants: {
+                        canRead: false,
+                        canWrite: false,
+                        canDelete: false
+                    },
+                    policies: {
+                        canRead: false,
+                        canWrite: false,
+                        canDelete: false
+                    },
+                    proxy: {
+                        canRead: false,
+                        canWrite: false,
+                        canDelete: false
+                    }
+                }
+            }]
+        }));
+
+        // 1st change detection triggers ngOnInit
+        fixture.detectChanges();
+        // wait for async calls
+        ngCoreTesting.tick();
+        // 2nd change detection completes after the getUserGroup calls
+        fixture.detectChanges();
+
+        //assertions
+        var getUserGroupCall = nfRegistryApi.getUserGroup.calls.first();
+        expect(getUserGroupCall.args[0]).toBe('123');
+        expect(nfRegistryApi.getUserGroup.calls.count()).toBe(1);
+
+        // the function to test
+        comp.toggleGroupManagePoliciesPrivileges({
+            checked: false
+        }, 'read');
+
+        //assertions
+        expect(nfRegistryApi.getPolicyActionResource.calls.count()).toBe(1);
+        expect(nfRegistryApi.putPolicyActionResource.calls.count()).toBe(1);
+        expect(nfRegistryApi.getUserGroup.calls.count()).toBe(2);
+    }));
+
+    it('should toggle to create the manage tenants privileges for the current group', ngCoreTesting.fakeAsync(function () {
+        // Spy
+        spyOn(nfRegistryApi, 'getPolicyActionResource').and.callFake(function () {
+        }).and.returnValue(rxjs.Observable.of({
+            status: 404,
+            userGroups: []
+        }));
+        spyOn(nfRegistryApi, 'postPolicyActionResource').and.callFake(function () {
+        }).and.returnValue(rxjs.Observable.of({
+            userGroups: [
+                {
+                    identifier: '123',
+                    identity: 'Group #1',
+                    resourcePermissions: {
+                        anyTopLevelResource: {
+                            canRead: false,
+                            canWrite: false,
+                            canDelete: false
+                        },
+                        buckets: {
+                            canRead: false,
+                            canWrite: false,
+                            canDelete: false
+                        },
+                        tenants: {
+                            canRead: false,
+                            canWrite: false,
+                            canDelete: false
+                        },
+                        policies: {
+                            canRead: false,
+                            canWrite: false,
+                            canDelete: false
+                        },
+                        proxy: {
+                            canRead: false,
+                            canWrite: false,
+                            canDelete: false
+                        }
+                    }
+                }
+            ]
+        }));
+        spyOn(nfRegistryApi, 'getUserGroup').and.callFake(function () {
+        }).and.returnValue(rxjs.Observable.of({
+            identifier: '123',
+            identity: 'Group #1',
+            resourcePermissions: {
+                anyTopLevelResource: {
+                    canRead: false,
+                    canWrite: false,
+                    canDelete: false
+                },
+                buckets: {
+                    canRead: false,
+                    canWrite: false,
+                    canDelete: false
+                },
+                tenants: {
+                    canRead: false,
+                    canWrite: false,
+                    canDelete: false
+                },
+                policies: {
+                    canRead: false,
+                    canWrite: false,
+                    canDelete: false
+                },
+                proxy: {
+                    canRead: false,
+                    canWrite: false,
+                    canDelete: false
+                }
+            },
+            users: [{
+                identifier: '123',
+                identity: 'User #1',
+                resourcePermissions: {
+                    anyTopLevelResource: {
+                        canRead: false,
+                        canWrite: false,
+                        canDelete: false
+                    },
+                    buckets: {
+                        canRead: false,
+                        canWrite: false,
+                        canDelete: false
+                    },
+                    tenants: {
+                        canRead: false,
+                        canWrite: false,
+                        canDelete: false
+                    },
+                    policies: {
+                        canRead: false,
+                        canWrite: false,
+                        canDelete: false
+                    },
+                    proxy: {
+                        canRead: false,
+                        canWrite: false,
+                        canDelete: false
+                    }
+                }
+            }]
+        }));
+
+        // 1st change detection triggers ngOnInit
+        fixture.detectChanges();
+        // wait for async calls
+        ngCoreTesting.tick();
+        // 2nd change detection completes after the getUserGroup calls
+        fixture.detectChanges();
+
+        //assertions
+        var getUserGroupCall = nfRegistryApi.getUserGroup.calls.first();
+        expect(getUserGroupCall.args[0]).toBe('123');
+        expect(nfRegistryApi.getUserGroup.calls.count()).toBe(1);
+
+        // the function to test
+        comp.toggleGroupManageTenantsPrivileges({
+            checked: true
+        }, 'read');
+
+        //assertions
+        expect(nfRegistryApi.getPolicyActionResource.calls.count()).toBe(1);
+        expect(nfRegistryApi.postPolicyActionResource.calls.count()).toBe(1);
+        expect(nfRegistryApi.getUserGroup.calls.count()).toBe(2);
+    }));
+
+    it('should toggle to update the manage tenants privileges for the current group', ngCoreTesting.fakeAsync(function () {
+        // Spy
+        spyOn(nfRegistryApi, 'getPolicyActionResource').and.callFake(function () {
+        }).and.returnValue(rxjs.Observable.of({
+            userGroups: []
+        }));
+        spyOn(nfRegistryApi, 'putPolicyActionResource').and.callFake(function () {
+        }).and.returnValue(rxjs.Observable.of({
+            userGroups: [
+                {
+                    identifier: '123',
+                    identity: 'Group #1',
+                    resourcePermissions: {
+                        anyTopLevelResource: {
+                            canRead: false,
+                            canWrite: false,
+                            canDelete: false
+                        },
+                        buckets: {
+                            canRead: false,
+                            canWrite: false,
+                            canDelete: false
+                        },
+                        tenants: {
+                            canRead: false,
+                            canWrite: false,
+                            canDelete: false
+                        },
+                        policies: {
+                            canRead: false,
+                            canWrite: false,
+                            canDelete: false
+                        },
+                        proxy: {
+                            canRead: false,
+                            canWrite: false,
+                            canDelete: false
+                        }
+                    }
+                }
+            ]
+        }));
+        spyOn(nfRegistryApi, 'getUserGroup').and.callFake(function () {
+        }).and.returnValue(rxjs.Observable.of({
+            identifier: '123',
+            identity: 'Group #1',
+            resourcePermissions: {
+                anyTopLevelResource: {
+                    canRead: false,
+                    canWrite: false,
+                    canDelete: false
+                },
+                buckets: {
+                    canRead: false,
+                    canWrite: false,
+                    canDelete: false
+                },
+                tenants: {
+                    canRead: false,
+                    canWrite: false,
+                    canDelete: false
+                },
+                policies: {
+                    canRead: false,
+                    canWrite: false,
+                    canDelete: false
+                },
+                proxy: {
+                    canRead: false,
+                    canWrite: false,
+                    canDelete: false
+                }
+            },
+            users: [{
+                identifier: '123',
+                identity: 'User #1',
+                resourcePermissions: {
+                    anyTopLevelResource: {
+                        canRead: false,
+                        canWrite: false,
+                        canDelete: false
+                    },
+                    buckets: {
+                        canRead: false,
+                        canWrite: false,
+                        canDelete: false
+                    },
+                    tenants: {
+                        canRead: false,
+                        canWrite: false,
+                        canDelete: false
+                    },
+                    policies: {
+                        canRead: false,
+                        canWrite: false,
+                        canDelete: false
+                    },
+                    proxy: {
+                        canRead: false,
+                        canWrite: false,
+                        canDelete: false
+                    }
+                }
+            }]
+        }));
+
+        // 1st change detection triggers ngOnInit
+        fixture.detectChanges();
+        // wait for async calls
+        ngCoreTesting.tick();
+        // 2nd change detection completes after the getUserGroup calls
+        fixture.detectChanges();
+
+        //assertions
+        var getUserGroupCall = nfRegistryApi.getUserGroup.calls.first();
+        expect(getUserGroupCall.args[0]).toBe('123');
+        expect(nfRegistryApi.getUserGroup.calls.count()).toBe(1);
+
+        // the function to test
+        comp.toggleGroupManageTenantsPrivileges({
+            checked: true
+        }, 'read');
+
+        //assertions
+        expect(nfRegistryApi.getPolicyActionResource.calls.count()).toBe(1);
+        expect(nfRegistryApi.putPolicyActionResource.calls.count()).toBe(1);
+        expect(nfRegistryApi.getUserGroup.calls.count()).toBe(2);
+    }));
+
+    it('should toggle to remove the manage tenants privileges for the current group', ngCoreTesting.fakeAsync(function () {
+        // Spy
+        spyOn(nfRegistryApi, 'getPolicyActionResource').and.callFake(function () {
+        }).and.returnValue(rxjs.Observable.of({
+            status: 400,
+            userGroups: [
+                {
+                    identifier: '123',
+                    identity: 'Group #1',
+                    resourcePermissions: {
+                        anyTopLevelResource: {
+                            canRead: false,
+                            canWrite: false,
+                            canDelete: false
+                        },
+                        buckets: {
+                            canRead: false,
+                            canWrite: false,
+                            canDelete: false
+                        },
+                        tenants: {
+                            canRead: false,
+                            canWrite: false,
+                            canDelete: false
+                        },
+                        policies: {
+                            canRead: false,
+                            canWrite: false,
+                            canDelete: false
+                        },
+                        proxy: {
+                            canRead: false,
+                            canWrite: false,
+                            canDelete: false
+                        }
+                    }
+                }
+            ]
+        }));
+        spyOn(nfRegistryApi, 'putPolicyActionResource').and.callFake(function () {
+        }).and.returnValue(rxjs.Observable.of({}));
+        spyOn(nfRegistryApi, 'getUserGroup').and.callFake(function () {
+        }).and.returnValue(rxjs.Observable.of({
+            identifier: '123',
+            identity: 'Group #1',
+            resourcePermissions: {
+                anyTopLevelResource: {
+                    canRead: false,
+                    canWrite: false,
+                    canDelete: false
+                },
+                buckets: {
+                    canRead: false,
+                    canWrite: false,
+                    canDelete: false
+                },
+                tenants: {
+                    canRead: false,
+                    canWrite: false,
+                    canDelete: false
+                },
+                policies: {
+                    canRead: false,
+                    canWrite: false,
+                    canDelete: false
+                },
+                proxy: {
+                    canRead: false,
+                    canWrite: false,
+                    canDelete: false
+                }
+            },
+            users: [{
+                identifier: '123',
+                identity: 'User #1',
+                resourcePermissions: {
+                    anyTopLevelResource: {
+                        canRead: false,
+                        canWrite: false,
+                        canDelete: false
+                    },
+                    buckets: {
+                        canRead: false,
+                        canWrite: false,
+                        canDelete: false
+                    },
+                    tenants: {
+                        canRead: false,
+                        canWrite: false,
+                        canDelete: false
+                    },
+                    policies: {
+                        canRead: false,
+                        canWrite: false,
+                        canDelete: false
+                    },
+                    proxy: {
+                        canRead: false,
+                        canWrite: false,
+                        canDelete: false
+                    }
+                }
+            }]
+        }));
+
+        // 1st change detection triggers ngOnInit
+        fixture.detectChanges();
+        // wait for async calls
+        ngCoreTesting.tick();
+        // 2nd change detection completes after the getUserGroup calls
+        fixture.detectChanges();
+
+        //assertions
+        var getUserGroupCall = nfRegistryApi.getUserGroup.calls.first();
+        expect(getUserGroupCall.args[0]).toBe('123');
+        expect(nfRegistryApi.getUserGroup.calls.count()).toBe(1);
+
+        // the function to test
+        comp.toggleGroupManageTenantsPrivileges({
+            checked: false
+        }, 'read');
+
+        //assertions
+        expect(nfRegistryApi.getPolicyActionResource.calls.count()).toBe(1);
+        expect(nfRegistryApi.putPolicyActionResource.calls.count()).toBe(1);
+        expect(nfRegistryApi.getUserGroup.calls.count()).toBe(2);
+    }));
+
+    it('should open a modal dialog UX enabling the addition of the current user to a group(s)', ngCoreTesting.fakeAsync(function () {
+        // Spy
+        spyOn(comp, 'filterUsers').and.callFake(function () {
+        });
+        spyOn(comp.dialog, 'open').and.callFake(function () {
+            return {
+                afterClosed: function () {
+                    return rxjs.Observable.of({});
+                }
+            }
+        });
+        spyOn(nfRegistryApi, 'getUserGroup').and.callFake(function () {
+        }).and.returnValue(rxjs.Observable.of({
+            identifier: '123',
+            identity: 'Group #1',
+            resourcePermissions: {
+                anyTopLevelResource: {
+                    canRead: false,
+                    canWrite: false,
+                    canDelete: false
+                },
+                buckets: {
+                    canRead: false,
+                    canWrite: false,
+                    canDelete: false
+                },
+                tenants: {
+                    canRead: false,
+                    canWrite: false,
+                    canDelete: false
+                },
+                policies: {
+                    canRead: false,
+                    canWrite: false,
+                    canDelete: false
+                },
+                proxy: {
+                    canRead: false,
+                    canWrite: false,
+                    canDelete: false
+                }
+            },
+            users: [{
+                identifier: '123',
+                identity: 'User #1',
+                resourcePermissions: {
+                    anyTopLevelResource: {
+                        canRead: false,
+                        canWrite: false,
+                        canDelete: false
+                    },
+                    buckets: {
+                        canRead: false,
+                        canWrite: false,
+                        canDelete: false
+                    },
+                    tenants: {
+                        canRead: false,
+                        canWrite: false,
+                        canDelete: false
+                    },
+                    policies: {
+                        canRead: false,
+                        canWrite: false,
+                        canDelete: false
+                    },
+                    proxy: {
+                        canRead: false,
+                        canWrite: false,
+                        canDelete: false
+                    }
+                }
+            }]
+        }));
+        // 1st change detection triggers ngOnInit
+        fixture.detectChanges();
+        // wait for async calls
+        ngCoreTesting.tick();
+        // 2nd change detection completes after the getUserGroup calls
+        fixture.detectChanges();
+
+        //assertions
+        var getUserGroupCall = nfRegistryApi.getUserGroup.calls.first();
+        expect(getUserGroupCall.args[0]).toBe('123');
+        expect(nfRegistryApi.getUserGroup.calls.count()).toBe(1);
+
+        // the function to test
+        comp.addUsersToGroup();
+
+        //assertions
+        expect(nfRegistryApi.getUserGroup.calls.count()).toBe(2);
+        expect(comp.filterUsers).toHaveBeenCalled();
+    }));
+
+    it('should sort `users` by `column`', ngCoreTesting.fakeAsync(function () {
+        spyOn(comp, 'filterUsers').and.callFake(function () {
+        });
+        spyOn(nfRegistryApi, 'getUserGroup').and.callFake(function () {
+        }).and.returnValue(rxjs.Observable.of({
+            identifier: '123',
+            identity: 'Group #1',
+            resourcePermissions: {
+                anyTopLevelResource: {
+                    canRead: false,
+                    canWrite: false,
+                    canDelete: false
+                },
+                buckets: {
+                    canRead: false,
+                    canWrite: false,
+                    canDelete: false
+                },
+                tenants: {
+                    canRead: false,
+                    canWrite: false,
+                    canDelete: false
+                },
+                policies: {
+                    canRead: false,
+                    canWrite: false,
+                    canDelete: false
+                },
+                proxy: {
+                    canRead: false,
+                    canWrite: false,
+                    canDelete: false
+                }
+            },
+            users: [{
+                identifier: '123',
+                identity: 'User #1',
+                resourcePermissions: {
+                    anyTopLevelResource: {
+                        canRead: false,
+                        canWrite: false,
+                        canDelete: false
+                    },
+                    buckets: {
+                        canRead: false,
+                        canWrite: false,
+                        canDelete: false
+                    },
+                    tenants: {
+                        canRead: false,
+                        canWrite: false,
+                        canDelete: false
+                    },
+                    policies: {
+                        canRead: false,
+                        canWrite: false,
+                        canDelete: false
+                    },
+                    proxy: {
+                        canRead: false,
+                        canWrite: false,
+                        canDelete: false
+                    }
+                }
+            }]
+        }));
+
+        // 1st change detection triggers ngOnInit
+        fixture.detectChanges();
+        // wait for async calls
+        ngCoreTesting.tick();
+        // 2nd change detection completes after the async calls
+        fixture.detectChanges();
+
+        // object to be updated by the test
+        var column = {name: 'name', label: 'Display Name', sortable: true};
+
+        // The function to test
+        comp.sortUsers(column);
+
+        //assertions
+        expect(column.active).toBe(true);
+        var filterUsersCall = comp.filterUsers.calls.first();
+        expect(filterUsersCall.args[0]).toBeUndefined();
+        expect(filterUsersCall.args[1]).toBeUndefined();
+    }));
+
+    it('should remove user from group', ngCoreTesting.fakeAsync(function () {
+        // Spy
+        spyOn(comp, 'filterUsers').and.callFake(function () {
+        });
+        spyOn(comp.snackBarService, 'openCoaster');
+        spyOn(nfRegistryApi, 'getUserGroup').and.callFake(function () {
+        }).and.returnValue(rxjs.Observable.of({
+            identifier: '123',
+            identity: 'Group #1',
+            resourcePermissions: {
+                anyTopLevelResource: {
+                    canRead: false,
+                    canWrite: false,
+                    canDelete: false
+                },
+                buckets: {
+                    canRead: false,
+                    canWrite: false,
+                    canDelete: false
+                },
+                tenants: {
+                    canRead: false,
+                    canWrite: false,
+                    canDelete: false
+                },
+                policies: {
+                    canRead: false,
+                    canWrite: false,
+                    canDelete: false
+                },
+                proxy: {
+                    canRead: false,
+                    canWrite: false,
+                    canDelete: false
+                }
+            },
+            users: [{
+                identifier: '123',
+                identity: 'User #1',
+                resourcePermissions: {
+                    anyTopLevelResource: {
+                        canRead: false,
+                        canWrite: false,
+                        canDelete: false
+                    },
+                    buckets: {
+                        canRead: false,
+                        canWrite: false,
+                        canDelete: false
+                    },
+                    tenants: {
+                        canRead: false,
+                        canWrite: false,
+                        canDelete: false
+                    },
+                    policies: {
+                        canRead: false,
+                        canWrite: false,
+                        canDelete: false
+                    },
+                    proxy: {
+                        canRead: false,
+                        canWrite: false,
+                        canDelete: false
+                    }
+                }
+            }]
+        }));
+        spyOn(nfRegistryApi, 'updateUserGroup').and.callFake(function () {
+        }).and.returnValue(rxjs.Observable.of({}));
+        spyOn(comp.router, 'navigateByUrl').and.callFake(function () {
+        });
+        // 1st change detection triggers ngOnInit
+        fixture.detectChanges();
+        // wait for async calls
+        ngCoreTesting.tick();
+        // 2nd change detection completes after the getUserGroup calls
+        fixture.detectChanges();
+
+        //assertions
+        var getUserGroupCall = nfRegistryApi.getUserGroup.calls.first();
+        expect(getUserGroupCall.args[0]).toBe('123');
+        expect(nfRegistryApi.getUserGroup.calls.count()).toBe(1);
+
+        var user = {
+            identifier: '123'
+        };
+
+        // the function to test
+        comp.removeUserFromGroup(user);
+
+        //assertions
+        expect(nfRegistryApi.getUserGroup.calls.count()).toBe(2);
+        expect(nfRegistryApi.updateUserGroup.calls.count()).toBe(1);
+        expect(comp.snackBarService.openCoaster.calls.count()).toBe(1);
+        expect(comp.filterUsers).toHaveBeenCalled();
+    }));
+
+    it('should update group name', ngCoreTesting.fakeAsync(function () {
+        // Spy
+        spyOn(comp.dialogService, 'openConfirm').and.callFake(function () {
+            return {
+                afterClosed: function () {
+                    return rxjs.Observable.of(true);
+                }
+            }
+        });
+        spyOn(comp.snackBarService, 'openCoaster');
+        spyOn(nfRegistryApi, 'getUserGroup').and.callFake(function () {
+        }).and.returnValue(rxjs.Observable.of({
+            identifier: '123',
+            identity: 'Group #1',
+            resourcePermissions: {
+                anyTopLevelResource: {
+                    canRead: false,
+                    canWrite: false,
+                    canDelete: false
+                },
+                buckets: {
+                    canRead: false,
+                    canWrite: false,
+                    canDelete: false
+                },
+                tenants: {
+                    canRead: false,
+                    canWrite: false,
+                    canDelete: false
+                },
+                policies: {
+                    canRead: false,
+                    canWrite: false,
+                    canDelete: false
+                },
+                proxy: {
+                    canRead: false,
+                    canWrite: false,
+                    canDelete: false
+                }
+            },
+            users: [{
+                identifier: '123',
+                identity: 'User #1',
+                resourcePermissions: {
+                    anyTopLevelResource: {
+                        canRead: false,
+                        canWrite: false,
+                        canDelete: false
+                    },
+                    buckets: {
+                        canRead: false,
+                        canWrite: false,
+                        canDelete: false
+                    },
+                    tenants: {
+                        canRead: false,
+                        canWrite: false,
+                        canDelete: false
+                    },
+                    policies: {
+                        canRead: false,
+                        canWrite: false,
+                        canDelete: false
+                    },
+                    proxy: {
+                        canRead: false,
+                        canWrite: false,
+                        canDelete: false
+                    }
+                }
+            }]
+        }));
+        spyOn(nfRegistryApi, 'updateUserGroup').and.callFake(function () {
+        }).and.returnValue(rxjs.Observable.of({
+            identifier: '123',
+            identity: 'test',
+            status: 200
+        }));
+
+        // 1st change detection triggers ngOnInit
+        fixture.detectChanges();
+        // wait for async calls
+        ngCoreTesting.tick();
+        // 2nd change detection completes after the getUserGroup calls
+        fixture.detectChanges();
+
+        //assertions
+        var getUserGroupCall = nfRegistryApi.getUserGroup.calls.first();
+        expect(getUserGroupCall.args[0]).toBe('123');
+        expect(nfRegistryApi.getUserGroup.calls.count()).toBe(1);
+
+        // the function to test
+        comp.updateGroupName('test');
+
+        //assertions
+        expect(comp.snackBarService.openCoaster.calls.count()).toBe(1);
+        expect(comp.nfRegistryService.group.identity).toBe('test');
+    }));
+
+    it('should fail to update group name (409)', ngCoreTesting.fakeAsync(function () {
+        // Spy
+        spyOn(comp.dialogService, 'openConfirm').and.callFake(function () {
+            return {
+                afterClosed: function () {
+                    return rxjs.Observable.of(true);
+                }
+            }
+        });
+        spyOn(comp.snackBarService, 'openCoaster');
+        spyOn(nfRegistryApi, 'getUserGroup').and.callFake(function () {
+        }).and.returnValue(rxjs.Observable.of({
+            identifier: '123',
+            identity: 'Group #1',
+            resourcePermissions: {
+                anyTopLevelResource: {
+                    canRead: false,
+                    canWrite: false,
+                    canDelete: false
+                },
+                buckets: {
+                    canRead: false,
+                    canWrite: false,
+                    canDelete: false
+                },
+                tenants: {
+                    canRead: false,
+                    canWrite: false,
+                    canDelete: false
+                },
+                policies: {
+                    canRead: false,
+                    canWrite: false,
+                    canDelete: false
+                },
+                proxy: {
+             

<TRUNCATED>

[13/51] [partial] nifi-registry git commit: NIFIREG-201 Refactoring project structure to better isolate extensions

Posted by kd...@apache.org.
http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/6f26290d/nifi-registry-core/nifi-registry-web-api/src/main/resources/META-INF/NOTICE
----------------------------------------------------------------------
diff --git a/nifi-registry-core/nifi-registry-web-api/src/main/resources/META-INF/NOTICE b/nifi-registry-core/nifi-registry-web-api/src/main/resources/META-INF/NOTICE
new file mode 100644
index 0000000..737dad8
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-web-api/src/main/resources/META-INF/NOTICE
@@ -0,0 +1,184 @@
+nifi-registry-web-api
+Copyright 2014-2017 The Apache Software Foundation
+
+This product includes software developed at
+The Apache Software Foundation (http://www.apache.org/).
+
+===========================================
+Apache Software License v2
+===========================================
+
+The following binary components are provided under the Apache Software License v2
+
+  (ASLv2) Apache Commons Lang
+    The following NOTICE information applies:
+      Apache Commons Lang
+      Copyright 2001-2017 The Apache Software Foundation
+
+      This product includes software from the Spring Framework,
+      under the Apache License 2.0 (see: StringUtils.containsWhitespace())
+
+  (ASLv2) Jackson JSON processor
+    The following NOTICE information applies:
+      # Jackson JSON processor
+
+      Jackson is a high-performance, Free/Open Source JSON processing library.
+      It was originally written by Tatu Saloranta (tatu.saloranta@iki.fi), and has
+      been in development since 2007.
+      It is currently developed by a community of developers, as well as supported
+      commercially by FasterXML.com.
+
+      ## Licensing
+
+      Jackson core and extension components may licensed under different licenses.
+      To find the details that apply to this artifact see the accompanying LICENSE file.
+      For more information, including possible other licensing options, contact
+      FasterXML.com (http://fasterxml.com).
+
+      ## Credits
+
+      A list of contributors may be found from CREDITS file, which is included
+      in some artifacts (usually source distributions); but is always available
+      from the source code management (SCM) system project uses.
+
+  (ASLv2) Classmate
+    The following NOTICE information applies
+        Java ClassMate library was originally written by Tatu Saloranta (tatu.saloranta@iki.fi)
+
+        Other developers who have contributed code are:
+
+        * Brian Langel
+
+  (ASLv2) Apache Commons IO
+    The following NOTICE information applies:
+      Apache Commons IO
+      Copyright 2002-2016 The Apache Software Foundation
+
+  (ASLv2) Apache log4j
+    The following NOTICE information applies:
+      Apache log4j
+      Copyright 2010 The Apache Software Foundation
+
+   (ASLv2) Spring Framework
+     The following NOTICE information applies:
+       Spring Framework 5.0.2.RELEASE
+       Copyright (c) 2002-2017 Pivotal, Inc.
+
+   (ASLv2) Spring Security
+     The following NOTICE information applies:
+       Spring Framework 5.0.5.RELEASE
+       Copyright (c) 2002-2017 Pivotal, Inc.
+
+       This product includes software developed by Spring Security
+       Project (http://www.springframework.org/security).
+
+   (ASLv2) Spring LDAP
+      The following NOTICE information applies:
+        Spring LDAP 2.3.2.RELEASE
+        Copyright (c) 2002-2017 Pivotal, Inc.
+
+        This product includes software developed by the Spring LDAP
+        Project (http://www.springframework.org/ldap).
+
+   (ASLv2) Apache Tomcat Embed EL
+      The following NOTICE information applies:
+        Apache Tomcat
+        Copyright 1999-2017 The Apache Software Foundation
+
+        This product includes software developed at
+        The Apache Software Foundation (http://www.apache.org/).
+
+        This software contains code derived from netty-native
+        developed by the Netty project
+        (http://netty.io, https://github.com/netty/netty-tcnative/)
+        and from finagle-native developed at Twitter
+        (https://github.com/twitter/finagle).
+
+        The Windows Installer is built with the Nullsoft
+        Scriptable Install System (NSIS), which is
+        open source software.  The original software and
+        related information is available at
+        http://nsis.sourceforge.net.
+
+        Java compilation software for JSP pages is provided by the Eclipse
+        JDT Core Batch Compiler component, which is open source software.
+        The original software and related information is available at
+        http://www.eclipse.org/jdt/core/.
+
+        For portions of the Tomcat JNI OpenSSL API and the OpenSSL JSSE integration
+        The org.apache.tomcat.jni and the org.apache.tomcat.net.openssl packages
+        are derivative work originating from the Netty project and the finagle-native
+        project developed at Twitter
+        * Copyright 2014 The Netty Project
+        * Copyright 2014 Twitter
+
+        The original XML Schemas for Java EE Deployment Descriptors:
+         - javaee_5.xsd
+         - javaee_web_services_1_2.xsd
+         - javaee_web_services_client_1_2.xsd
+         - javaee_6.xsd
+         - javaee_web_services_1_3.xsd
+         - javaee_web_services_client_1_3.xsd
+         - jsp_2_2.xsd
+         - web-app_3_0.xsd
+         - web-common_3_0.xsd
+         - web-fragment_3_0.xsd
+         - javaee_7.xsd
+         - javaee_web_services_1_4.xsd
+         - javaee_web_services_client_1_4.xsd
+         - jsp_2_3.xsd
+         - web-app_3_1.xsd
+         - web-common_3_1.xsd
+         - web-fragment_3_1.xsd
+         - javaee_8.xsd
+         - web-app_4_0.xsd
+         - web-common_4_0.xsd
+         - web-fragment_4_0.xsd
+
+        may be obtained from:
+        http://www.oracle.com/webfolder/technetwork/jsc/xml/ns/javaee/index.html
+
+************************
+Common Development and Distribution License 1.1
+************************
+
+The following binary components are provided under the Common Development and Distribution License 1.1. See project link for details.
+
+    (CDDL 1.1) (GPL2 w/ CPE) javax.annotation API (javax.annotation:javax.annotation-api:jar:1.2 - http://jcp.org/en/jsr/detail?id=250)
+    (CDDL 1.1) (GPL2 w/ CPE) aopalliance-repackaged (org.glassfish.hk2.external:aopalliance-repackaged:jar:2.5.0-b42 - https://javaee.github.io/glassfish/)
+    (CDDL 1.1) (GPL2 w/ CPE) asm-all-repackaged (org.glassfish.hk2.external:asm-all-repackaged:jar:2.5.0-b42 - https://javaee.github.io/glassfish/)
+    (CDDL 1.1) (GPL2 w/ CPE) class-model (org.glassfish.hk2:class-model:jar:2.5.0-b42 - https://javaee.github.io/glassfish/)
+    (CDDL 1.1) (GPL2 w/ CPE) config-types (org.glassfish.hk2:config-types:jar:2.5.0-b42 - https://javaee.github.io/glassfish/)
+    (CDDL 1.1) (GPL2 w/ CPE) hk2 (org.glassfish.hk2:hk2:jar:2.5.0-b42 - https://javaee.github.io/glassfish/)
+    (CDDL 1.1) (GPL2 w/ CPE) hk2-api (org.glassfish.hk2:hk2-api:jar:2.5.0-b42 - https://javaee.github.io/glassfish/)
+    (CDDL 1.1) (GPL2 w/ CPE) hk2-utils (org.glassfish.hk2:hk2-utils:jar:2.5.0-b42 - https://javaee.github.io/glassfish/)
+    (CDDL 1.1) (GPL2 w/ CPE) hk2-locator (org.glassfish.hk2:hk2-locator:jar:2.5.0-b42 - https://javaee.github.io/glassfish/)
+    (CDDL 1.1) (GPL2 w/ CPE) hk2-config (org.glassfish.hk2:hk2-config:jar:2.5.0-b42 - https://javaee.github.io/glassfish/)
+    (CDDL 1.1) (GPL2 w/ CPE) hk2-core (org.glassfish.hk2:hk2-core:jar:2.5.0-b42 - https://javaee.github.io/glassfish/)
+    (CDDL 1.1) (GPL2 w/ CPE) hk2-runlevel (org.glassfish.hk2:hk2-runlevel:jar:2.5.0-b42 - https://javaee.github.io/glassfish/)
+    (CDDL 1.1) (GPL2 w/ CPE) spring-bridge (org.glassfish.hk2:spring-bridge:jar:2.5.0-b42 - https://javaee.github.io/glassfish/)
+    (CDDL 1.1) (GPL2 w/ CPE) javax.inject:1 as OSGi bundle (org.glassfish.hk2.external:javax.inject:jar:2.4.0-b25 - https://hk2.java.net/external/javax.inject)
+    (CDDL 1.1) (GPL2 w/ CPE) javax.ws.rs-api (javax.ws.rs:javax.ws.rs-api:jar:2.1 - http://jax-rs-spec.java.net)
+    (CDDL 1.1) (GPL2 w/ CPE) javax.el (org.glassfish:javax.el:jar:3.0.1-b08 - https://github.com/javaee/el-spec)
+    (CDDL 1.1) (GPL2 w/ CPE) jersey-bean-validation (org.glassfish.jersey.ext:jersey-bean-validation:jar:2.26 - https://jersey.github.io/)
+    (CDDL 1.1) (GPL2 w/ CPE) jersey-client (org.glassfish.jersey.core:jersey-client:jar:2.26 - https://jersey.github.io/)
+    (CDDL 1.1) (GPL2 w/ CPE) jersey-common (org.glassfish.jersey.core:jersey-common:jar:2.26 - https://jersey.github.io/)
+    (CDDL 1.1) (GPL2 w/ CPE) jersey-container-servlet-core (org.glassfish.jersey.containers:jersey-container-servlet-core:jar:2.26 - https://jersey.github.io/)
+    (CDDL 1.1) (GPL2 w/ CPE) jersey-entity-filtering (org.glassfish.jersey.ext:jersey-entity-filtering:jar:2.26 - https://jersey.github.io/)
+    (CDDL 1.1) (GPL2 w/ CPE) jersey-hk2 (org.glassfish.jersey.inject:jersey-hk2:jar:2.26 - https://jersey.github.io/)
+    (CDDL 1.1) (GPL2 w/ CPE) jersey-media-jaxb (org.glassfish.jersey.media:jersey-media-jaxb:jar:2.26 - https://jersey.github.io/)
+    (CDDL 1.1) (GPL2 w/ CPE) jersey-media-json-jackson (org.glassfish.jersey.media:jersey-media-json-jackson:jar:2.26 - https://jersey.github.io/)
+    (CDDL 1.1) (GPL2 w/ CPE) jersey-server (org.glassfish.jersey.core:jersey-server:jar:2.26 - https://jersey.github.io/)
+    (CDDL 1.1) (GPL2 w/ CPE) jersey-spring4 (org.glassfish.jersey.ext:jersey-spring4:jar:2.26 - https://jersey.github.io/)
+    (CDDL 1.1) (GPL2 w/ CPE) OSGi resource locator bundle (org.glassfish.hk2:osgi-resource-locator:jar:1.0.1 - http://glassfish.org/osgi-resource-locator)
+
+************************
+Eclipse Public License 1.0
+************************
+
+The following binary components are provided under the Eclipse Public License 1.0.  See project link for details.
+
+    (EPL 1.0)(MPL 2.0) H2 Database (com.h2database:h2:jar:1.3.176 - http://www.h2database.com/html/license.html)
+    (EPL 1.0)(LGPL 2.1) Logback Classic (ch.qos.logback:logback-classic:jar:1.2.3 - http://logback.qos.ch/)
+    (EPL 1.0)(LGPL 2.1) Logback Core (ch.qos.logback:logback-core:jar:1.2.3 - http://logback.qos.ch/)
+    (EPL 1.0) AspectJ Weaver (org.aspectj:aspectjweaver:jar:1.8.13 - http://www.eclipse.org/aspectj/)

http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/6f26290d/nifi-registry-core/nifi-registry-web-api/src/main/resources/META-INF/services/org.apache.nifi.registry.security.authentication.IdentityProvider
----------------------------------------------------------------------
diff --git a/nifi-registry-core/nifi-registry-web-api/src/main/resources/META-INF/services/org.apache.nifi.registry.security.authentication.IdentityProvider b/nifi-registry-core/nifi-registry-web-api/src/main/resources/META-INF/services/org.apache.nifi.registry.security.authentication.IdentityProvider
new file mode 100644
index 0000000..ea80a03
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-web-api/src/main/resources/META-INF/services/org.apache.nifi.registry.security.authentication.IdentityProvider
@@ -0,0 +1,15 @@
+# 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.
+org.apache.nifi.registry.web.security.authentication.kerberos.KerberosIdentityProvider
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/6f26290d/nifi-registry-core/nifi-registry-web-api/src/main/resources/banner.txt
----------------------------------------------------------------------
diff --git a/nifi-registry-core/nifi-registry-web-api/src/main/resources/banner.txt b/nifi-registry-core/nifi-registry-web-api/src/main/resources/banner.txt
new file mode 100644
index 0000000..6fb0ffc
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-web-api/src/main/resources/banner.txt
@@ -0,0 +1,8 @@
+
+  Apache NiFi   _     _
+ _ __ ___  __ _(_)___| |_ _ __ _   _
+| '__/ _ \/ _` | / __| __| '__| | | |
+| | |  __/ (_| | \__ \ |_| |  | |_| |
+|_|  \___|\__, |_|___/\__|_|   \__, |
+==========|___/================|___/=
+               v${application.version}

http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/6f26290d/nifi-registry-core/nifi-registry-web-api/src/main/resources/images/bgNifiLogo.png
----------------------------------------------------------------------
diff --git a/nifi-registry-core/nifi-registry-web-api/src/main/resources/images/bgNifiLogo.png b/nifi-registry-core/nifi-registry-web-api/src/main/resources/images/bgNifiLogo.png
new file mode 100644
index 0000000..2558d43
Binary files /dev/null and b/nifi-registry-core/nifi-registry-web-api/src/main/resources/images/bgNifiLogo.png differ

http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/6f26290d/nifi-registry-core/nifi-registry-web-api/src/main/resources/images/nifi16.ico
----------------------------------------------------------------------
diff --git a/nifi-registry-core/nifi-registry-web-api/src/main/resources/images/nifi16.ico b/nifi-registry-core/nifi-registry-web-api/src/main/resources/images/nifi16.ico
new file mode 100644
index 0000000..2ac3670
Binary files /dev/null and b/nifi-registry-core/nifi-registry-web-api/src/main/resources/images/nifi16.ico differ

http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/6f26290d/nifi-registry-core/nifi-registry-web-api/src/main/resources/swagger/security-definitions.json
----------------------------------------------------------------------
diff --git a/nifi-registry-core/nifi-registry-web-api/src/main/resources/swagger/security-definitions.json b/nifi-registry-core/nifi-registry-web-api/src/main/resources/swagger/security-definitions.json
new file mode 100644
index 0000000..411fb3b
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-web-api/src/main/resources/swagger/security-definitions.json
@@ -0,0 +1,12 @@
+{
+  "BasicAuth": {
+    "type": "basic",
+    "description": "HTTP Basic Auth"
+  },
+  "Authorization": {
+    "type": "apiKey",
+    "name": "Authorization",
+    "in": "header",
+    "description": "NiFi Registry Auth Token (JWT)"
+  }
+}

http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/6f26290d/nifi-registry-core/nifi-registry-web-api/src/main/resources/templates/endpoint.hbs
----------------------------------------------------------------------
diff --git a/nifi-registry-core/nifi-registry-web-api/src/main/resources/templates/endpoint.hbs b/nifi-registry-core/nifi-registry-web-api/src/main/resources/templates/endpoint.hbs
new file mode 100644
index 0000000..1394136
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-web-api/src/main/resources/templates/endpoint.hbs
@@ -0,0 +1,61 @@
+{{!--
+    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.
+--}}
+<div class="endpoints">
+    <span class="path hidden">{{@key}}</span>
+    {{#post}}
+        <div class="endpoint post">
+            <div class="operation-handle">
+                <div class="method">POST</div>
+                <div class="path mono"></div>
+                <div class="summary" title="{{summary}}">{{summary}}</div>
+                <div class="clear"></div>
+            </div>
+            {{> operation}}
+        </div>
+    {{/post}}
+    {{#get}}
+        <div class="endpoint get">
+            <div class="operation-handle">
+                <div class="method">GET</div>
+                <div class="path mono"></div>
+                <div class="summary" title="{{summary}}">{{summary}}</div>
+                <div class="clear"></div>
+            </div>
+            {{> operation}}
+        </div>
+    {{/get}}
+    {{#put}}
+        <div class="endpoint put">
+            <div class="operation-handle">
+                <div class="method">PUT</div>
+                <div class="path mono"></div>
+                <div class="summary" title="{{summary}}">{{summary}}</div>
+                <div class="clear"></div>
+            </div>
+            {{> operation}}
+        </div>
+    {{/put}}
+    {{#delete}}
+        <div class="endpoint delete">
+            <div class="operation-handle">
+                <div class="method">DELETE</div>
+                <div class="path mono"></div>
+                <div class="summary" title="{{summary}}">{{summary}}</div>
+                <div class="clear"></div>
+            </div>
+            {{> operation}}
+        </div>
+    {{/delete}}
+</div>
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/6f26290d/nifi-registry-core/nifi-registry-web-api/src/main/resources/templates/example.hbs
----------------------------------------------------------------------
diff --git a/nifi-registry-core/nifi-registry-web-api/src/main/resources/templates/example.hbs b/nifi-registry-core/nifi-registry-web-api/src/main/resources/templates/example.hbs
new file mode 100644
index 0000000..26a4283
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-web-api/src/main/resources/templates/example.hbs
@@ -0,0 +1,18 @@
+{{!--
+    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.
+--}}{{!-- formatting here matters... in whitespace: pre. this is not comprehensive but sufficent for our examples --}}
+{{#each properties}}    {{#ifeq type "string"}}"{{@key}}": "value"{{/ifeq}}{{#ifeq type "boolean"}}"{{@key}}": true{{/ifeq}}{{#ifeq type "integer"}}"{{@key}}": 0{{/ifeq}}{{#ifeq type "number"}}"{{@key}}": 0.0{{/ifeq}}{{#if $ref}}"{{@key}}": <span class="nested collapsed"><span class="nested-id hidden">{{basename $ref}}</span><span class="nested-example"><span class="open-object">&#123;&#8230;&#125;</span></span></span>{{/if}}{{#ifeq type "array"}}"{{@key}}": [{{#if items.$ref}}<span class="nested collapsed"><span class="nested-id hidden">{{basename items.$ref}}</span><span class="nested-example"><span class="open-object">&#123;&#8230;&#125;</span></span></span>{{else}}"value"{{/if}}]{{/ifeq}}{{#ifeq type "object"}}"{{@key}}": <span class="open-object">&#123;
+        "name": {{#if additionalProperties.$ref}}<span class="nested collapsed"><span class="nested-id hidden">{{basename additionalProperties.$ref}}</span><span class="nested-example"><span class="open-object">&#123;&#8230;&#125;</span></span></span>{{else}}{{#ifeq additionalProperties.type "integer"}}0{{else}}"value"{{/ifeq}}{{/if}}
+    &#125;</span>{{/ifeq}}<span class="comma">,</span>
+{{/each}}
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/6f26290d/nifi-registry-core/nifi-registry-web-api/src/main/resources/templates/index.html.hbs
----------------------------------------------------------------------
diff --git a/nifi-registry-core/nifi-registry-web-api/src/main/resources/templates/index.html.hbs b/nifi-registry-core/nifi-registry-web-api/src/main/resources/templates/index.html.hbs
new file mode 100644
index 0000000..97a8fc5
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-web-api/src/main/resources/templates/index.html.hbs
@@ -0,0 +1,514 @@
+<!DOCTYPE html>
+<!--
+    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.
+-->
+<html>
+    <head>
+        <title>{{info.title}}-{{info.version}}</title>
+        <meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/>
+        <link rel="shortcut icon" href="images/nifi16.ico"/>
+        <script type="text/javascript" src="../../nifi/assets/jquery/dist/jquery.min.js"></script>
+        <script type="text/javascript">
+            if (typeof window.jQuery === 'undefined') {
+                document.write(unescape('%3Cscript src="https://code.jquery.com/jquery-3.1.1.min.js" type="text/javascript" %3E%3C/script%3E'));
+            }
+        </script>
+        <style>
+            @import "https://fonts.googleapis.com/css?family=Open+Sans:300,300italic,400,400italic,600,600italic|Noto+Serif:400,400italic,700,700italic|Droid+Sans+Mono:400";
+
+            html {
+                overflow-y: scroll;
+            }
+            
+            html, html a {
+                -webkit-font-smoothing: antialiased;
+                text-shadow: 1px 1px 1px rgba(0,0,0,0.004);
+            }
+
+            body {
+                width: 62.5em;
+                margin: 0 auto;
+                display: block;
+                font-family: "Open Sans", "DejaVu Sans", sans-serif;
+            }
+            
+            div.header {
+                margin-top: 10px;
+            }
+            
+            img.logo {
+                float: left;
+                margin-right: 10px;
+            }
+            
+            div.header > div.title {
+                font-size: 30px;
+                height: 50px;
+                line-height: 50px;
+            }
+            
+            .sub-title {
+                font-style: italic;
+                color: #aaa;
+            }
+            
+            div.overview {
+                margin-top: 10px;
+                margin-bottom: 15px;
+            }
+            
+            div.endpoint {
+                margin-bottom: 10px;
+            }
+
+            /* get */
+            
+            div.endpoint.get {
+                border: 1px solid #174961;
+            }
+            
+            div.get div.operation-handle {
+                background-color: rgba(23, 73, 97, .15);
+            }
+            
+            div.get div.method {
+                background-color: #174961;
+            }
+            
+            div.get div.operation {
+                border-top: 1px solid #174961;
+            }
+            
+            /* post */
+            
+            div.endpoint.post {
+                border: 1px solid #7298AC;
+            }
+            
+            div.post div.operation-handle {
+                background-color: rgba(114, 152, 172, .15);
+            }
+            
+            div.post div.method {
+                background-color: #7298AC;
+            }
+            
+            div.post div.operation {
+                border-top: 1px solid #7298AC;
+            }
+            
+            /* put */
+            
+            div.endpoint.put {
+                border: 1px solid #063046;
+            }
+            
+            div.put div.operation-handle {
+                background-color: rgba(6, 48, 70, .15);
+            }
+            
+            div.put div.method {
+                background-color: #063046;
+            }
+            
+            div.put div.operation {
+                border-top: 1px solid #063046;
+            }
+            
+            /* delete */
+            
+            div.endpoint.delete {
+                border: 1px solid #47758E;
+            }
+            
+            div.delete div.operation-handle {
+                background-color: rgba(71, 117, 142, .15);
+            }
+            
+            div.delete div.method {
+                background-color: #47758E;
+            }
+            
+            div.delete div.operation {
+                border-top: 1px solid #47758E;
+            }
+            
+            /* operations */
+            
+            div.operation-handle {
+                cursor: pointer;
+                padding-right: 5px;
+                height: 22px;
+            }
+            
+            div.method {
+                float: left;
+                width: 75px;
+                color: #fff;
+                text-align: center;
+                background-color: #7098ad;
+                margin-right: 10px;
+                font-weight: bold;
+            }
+
+            div.endpoint div.path {
+                float: left;
+                line-height: 22px;
+                overflow: hidden;
+                text-overflow: ellipsis;
+            }
+
+            div.summary {
+                float: right;
+                font-size: 12px;
+                line-height: 22px;
+                white-space: nowrap;
+                overflow: hidden;
+                text-overflow: ellipsis;
+                width: 40%;
+                text-align: right;
+            }
+
+            div.operation {
+                padding: 5px;
+                font-size: 12px;
+            }
+
+            div.operation > div.title {
+                font-weight: bold;
+                color: #000;
+            }
+            
+            div.operation > table {
+                margin-left: 5px;
+                margin-right: 5px;
+            }
+            
+            div.operation div.details {
+                margin-left: 5px;
+                margin-bottom: 5px;
+                color: #333;
+            }
+            
+            div.operation div.description {
+                margin-bottom: 10px;
+            }
+
+            div.mediatype {
+                line-height: 16px;
+            }
+
+            div.mediatype > div.title {
+                float: left;
+                width: 70px;
+            }
+            
+            div.mediatype div.title {
+                float: left;
+            }
+            
+            div.type {
+                position: fixed;
+                width: 800px;
+                height: 500px;
+                left: 50%;
+                top: 50%;
+                margin-left: -400px;
+                margin-top: -250px;
+                border: 3px solid #365C6A;
+                box-shadow: 4px 4px 6px rgba(0, 0, 0, 0.9);
+                padding: 10px;
+                background-color: #eee;
+                font-size: 12px;
+            }
+            
+            div.type-container {
+                overflow-y: auto;
+                height: 415px;
+                border-bottom: 1px solid #ccc;
+            }
+            
+            div.close {
+                border: 1px solid #aaa;
+                background-color: #ddd;
+                float: right;
+                margin-top: 10px;
+                font-weight: bold;
+                height: 25px;
+                line-height: 25px;
+                padding: 0 10px;
+                cursor: pointer;
+            }
+            
+            div.close:hover {
+                background-color: #d1d1d1;
+            }
+            
+            div.section-header > div.title {
+                font-size: 24px;
+                float: left;
+            }
+            
+            div.section-description {
+                float: right;
+                margin-top: 10px;
+            }
+            
+            div.section-endpoints {
+                margin-top: 10px;
+            }
+            
+            /* tables */
+
+            table {
+                background-color: #fefefe;
+                border: 1px solid #ccc;
+                border-left: 6px solid #ccc;
+                color: #555;
+                display: block;
+                margin-bottom: 12px;
+                padding: 5px 8px;
+            }
+            
+            table th {
+                font-weight: bold;
+                vertical-align:top;
+                text-align:left;
+                padding: 4px 15px;
+                border-width: 0;
+                white-space: nowrap;
+            }
+            
+            table td {
+                vertical-align:top;
+                text-align:left;
+                padding: 2px 15px;
+                border-width: 0;
+                white-space: nowrap;
+            }
+            
+            table td:last-child {
+                width: 99%;
+                white-space: normal;
+            }
+            
+            code.example {
+                background-color: #fefefe;
+                border: 1px solid #ccc;
+                border-left: 6px solid #ccc;
+                color: #555;
+                margin-bottom: 10px;
+                padding: 5px 8px;
+                white-space: pre;
+                display: block;
+                tab-size: 4;
+                -moz-tab-size: 4;
+                -o-tab-size: 4;
+                line-height: 20px
+            }
+            
+            span.nested.collapsed {
+                cursor: pointer;
+                border: 1px solid #7298AC;
+                background-color: rgba(114, 152, 172, .15);
+                padding: 1px;
+            }
+            
+            /* general */
+            
+            .mono {
+                font-family: monospace;
+            }
+            
+            div.clear {
+                clear: both;
+            }
+
+            .hidden {
+                display: none;
+            }
+            
+            a, .link {
+                cursor: pointer;
+                color: #1e373f;
+                font-weight: normal;
+            }
+            
+            a:hover, .link:hover {
+                color: #264c58;
+                text-decoration: underline;
+            }
+        </style>
+        <script type="text/javascript">
+            $(document).ready(function () {
+                // hide any open type dialogs
+                $('html').on('click', function() {
+                    $('div.type').hide();
+                }).on('keydown', function(e) {
+                    if (e.which === 27) {
+                        $('div.type').hide();
+                    }
+                });
+                
+                // populate all paths - this is necessary because the @key
+                // doesn't seem to reset after iterating through a nested 
+                // array or object
+                $('span.path').each(function() {
+                    var path = $(this);
+                    var endpoint = path.parent();
+                    endpoint.find('div.path').text(path.text());
+                });
+                
+                // toggles the visibility of a given operation
+                $('div.operation-handle').on('click', function () {
+                    $(this).next('div.operation').slideToggle();
+                });
+                
+                // add support for clicking to view the definition of a type
+                $('a.type-link').on('click', function(e) {
+                    // hide any previously shown dialogs
+                    $('div.type').hide();
+
+                    // show the type selected
+                    var link = $(this);
+                    var typeId = link.text();
+                    $('#' + typeId).show();
+                    e.stopPropagation();
+                });
+                
+                // prevent hiding when clicking on the type dialog
+                $('div.type').on('click', function(e) {
+                    e.stopPropagation();
+                });
+                
+                // due to lack of support for @last when iterating objects in 
+                // handlebars we need to remove the last comma from each example
+                $('code.example').find('span.comma:last').remove();
+                
+                // populate nested examples
+                $('code.example').on('click', 'span.nested', function(e) {
+                    var nested = $(this).removeClass('collapsed');
+                    var nestedId = nested.find('span.nested-id');
+                    var nestedExample = nested.find('span.nested-example');
+                    
+                    // get the id of the nested example
+                    var typeId = nestedId.text();
+                    var example = $('#' + typeId + ' code.example').html();
+                    var depth = nestedId.parents('span.open-object').length;
+                    
+                    // tab over as appropriate
+                    example = example.replace(/(\r\n|\r|\n)/g, function(match) {
+                        var tab = '\t';
+                        for (var i = 0; i < depth - 1; i++) {
+                            tab += '\t';
+                        }
+                        return match + tab;
+                    });
+                    
+                    // copy over the example
+                    nestedExample.html(example);
+                    e.stopPropagation();
+                });
+                
+                // handle close button
+                $('div.close').on('click', function() {
+                    $(this).closest('div.type').hide();
+                });
+                
+                // function for organizing the endpoints
+                var organizeEndpoints = function(term, container) {
+                    $('div.unorganized > div.endpoints').each(function() {
+                        var endpoints = $(this);
+                        var path = endpoints.find('div.path').text();
+                        
+                        if (term === null || path.indexOf(term) === 0) {
+                            endpoints.detach().appendTo(container);
+                        }
+                    });
+                };
+                
+                // organize the endpoints
+                organizeEndpoints('/buckets', $('#bucket-endpoints'));
+                organizeEndpoints('/items', $('#item-endpoints'));
+                organizeEndpoints('/tenants', $('#tenant-endpoints'));
+                organizeEndpoints('/policies', $('#policy-endpoints'));
+                organizeEndpoints('/access', $('#access-endpoints'));
+
+                // handle expanding/collapsing the sections
+                $('div.section-header > div.title').on('click', function() {
+                    $(this).parent('div.section-header').next('div.section-endpoints').slideToggle();
+                });
+            });
+        </script>
+    </head>
+    <body>
+        <div class="header">
+            <img class="logo" src="images/bgNifiLogo.png" alt="NiFi Logo"/>
+            <div class="title">{{basePath}}</div>
+            <div class="sub-title">{{info.title}} {{info.version}}</div>
+            <div class="clear"></div>
+        </div>
+        <div class="clear"></div>
+        <div class="overview">{{info.description}}</div>
+        <div class="section">
+            <div class="section-header">
+                <div class="title link">Buckets</div>
+                <div class="sub-title section-description">Bucket endpoints</div>
+                <div class="clear"></div>
+            </div>
+            <div id="bucket-endpoints" class="section-endpoints hidden"></div>
+        </div>
+        <div class="section">
+            <div class="section-header">
+                <div class="title link">Items</div>
+                <div class="sub-title section-description">Item endpoints</div>
+                <div class="clear"></div>
+            </div>
+            <div id="item-endpoints" class="section-endpoints hidden"></div>
+        </div>
+        <div class="section">
+            <div class="section-header">
+                <div class="title link">Tenants</div>
+                <div class="sub-title section-description">Tenant endpoints</div>
+                <div class="clear"></div>
+            </div>
+            <div id="tenant-endpoints" class="section-endpoints hidden"></div>
+        </div>
+         <div class="section">
+              <div class="section-header">
+                  <div class="title link">Policies</div>
+                  <div class="sub-title section-description">Policy endpoints</div>
+                  <div class="clear"></div>
+              </div>
+              <div id="policy-endpoints" class="section-endpoints hidden"></div>
+          </div>
+          <div class="section">
+              <div class="section-header">
+                  <div class="title link">Access</div>
+                  <div class="sub-title section-description">Access endpoints</div>
+                  <div class="clear"></div>
+              </div>
+              <div id="access-endpoints" class="section-endpoints hidden"></div>
+          </div>
+
+        <div class="unorganized hidden">
+            {{#each paths}}
+                {{> endpoint}}
+            {{/each}}
+        </div>
+        {{#each definitions}}
+            {{> type}}
+        {{/each}}
+    </body>
+</html>
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/6f26290d/nifi-registry-core/nifi-registry-web-api/src/main/resources/templates/operation.hbs
----------------------------------------------------------------------
diff --git a/nifi-registry-core/nifi-registry-web-api/src/main/resources/templates/operation.hbs b/nifi-registry-core/nifi-registry-web-api/src/main/resources/templates/operation.hbs
new file mode 100644
index 0000000..64bd582
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-web-api/src/main/resources/templates/operation.hbs
@@ -0,0 +1,110 @@
+{{!--
+    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.
+--}}
+<div class="operation hidden">
+    {{#if description}}
+    <div class="description">
+        {{description}}
+    </div>
+    {{/if}}
+    <div class="title">Request</div>
+    <div class="mediatypes details">
+        {{#if consumes}}
+        <div class="mediatype"><div class="title">consumes:</div><div class="mono">{{join consumes ", "}}</div><div class="clear"></div></div>
+        {{/if}}
+    </div>
+    {{#if parameters}}
+    <table>
+        <thead>
+            <tr>
+                <th>Name</th>
+                <th>Location</th>
+                <th>Type</th>
+                <th>Description</th>
+            </tr>
+        </thead>
+        <tbody>
+    {{/if}}
+    {{#each parameters}}
+        <tr>
+            <td>{{#ifeq in "body"}}{{else}}{{name}}{{/ifeq}}</td>
+            <td>{{in}}</td>
+            {{#ifeq in "body"}}
+                <td>
+                {{#ifeq schema.type "array"}}Array[<a class="type-link" href="javascript:void(0);">{{basename schema.items.$ref}}</a>]{{/ifeq}}
+                {{#schema.$ref}}<a class="type-link" href="javascript:void(0);">{{basename schema.$ref}}</a> {{/schema.$ref}}
+                </td>
+            {{else}}
+                {{#ifeq type "array"}}
+                        <td>Array[{{items.type}}] ({{collectionFormat}})</td>
+                {{else}}
+                    {{#ifeq type "ref"}}
+                        <td>string</td>
+                    {{else}}
+                        <td>{{type}} {{#format}}({{format}}){{/format}}</td>
+                    {{/ifeq}}
+                {{/ifeq}}
+            {{/ifeq}}
+            <td>{{description}}</td>
+        </tr>
+    {{/each}}
+    {{#if parameters}}
+        </tbody>
+    </table>
+    {{/if}}
+    <div class="title">Response</div>
+    <div class="mediatypes details">
+        {{#if produces}}
+        <div class="mediatype"><div class="title">produces:</div><div class="mono">{{join produces ", "}}</div><div class="clear"></div></div>
+        {{/if}}
+    </div>
+    <table>
+        <thead>
+            <tr>
+                <th>Status Code</th>
+                <th>Type</th>
+                <th>Description</th>
+            </tr>
+        </thead>
+        <tbody>
+            {{#each responses}}
+            <tr>
+                <td>{{@key}}</td>
+                <td>
+                    {{#if schema}}
+                        {{#ifeq schema.type "array"}}
+                            {{#if schema.items.$ref}}
+                                array[<a class="type-link" href="javascript:void(0);">{{basename schema.items.$ref}}</a>]
+                            {{else}}
+                                array[{{schema.items.type}}]
+                            {{/if}}
+                        {{else}}
+                            {{#schema.$ref}}<a class="type-link" href="javascript:void(0);">{{basename schema.$ref}}</a>{{/schema.$ref}}
+                        {{/ifeq}}
+                    {{else}}
+                        string
+                    {{/if}}
+                </td>
+                <td>{{description}}</td>
+            </tr>
+            {{/each}}
+        </tbody>
+    </table>
+    {{#if vendorExtensions.x-access-policy}}
+        <div class="title">Authorization</div>
+        <div class="authorization details">
+            Requires access policy: {{vendorExtensions.x-access-policy.action}}:{{vendorExtensions.x-access-policy.resource}}
+        </div>
+    {{/if}}
+</div>
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/6f26290d/nifi-registry-core/nifi-registry-web-api/src/main/resources/templates/type.hbs
----------------------------------------------------------------------
diff --git a/nifi-registry-core/nifi-registry-web-api/src/main/resources/templates/type.hbs b/nifi-registry-core/nifi-registry-web-api/src/main/resources/templates/type.hbs
new file mode 100644
index 0000000..f6f117b
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-web-api/src/main/resources/templates/type.hbs
@@ -0,0 +1,57 @@
+{{!--
+    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.
+--}}
+<div id="{{@key}}" class="type hidden">
+    <h3>{{@key}}</h3>
+    <div class="type-container">
+        <table>
+            <tr>
+                <th>Name</th>
+                <th>Type</th>
+                <th>Required</th>
+                <th>Description</th>
+            </tr>
+            {{#each properties}}
+                <tr>
+                    <td>{{@key}}</td>
+                    <td>
+                        {{#ifeq type "array"}}
+                            {{#items.$ref}}
+                                {{type}}[<a class="type-link" href="javascript:void(0);">{{basename items.$ref}}</a>]
+                            {{/items.$ref}}
+                            {{^items.$ref}}
+                                {{type}}[{{items.type}}]
+                            {{/items.$ref}}
+                        {{else}}
+                            {{#$ref}}
+                                <a class="type-link" href="javascript:void(0);">{{basename $ref}}</a>
+                            {{/$ref}}
+                            {{^$ref}}
+                                {{type}}{{#format}} ({{format}}){{/format}}
+                            {{/$ref}}
+                        {{/ifeq}}
+                    </td>
+                    <td>{{#required}}required{{/required}}{{^required}}optional{{/required}}</td>
+                    <td>{{#description}}{{{description}}}{{/description}}
+                        {{#if enum}} Allowable values: {{join enum ", "}}{{/if}}
+                        {{#if readOnly}} This property is read only.{{/if}}</td>
+                </tr>
+            {{/each}}
+        </table>
+        <h4>Example JSON</h4>
+        <code class="example"><span class="open-object">&#123;{{> example}}&#125;</span></code>
+    </div>
+    <div class="close">Close</div>
+    <div class="clear"></div>
+</div>
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/6f26290d/nifi-registry-core/nifi-registry-web-api/src/test/groovy/org/apache/nifi/registry/security/authorization/ResourceAuthorizationFilterSpec.groovy
----------------------------------------------------------------------
diff --git a/nifi-registry-core/nifi-registry-web-api/src/test/groovy/org/apache/nifi/registry/security/authorization/ResourceAuthorizationFilterSpec.groovy b/nifi-registry-core/nifi-registry-web-api/src/test/groovy/org/apache/nifi/registry/security/authorization/ResourceAuthorizationFilterSpec.groovy
new file mode 100644
index 0000000..e27dfbe
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-web-api/src/test/groovy/org/apache/nifi/registry/security/authorization/ResourceAuthorizationFilterSpec.groovy
@@ -0,0 +1,170 @@
+/*
+ * 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.security.authorization
+
+import org.apache.nifi.registry.security.authorization.exception.AccessDeniedException
+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.apache.nifi.registry.web.security.authorization.HttpMethodAuthorizationRules
+import org.apache.nifi.registry.web.security.authorization.ResourceAuthorizationFilter
+import org.apache.nifi.registry.web.security.authorization.StandardHttpMethodAuthorizationRules
+import org.springframework.http.HttpMethod
+import org.springframework.mock.web.MockHttpServletRequest
+import org.springframework.mock.web.MockHttpServletResponse
+import spock.lang.Specification
+
+import javax.servlet.FilterChain
+import javax.servlet.http.HttpServletRequest
+import javax.servlet.http.HttpServletResponse
+
+class ResourceAuthorizationFilterSpec extends Specification {
+
+    AuthorizableLookup authorizableLookup = new StandardAuthorizableLookup()
+    AuthorizationService mockAuthorizationService = Mock(AuthorizationService)
+    FilterChain mockFilterChain = Mock(FilterChain)
+    ResourceAuthorizationFilter.Builder resourceAuthorizationFilterBuilder
+
+    // runs before every feature method
+    def setup() {
+        mockAuthorizationService.getAuthorizableLookup() >> authorizableLookup
+        resourceAuthorizationFilterBuilder = ResourceAuthorizationFilter.builder().setAuthorizationService(mockAuthorizationService)
+    }
+
+    // runs after every feature method
+    def cleanup() {
+        //mockAuthorizationService = null
+        //mockFilterChain = null
+        resourceAuthorizationFilterBuilder = null
+    }
+
+    // runs before the first feature method
+    def setupSpec() {}
+
+    // runs after the last feature method
+    def cleanupSpec() {}
+
+
+    def "unsecured requests are allowed without an authorization check"() {
+
+        setup:
+        def resourceAuthorizationFilter = resourceAuthorizationFilterBuilder.addResourceType(ResourceType.Actuator).build()
+        def httpServletRequest = createUnsecuredRequest()
+        def httpServletResponse = createResponse()
+
+        when: "doFilter() is called"
+        resourceAuthorizationFilter.doFilter(httpServletRequest, httpServletResponse, mockFilterChain)
+
+        then: "response is forwarded without authorization check"
+        0 * mockAuthorizationService._
+        1 * mockFilterChain.doFilter(_ as HttpServletRequest, _ as HttpServletResponse)
+
+    }
+
+
+    def "secure requests to an unguarded resource are allowed without an authorization check"() {
+
+        setup:
+        def resourceAuthorizationFilter = resourceAuthorizationFilterBuilder.addResourceType(ResourceType.Actuator).build()
+        def httpServletRequest = createSecureRequest(HttpMethod.POST, ResourceType.Bucket)
+        def httpServletResponse = createResponse()
+
+        when: "doFilter() is called"
+        resourceAuthorizationFilter.doFilter(httpServletRequest, httpServletResponse, mockFilterChain)
+
+        then: "response is forwarded without authorization check"
+        0 * mockAuthorizationService._
+        1 * mockFilterChain.doFilter(_ as HttpServletRequest, _ as HttpServletResponse)
+
+    }
+
+
+    def "secure requests to an unguarded HTTP method are allowed without an authorization check"() {
+
+        setup:
+        HttpMethodAuthorizationRules rules = new StandardHttpMethodAuthorizationRules(EnumSet.of(HttpMethod.POST, HttpMethod.PUT, HttpMethod.DELETE))
+        def resourceAuthorizationFilter = resourceAuthorizationFilterBuilder.addResourceType(ResourceType.Actuator, rules).build()
+        def httpServletRequest = createSecureRequest(HttpMethod.GET, ResourceType.Actuator)
+        def httpServletResponse = createResponse()
+
+        when: "doFilter() is called"
+        resourceAuthorizationFilter.doFilter(httpServletRequest, httpServletResponse, mockFilterChain)
+
+        then: "response is forwarded without authorization check"
+        0 * mockAuthorizationService._
+        1 * mockFilterChain.doFilter(_ as HttpServletRequest, _ as HttpServletResponse)
+
+    }
+
+
+    def "secure requests matching resource configuration rules perform authorization check"() {
+
+        setup:
+        // Stubbing setup for mockAuthorizationService is done in the then block as we are also verifying interactions with mock
+        def resourceAuthorizationFilter = resourceAuthorizationFilterBuilder.addResourceType(ResourceType.Actuator).build()
+        def authorizedRequest = createSecureRequest(HttpMethod.GET, ResourceType.Actuator)
+        def unauthorizedRequest = createSecureRequest(HttpMethod.POST, ResourceType.Actuator)
+        def httpServletResponse = createResponse()
+
+
+        when: "doFilter() is called with an authorized request"
+        resourceAuthorizationFilter.doFilter(authorizedRequest, httpServletResponse, mockFilterChain)
+
+        then: "response is forwarded after authorization check"
+        1 * mockAuthorizationService.authorize(_ as Authorizable, RequestAction.READ) >> { allowAccess() }
+        1 * mockFilterChain.doFilter(_ as HttpServletRequest, _ as HttpServletResponse)
+
+
+        when: "doFilter() is called with an unauthorized request"
+        resourceAuthorizationFilter.doFilter(unauthorizedRequest, httpServletResponse, mockFilterChain)
+
+        then: "authorization check is performed and response is not forwarded"
+        1 * mockAuthorizationService.authorize(_ as Authorizable, RequestAction.WRITE) >> { denyAccess() }
+        0 * mockFilterChain.doFilter(*_)
+
+    }
+
+    static private HttpServletRequest createUnsecuredRequest() {
+        HttpServletRequest req = new MockHttpServletRequest()
+        req.setScheme("http")
+        req.setSecure(false)
+        return req
+    }
+
+    static private HttpServletRequest createSecureRequest(HttpMethod httpMethod, ResourceType resourceType) {
+        HttpServletRequest req = new MockHttpServletRequest()
+        req.setMethod(httpMethod.name())
+        req.setScheme("https")
+        req.setServletPath(resourceType.getValue())
+        req.setSecure(true)
+        return req
+    }
+
+    static private HttpServletResponse createResponse() {
+        HttpServletResponse res = new MockHttpServletResponse()
+        return res
+    }
+
+    static private void allowAccess() {
+        // Do nothing (no thrown exception indicates access is allowed
+    }
+
+    static private void denyAccess() {
+        throw new AccessDeniedException("This is an expected AccessDeniedException.")
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/6f26290d/nifi-registry-core/nifi-registry-web-api/src/test/java/org/apache/nifi/registry/NiFiRegistryTestApiApplication.java
----------------------------------------------------------------------
diff --git a/nifi-registry-core/nifi-registry-web-api/src/test/java/org/apache/nifi/registry/NiFiRegistryTestApiApplication.java b/nifi-registry-core/nifi-registry-web-api/src/test/java/org/apache/nifi/registry/NiFiRegistryTestApiApplication.java
new file mode 100644
index 0000000..04514dd
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-web-api/src/test/java/org/apache/nifi/registry/NiFiRegistryTestApiApplication.java
@@ -0,0 +1,40 @@
+/*
+ * 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;
+
+import org.apache.nifi.registry.db.DataSourceFactory;
+import org.springframework.boot.autoconfigure.SpringBootApplication;
+import org.springframework.boot.web.servlet.support.SpringBootServletInitializer;
+import org.springframework.context.annotation.ComponentScan;
+import org.springframework.context.annotation.FilterType;
+
+@SpringBootApplication
+@ComponentScan(
+        excludeFilters = {
+                @ComponentScan.Filter(
+                        type = FilterType.ASSIGNABLE_TYPE,
+                        value = SpringBootServletInitializer.class), // Avoid loading NiFiRegistryApiApplication
+                @ComponentScan.Filter(
+                        type = FilterType.ASSIGNABLE_TYPE,
+                        value = DataSourceFactory.class), // Avoid loading DataSourceFactory
+                @ComponentScan.Filter(
+                        type = FilterType.REGEX,
+                        pattern = "org\\.apache\\.nifi\\.registry\\.NiFiRegistryPropertiesFactory"), // Avoid loading NiFiRegistryPropertiesFactory
+        })
+public class NiFiRegistryTestApiApplication extends SpringBootServletInitializer {
+
+}

http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/6f26290d/nifi-registry-core/nifi-registry-web-api/src/test/java/org/apache/nifi/registry/SecureLdapTestApiApplication.java
----------------------------------------------------------------------
diff --git a/nifi-registry-core/nifi-registry-web-api/src/test/java/org/apache/nifi/registry/SecureLdapTestApiApplication.java b/nifi-registry-core/nifi-registry-web-api/src/test/java/org/apache/nifi/registry/SecureLdapTestApiApplication.java
new file mode 100644
index 0000000..74d0730
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-web-api/src/test/java/org/apache/nifi/registry/SecureLdapTestApiApplication.java
@@ -0,0 +1,45 @@
+/*
+ * 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;
+
+import org.apache.nifi.registry.db.DataSourceFactory;
+import org.apache.nifi.registry.security.authorization.AuthorizerFactory;
+import org.springframework.boot.autoconfigure.SpringBootApplication;
+import org.springframework.boot.web.servlet.support.SpringBootServletInitializer;
+import org.springframework.context.annotation.ComponentScan;
+import org.springframework.context.annotation.FilterType;
+
+@SpringBootApplication
+@ComponentScan(
+        basePackages = "org.apache.nifi.registry",
+        excludeFilters = {
+                @ComponentScan.Filter(
+                        type = FilterType.ASSIGNABLE_TYPE,
+                        value = SpringBootServletInitializer.class), // Avoid loading NiFiRegistryApiApplication
+                @ComponentScan.Filter(
+                        type = FilterType.ASSIGNABLE_TYPE,
+                        value = DataSourceFactory.class), // Avoid loading DataSourceFactory
+                @ComponentScan.Filter(
+                        type = FilterType.ASSIGNABLE_TYPE,
+                        value = AuthorizerFactory.class), // Avoid loading AuthorizerFactory.getAuthorizer(), as we need to add it again with test-specific @DependsOn annotation
+                @ComponentScan.Filter(
+                        type = FilterType.REGEX,
+                        pattern = "org\\.apache\\.nifi\\.registry\\.NiFiRegistryPropertiesFactory"), // Avoid loading NiFiRegistryPropertiesFactory
+        })
+public class SecureLdapTestApiApplication extends SpringBootServletInitializer {
+
+}

http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/6f26290d/nifi-registry-core/nifi-registry-web-api/src/test/java/org/apache/nifi/registry/web/TestRestAPI.java
----------------------------------------------------------------------
diff --git a/nifi-registry-core/nifi-registry-web-api/src/test/java/org/apache/nifi/registry/web/TestRestAPI.java b/nifi-registry-core/nifi-registry-web-api/src/test/java/org/apache/nifi/registry/web/TestRestAPI.java
new file mode 100644
index 0000000..3cbc892
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-web-api/src/test/java/org/apache/nifi/registry/web/TestRestAPI.java
@@ -0,0 +1,170 @@
+/*
+ * 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;
+
+import org.apache.nifi.registry.bucket.Bucket;
+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.flow.VersionedProcessGroup;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import javax.ws.rs.WebApplicationException;
+import javax.ws.rs.client.Client;
+import javax.ws.rs.client.ClientBuilder;
+import javax.ws.rs.client.Entity;
+import javax.ws.rs.core.MediaType;
+import javax.ws.rs.core.Response;
+import java.util.ArrayList;
+import java.util.List;
+
+public class TestRestAPI {
+
+    public static final Logger LOGGER = LoggerFactory.getLogger(TestRestAPI.class);
+
+    public static final String REGISTRY_API_URL = "http://localhost:18080/nifi-registry-api";
+    public static final String REGISTRY_API_BUCKETS_URL = REGISTRY_API_URL + "/buckets";
+    public static final String REGISTRY_API_FLOWS_URL = REGISTRY_API_URL + "/flows";
+
+    public static void main(String[] args) {
+        try {
+            final Client client = ClientBuilder.newClient();
+
+            // create some buckets
+            final int numBuckets = 20;
+            final List<Bucket> createdBuckets = new ArrayList<>();
+
+            for (int i=0; i < numBuckets; i++) {
+                final Bucket createdBucket = createBucket(client, i);
+                System.out.println("Created bucket # " + i + " with id " + createdBucket.getIdentifier());
+                createdBuckets.add(createdBucket);
+            }
+
+            // create some flows
+            final int numFlowsPerBucket = 10;
+            final List<VersionedFlow> allFlows = new ArrayList<>();
+
+            for (final Bucket bucket : createdBuckets) {
+                final List<VersionedFlow> createdFlows = createFlows(client, bucket, numFlowsPerBucket);
+                allFlows.addAll(createdFlows);
+            }
+
+            // create some snapshots
+            final int numSnapshotsPerFlow = 10;
+            for (final VersionedFlow flow : allFlows) {
+                createSnapshots(client, flow, numSnapshotsPerFlow);
+            }
+
+            // Retrieve the flow by id
+//            final Response flowResponse = client.target(REGISTRY_API_FLOWS_URL)
+//                    .path("/{flowId}")
+//                    .resolveTemplate("flowId", createdFlow.getIdentifier())
+//                    .request()
+//                    .get();
+//
+//            final String flowJson = flowResponse.readEntity(String.class);
+//            System.out.println("Flow: " + flowJson);
+
+        } catch (WebApplicationException e) {
+            LOGGER.error(e.getMessage(), e);
+
+            final Response response = e.getResponse();
+            LOGGER.error(response.readEntity(String.class));
+        }
+    }
+
+    private static Bucket createBucket(Client client, int num) {
+        final Bucket bucket = new Bucket();
+        bucket.setName("Bucket #" + num);
+        bucket.setDescription("This is bucket #" + num);
+
+        final Bucket createdBucket = client.target(REGISTRY_API_BUCKETS_URL)
+                .request()
+                .post(
+                        Entity.entity(bucket, MediaType.APPLICATION_JSON),
+                        Bucket.class
+                );
+
+        return createdBucket;
+    }
+
+    private static VersionedFlow createFlow(Client client, Bucket bucket, int num) {
+        final VersionedFlow versionedFlow = new VersionedFlow();
+        versionedFlow.setName(bucket.getName() + " Flow #" + num);
+        versionedFlow.setDescription("This is " + bucket.getName() + " flow #" + num);
+
+        final VersionedFlow createdFlow = client.target(REGISTRY_API_BUCKETS_URL)
+                .path("/{bucketId}/flows")
+                .resolveTemplate("bucketId", bucket.getIdentifier())
+                .request()
+                .post(
+                        Entity.entity(versionedFlow, MediaType.APPLICATION_JSON),
+                        VersionedFlow.class
+                );
+
+        return createdFlow;
+    }
+
+    private static List<VersionedFlow> createFlows(Client client, Bucket bucket, int numFlows) {
+        final List<VersionedFlow> createdFlows = new ArrayList<>();
+
+        for (int i=0; i < numFlows; i++) {
+            final VersionedFlow createdFlow = createFlow(client, bucket, i);
+            System.out.println("Created flow # " + i + " with id " + createdFlow.getIdentifier());
+            createdFlows.add(createdFlow);
+        }
+
+        return createdFlows;
+    }
+
+    private static VersionedFlowSnapshot createSnapshot(Client client, VersionedFlow flow, int num) {
+        final VersionedFlowSnapshotMetadata snapshotMetadata1 = new VersionedFlowSnapshotMetadata();
+        snapshotMetadata1.setBucketIdentifier(flow.getBucketIdentifier());
+        snapshotMetadata1.setFlowIdentifier(flow.getIdentifier());
+        snapshotMetadata1.setVersion(num);
+        snapshotMetadata1.setComments("This is snapshot #" + num);
+
+        final VersionedProcessGroup snapshotContents1 = new VersionedProcessGroup();
+        snapshotContents1.setIdentifier("pg1");
+        snapshotContents1.setName("Process Group 1");
+
+        final VersionedFlowSnapshot snapshot1 = new VersionedFlowSnapshot();
+        snapshot1.setSnapshotMetadata(snapshotMetadata1);
+        snapshot1.setFlowContents(snapshotContents1);
+
+        final VersionedFlowSnapshot createdSnapshot = client.target(REGISTRY_API_BUCKETS_URL)
+                .path("{bucketId}/flows/{flowId}/versions")
+                .resolveTemplate("bucketId", flow.getBucketIdentifier())
+                .resolveTemplate("flowId", flow.getIdentifier())
+                .request()
+                .post(
+                        Entity.entity(snapshot1, MediaType.APPLICATION_JSON_TYPE),
+                        VersionedFlowSnapshot.class
+                );
+
+        return createdSnapshot;
+    }
+
+    private static void createSnapshots(Client client, VersionedFlow flow, int numSnapshots) {
+        for (int i=1; i <= numSnapshots; i++) {
+            createSnapshot(client, flow, i);
+            System.out.println("Created snapshot # " + i + " for flow with id " + flow.getIdentifier());
+        }
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/6f26290d/nifi-registry-core/nifi-registry-web-api/src/test/java/org/apache/nifi/registry/web/api/BucketsIT.java
----------------------------------------------------------------------
diff --git a/nifi-registry-core/nifi-registry-web-api/src/test/java/org/apache/nifi/registry/web/api/BucketsIT.java b/nifi-registry-core/nifi-registry-web-api/src/test/java/org/apache/nifi/registry/web/api/BucketsIT.java
new file mode 100644
index 0000000..99c04c7
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-web-api/src/test/java/org/apache/nifi/registry/web/api/BucketsIT.java
@@ -0,0 +1,238 @@
+/*
+ * 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.bucket.Bucket;
+import org.junit.Test;
+import org.skyscreamer.jsonassert.JSONAssert;
+import org.springframework.test.context.jdbc.Sql;
+
+import javax.ws.rs.client.Entity;
+import javax.ws.rs.core.MediaType;
+import javax.ws.rs.core.Response;
+
+import static org.apache.nifi.registry.web.api.IntegrationTestUtils.assertBucketsEqual;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertTrue;
+
+public class BucketsIT extends UnsecuredITBase {
+
+    @Test
+    public void testGetBucketsEmpty() throws Exception {
+
+        // Given: a fresh context server with an empty DB
+        // When: the /buckets endpoint is queried
+
+        final Bucket[] buckets = client
+                .target(createURL("buckets"))
+                .request()
+                .get(Bucket[].class);
+
+        // Then: an empty array is returned
+
+        assertNotNull(buckets);
+        assertEquals(0, buckets.length);
+    }
+
+    @Test
+    @Sql(executionPhase = Sql.ExecutionPhase.BEFORE_TEST_METHOD, scripts = {"classpath:db/clearDB.sql", "classpath:db/BucketsIT.sql"})
+    public void testGetBuckets() throws Exception {
+
+        // Given: these buckets have been populated in the DB (see BucketsIT.sql)
+
+        String expected = "[" +
+                "{\"identifier\":\"1\"," +
+                "\"name\":\"Bucket 1\"," +
+                "\"createdTimestamp\":1505091060000," +
+                "\"description\":\"This is test bucket 1\"," +
+                "\"permissions\":{\"canRead\":true,\"canWrite\":true,\"canDelete\":true}," +
+                "\"link\":{\"params\":{\"rel\":\"self\"},\"href\":\"buckets/1\"}}," +
+                "{\"identifier\":\"2\"," +
+                "\"name\":\"Bucket 2\"," +
+                "\"createdTimestamp\":1505091120000," +
+                "\"description\":\"This is test bucket 2\"," +
+                "\"permissions\":{\"canRead\":true,\"canWrite\":true,\"canDelete\":true}," +
+                "\"link\":{\"params\":{\"rel\":\"self\"},\"href\":\"buckets/2\"}}," +
+                "{\"identifier\":\"3\"," +
+                "\"name\":\"Bucket 3\"," +
+                "\"createdTimestamp\":1505091180000," +
+                "\"description\":\"This is test bucket 3\"," +
+                "\"permissions\":{\"canRead\":true,\"canWrite\":true,\"canDelete\":true}," +
+                "\"link\":{\"params\":{\"rel\":\"self\"},\"href\":\"buckets/3\"}}" +
+                "]";
+
+        // When: the /buckets endpoint is queried
+
+        String bucketsJson = client
+                .target(createURL("buckets"))
+                .request()
+                .get(String.class);
+
+        // Then: the pre-populated list of buckets is returned
+
+        JSONAssert.assertEquals(expected, bucketsJson, false);
+        assertTrue(!bucketsJson.contains("null")); // JSON serialization from the server should not include null fields, such as "versionedFlows": null
+    }
+
+    @Test
+    public void testGetNonexistentBucket() throws Exception {
+        // Given: a fresh context server with an empty DB
+        // When: any /buckets/{id} endpoint is queried
+        Response response = client.target(createURL("buckets/a-nonexistent-identifier")).request().get();
+
+        // Then: a 404 response status is returned
+        assertEquals(404, response.getStatus());
+    }
+
+    @Test
+    public void testCreateBucketGetBucket() throws Exception {
+
+        // Given:
+
+        long testStartTime = System.currentTimeMillis();
+        final Bucket bucket = new Bucket();
+        bucket.setName("Integration Test Bucket");
+        bucket.setDescription("A bucket created by an integration test.");
+
+        // When: a bucket is created on the server
+
+        Bucket createdBucket = client
+                .target(createURL("buckets"))
+                .request()
+                .post(Entity.entity(bucket, MediaType.APPLICATION_JSON), Bucket.class);
+
+        // Then: the server returns the created bucket, with server-set fields populated correctly
+
+        assertBucketsEqual(bucket, createdBucket, false);
+        assertNotNull(createdBucket.getIdentifier());
+        assertTrue(createdBucket.getCreatedTimestamp() - testStartTime > 0L); // both server and client in same JVM, so there shouldn't be skew
+        assertNotNull(createdBucket.getLink());
+        assertNotNull(createdBucket.getLink().getUri());
+
+        // And when /buckets is queried, then the newly created bucket is returned in the list
+
+        final Bucket[] buckets = client
+                .target(createURL("buckets"))
+                .request()
+                .get(Bucket[].class);
+        assertNotNull(buckets);
+        assertEquals(1, buckets.length);
+        assertBucketsEqual(createdBucket, buckets[0], true);
+
+        // And when the link URI is queried, then the newly created bucket is returned
+
+        final Bucket bucketByLink = client
+                .target(createURL(buckets[0].getLink().getUri().toString()))
+                .request()
+                .get(Bucket.class);
+        assertBucketsEqual(createdBucket, bucketByLink, true);
+
+        // And when the bucket is queried by /buckets/ID, then the newly created bucket is returned
+
+        final Bucket bucketById = client
+                .target(createURL("buckets/" + createdBucket.getIdentifier()))
+                .request()
+                .get(Bucket.class);
+        assertBucketsEqual(createdBucket, bucketById, true);
+    }
+
+    @Test
+    public void testUpdateBucket() throws Exception {
+
+        // Given: a bucket exists on the server
+
+        final Bucket bucket = new Bucket();
+        bucket.setName("Integration Test Bucket");
+        bucket.setDescription("A bucket created by an integration test.");
+        Bucket createdBucket = client
+                .target(createURL("buckets"))
+                .request()
+                .post(Entity.entity(bucket, MediaType.APPLICATION_JSON), Bucket.class);
+
+        // When: the bucket is modified by the client and updated on the server
+
+        createdBucket.setName("Renamed Bucket");
+        createdBucket.setDescription("This bucket has been updated by an integration test.");
+
+        final Bucket updatedBucket = client
+                .target(createURL("buckets/" + createdBucket.getIdentifier()))
+                .request()
+                .put(Entity.entity(createdBucket, MediaType.APPLICATION_JSON), Bucket.class);
+
+        // Then: the server returns the updated bucket
+
+        assertBucketsEqual(createdBucket, updatedBucket, true);
+
+    }
+
+    @Test
+    public void testDeleteBucket() throws Exception {
+
+        // Given: a bucket has been created
+
+        final Bucket bucket = new Bucket();
+        bucket.setName("Integration Test Bucket");
+        bucket.setDescription("A bucket created by an integration test.");
+
+        Bucket createdBucket = client
+                .target(createURL("buckets"))
+                .request()
+                .post(Entity.entity(bucket, MediaType.APPLICATION_JSON), Bucket.class);
+
+        // When: that bucket deleted
+
+        final Bucket deletedBucket = client
+                .target(createURL("buckets/" + createdBucket.getIdentifier()))
+                .request()
+                .delete(Bucket.class);
+
+        // Then: the body of the server response matches the bucket that was deleted
+        //  and: the bucket is no longer accessible (resource not found)
+
+        createdBucket.setPermissions(null); // authorizedActions will not be present in deletedBucket
+        createdBucket.setLink(null); // links will not be present in deletedBucket
+        assertBucketsEqual(createdBucket, deletedBucket, true);
+
+        final Response response = client
+                .target(createURL("buckets/" + createdBucket.getIdentifier()))
+                .request()
+                .get();
+        assertEquals(404, response.getStatus());
+    }
+
+    @Test
+    public void getBucketFields() throws Exception {
+
+        // Given: the server is configured to return this fixed response
+
+        String expected = "{\"fields\":[\"ID\",\"NAME\",\"DESCRIPTION\",\"CREATED\"]}";
+
+        // When: the server is queried
+
+        String bucketFieldsJson = client
+                .target(createURL("buckets/fields"))
+                .request()
+                .get(String.class);
+
+        // Then: the fixed response is returned to the client
+
+        JSONAssert.assertEquals(expected, bucketFieldsJson, false);
+
+    }
+
+}


[32/51] [partial] nifi-registry git commit: NIFIREG-201 Refactoring project structure to better isolate extensions

Posted by kd...@apache.org.
http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/6f26290d/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/security/ldap/tenants/SearchScope.java
----------------------------------------------------------------------
diff --git a/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/security/ldap/tenants/SearchScope.java b/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/security/ldap/tenants/SearchScope.java
new file mode 100644
index 0000000..2e5e8a2
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/security/ldap/tenants/SearchScope.java
@@ -0,0 +1,28 @@
+/*
+ * 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.security.ldap.tenants;
+
+/**
+ * Scope for searching a directory server.
+ */
+public enum SearchScope {
+
+    OBJECT,
+    ONE_LEVEL,
+    SUBTREE;
+
+}

http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/6f26290d/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/security/ldap/tenants/TenantHolder.java
----------------------------------------------------------------------
diff --git a/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/security/ldap/tenants/TenantHolder.java b/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/security/ldap/tenants/TenantHolder.java
new file mode 100644
index 0000000..7ef3a8c
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/security/ldap/tenants/TenantHolder.java
@@ -0,0 +1,165 @@
+/*
+ * 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.security.ldap.tenants;
+
+
+import org.apache.nifi.registry.security.authorization.Group;
+import org.apache.nifi.registry.security.authorization.User;
+
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Map;
+import java.util.Set;
+
+/**
+ * A holder to provide atomic access to user group data structures.
+ */
+public class TenantHolder {
+
+    private final Set<User> allUsers;
+    private final Map<String,User> usersById;
+    private final Map<String,User> usersByIdentity;
+
+    private final Set<Group> allGroups;
+    private final Map<String,Group> groupsById;
+    private final Map<String, Set<Group>> groupsByUserIdentity;
+
+    /**
+     * Creates a new holder and populates all convenience data structures.
+     */
+    public TenantHolder(final Set<User> allUsers, final Set<Group> allGroups) {
+        // create a convenience map to retrieve a user by id
+        final Map<String, User> userByIdMap = Collections.unmodifiableMap(createUserByIdMap(allUsers));
+
+        // create a convenience map to retrieve a user by identity
+        final Map<String, User> userByIdentityMap = Collections.unmodifiableMap(createUserByIdentityMap(allUsers));
+
+        // create a convenience map to retrieve a group by id
+        final Map<String, Group> groupByIdMap = Collections.unmodifiableMap(createGroupByIdMap(allGroups));
+
+        // create a convenience map to retrieve the groups for a user identity
+        final Map<String, Set<Group>> groupsByUserIdentityMap = Collections.unmodifiableMap(createGroupsByUserIdentityMap(allGroups, allUsers));
+
+        // set all the holders
+        this.allUsers = allUsers;
+        this.allGroups = allGroups;
+        this.usersById = userByIdMap;
+        this.usersByIdentity = userByIdentityMap;
+        this.groupsById = groupByIdMap;
+        this.groupsByUserIdentity = groupsByUserIdentityMap;
+    }
+
+    /**
+     * Creates a Map from user identifier to User.
+     *
+     * @param users the set of all users
+     * @return the Map from user identifier to User
+     */
+    private Map<String,User> createUserByIdMap(final Set<User> users) {
+        Map<String,User> usersMap = new HashMap<>();
+        for (User user : users) {
+            usersMap.put(user.getIdentifier(), user);
+        }
+        return usersMap;
+    }
+
+    /**
+     * Creates a Map from user identity to User.
+     *
+     * @param users the set of all users
+     * @return the Map from user identity to User
+     */
+    private Map<String,User> createUserByIdentityMap(final Set<User> users) {
+        Map<String,User> usersMap = new HashMap<>();
+        for (User user : users) {
+            usersMap.put(user.getIdentity(), user);
+        }
+        return usersMap;
+    }
+
+    /**
+     * Creates a Map from group identifier to Group.
+     *
+     * @param groups the set of all groups
+     * @return the Map from group identifier to Group
+     */
+    private Map<String,Group> createGroupByIdMap(final Set<Group> groups) {
+        Map<String,Group> groupsMap = new HashMap<>();
+        for (Group group : groups) {
+            groupsMap.put(group.getIdentifier(), group);
+        }
+        return groupsMap;
+    }
+
+    /**
+     * Creates a Map from user identity to the set of Groups for that identity.
+     *
+     * @param groups all groups
+     * @param users all users
+     * @return a Map from User identity to the set of Groups for that identity
+     */
+    private Map<String, Set<Group>> createGroupsByUserIdentityMap(final Set<Group> groups, final Set<User> users) {
+        Map<String, Set<Group>> groupsByUserIdentity = new HashMap<>();
+
+        for (User user : users) {
+            Set<Group> userGroups = new HashSet<>();
+            for (Group group : groups) {
+                for (String groupUser : group.getUsers()) {
+                    if (groupUser.equals(user.getIdentifier())) {
+                        userGroups.add(group);
+                    }
+                }
+            }
+
+            groupsByUserIdentity.put(user.getIdentity(), userGroups);
+        }
+
+        return groupsByUserIdentity;
+    }
+
+    Set<User> getAllUsers() {
+        return allUsers;
+    }
+
+    Map<String, User> getUsersById() {
+        return usersById;
+    }
+
+    Set<Group> getAllGroups() {
+        return allGroups;
+    }
+
+    Map<String, Group> getGroupsById() {
+        return groupsById;
+    }
+
+    public User getUser(String identity) {
+        if (identity == null) {
+            throw new IllegalArgumentException("Identity cannot be null");
+        }
+        return usersByIdentity.get(identity);
+    }
+
+    public Set<Group> getGroups(String userIdentity) {
+        if (userIdentity == null) {
+            throw new IllegalArgumentException("User Identity cannot be null");
+        }
+        return groupsByUserIdentity.get(userIdentity);
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/6f26290d/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/security/util/XmlUtils.java
----------------------------------------------------------------------
diff --git a/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/security/util/XmlUtils.java b/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/security/util/XmlUtils.java
new file mode 100644
index 0000000..2caa8fa
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/security/util/XmlUtils.java
@@ -0,0 +1,44 @@
+/*
+ * 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.security.util;
+
+import javax.xml.stream.XMLInputFactory;
+import javax.xml.stream.XMLStreamException;
+import javax.xml.stream.XMLStreamReader;
+import javax.xml.transform.stream.StreamSource;
+import java.io.InputStream;
+
+public class XmlUtils {
+
+    public static XMLStreamReader createSafeReader(InputStream inputStream) throws XMLStreamException {
+        if (inputStream == null) {
+            throw new IllegalArgumentException("The provided input stream cannot be null");
+        }
+        return createSafeReader(new StreamSource(inputStream));
+    }
+
+    public static XMLStreamReader createSafeReader(StreamSource source) throws XMLStreamException {
+        if (source == null) {
+            throw new IllegalArgumentException("The provided source cannot be null");
+        }
+
+        XMLInputFactory xif = XMLInputFactory.newFactory();
+        xif.setProperty(XMLInputFactory.IS_SUPPORTING_EXTERNAL_ENTITIES, false);
+        xif.setProperty(XMLInputFactory.SUPPORT_DTD, false);
+        return xif.createXMLStreamReader(source);
+    }
+}

http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/6f26290d/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/serialization/SerializationException.java
----------------------------------------------------------------------
diff --git a/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/serialization/SerializationException.java b/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/serialization/SerializationException.java
new file mode 100644
index 0000000..dd05e77
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/serialization/SerializationException.java
@@ -0,0 +1,35 @@
+/*
+ * 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.serialization;
+
+/**
+ * An error that can occur during serialization or deserialization.
+ */
+public class SerializationException extends RuntimeException {
+
+    public SerializationException(String message) {
+        super(message);
+    }
+
+    public SerializationException(String message, Throwable cause) {
+        super(message, cause);
+    }
+
+    public SerializationException(Throwable cause) {
+        super(cause);
+    }
+}

http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/6f26290d/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/serialization/Serializer.java
----------------------------------------------------------------------
diff --git a/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/serialization/Serializer.java b/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/serialization/Serializer.java
new file mode 100644
index 0000000..ca424ad
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/serialization/Serializer.java
@@ -0,0 +1,43 @@
+/*
+ * 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.serialization;
+
+import java.io.InputStream;
+import java.io.OutputStream;
+
+/**
+ * Serializes and de-serializes objects.
+ */
+public interface Serializer<T> {
+
+    /**
+     * Serializes a snapshot to the given output stream.
+     *
+     * @param t the object to serialize
+     * @param out the output stream to serialize to
+     */
+    void serialize(T t, OutputStream out) throws SerializationException;
+
+    /**
+     * Deserializes the given InputStream back to an object of the given type.
+     *
+     * @param input the InputStream to deserialize
+     * @return the deserialized object
+     */
+    T deserialize(InputStream input) throws SerializationException;
+
+}

http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/6f26290d/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/serialization/VersionedProcessGroupSerializer.java
----------------------------------------------------------------------
diff --git a/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/serialization/VersionedProcessGroupSerializer.java b/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/serialization/VersionedProcessGroupSerializer.java
new file mode 100644
index 0000000..7322ce7
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/serialization/VersionedProcessGroupSerializer.java
@@ -0,0 +1,137 @@
+/*
+ * 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.serialization;
+
+import org.apache.nifi.registry.flow.VersionedProcessGroup;
+import org.apache.nifi.registry.serialization.jackson.JacksonVersionedProcessGroupSerializer;
+import org.apache.nifi.registry.serialization.jaxb.JAXBVersionedProcessGroupSerializer;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.stereotype.Service;
+
+import java.io.BufferedInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * <p>
+ * A serializer for VersionedProcessGroup that maps a "version" of the data model to a serializer.
+ * </p>
+ *
+ * <p>
+ * When serializing, the serializer associated with the {@link #CURRENT_DATA_MODEL_VERSION} is used.
+ * The version will be written as a header at the beginning of the OutputStream then followed by the content.
+ * </p>
+ *
+ * <p>
+ * When deserializing, each registered serializer will be asked to read a data model version number from the input stream
+ * in descending version order until a version number is read successfully.
+ * Then the associated serializer to the read data model version is used to deserialize content back to the target object.
+ * If no serializer can read the version, or no serializer is registered for the read version, then SerializationException is thrown.
+ * </p>
+ *
+ * <p>
+ * Current data model version is 2.
+ * Data Model Version Histories:
+ * <ul>
+ *     <li>version 2: Serialized by {@link JacksonVersionedProcessGroupSerializer}</li>
+ *     <li>version 1: Serialized by {@link JAXBVersionedProcessGroupSerializer}</li>
+ * </ul>
+ * </p>
+ */
+@Service
+public class VersionedProcessGroupSerializer implements Serializer<VersionedProcessGroup> {
+
+    private static final Logger logger = LoggerFactory.getLogger(VersionedProcessGroupSerializer.class);
+
+    static final Integer CURRENT_DATA_MODEL_VERSION = 2;
+
+    private final Map<Integer, VersionedSerializer<VersionedProcessGroup>> serializersByVersion;
+    private final VersionedSerializer<VersionedProcessGroup> defaultSerializer;
+    private final List<Integer> descendingVersions;
+    public static final int MAX_HEADER_BYTES = 1024;
+
+    public VersionedProcessGroupSerializer() {
+
+        final Map<Integer, VersionedSerializer<VersionedProcessGroup>> tempSerializers = new HashMap<>();
+        tempSerializers.put(2, new JacksonVersionedProcessGroupSerializer());
+        tempSerializers.put(1, new JAXBVersionedProcessGroupSerializer());
+
+        this.serializersByVersion = Collections.unmodifiableMap(tempSerializers);
+        this.defaultSerializer = tempSerializers.get(CURRENT_DATA_MODEL_VERSION);
+
+        final List<Integer> sortedVersions = new ArrayList<>(serializersByVersion.keySet());
+        sortedVersions.sort(Collections.reverseOrder(Integer::compareTo));
+        this.descendingVersions = sortedVersions;
+    }
+
+    @Override
+    public void serialize(final VersionedProcessGroup versionedProcessGroup, final OutputStream out) throws SerializationException {
+
+        defaultSerializer.serialize(CURRENT_DATA_MODEL_VERSION, versionedProcessGroup, out);
+    }
+
+    @Override
+    public VersionedProcessGroup deserialize(final InputStream input) throws SerializationException {
+
+        final InputStream markSupportedInput = input.markSupported() ? input : new BufferedInputStream(input);
+
+        // Mark the beginning of the stream.
+        markSupportedInput.mark(MAX_HEADER_BYTES);
+
+        // Applying each serializer
+        for (int serializerVersion : descendingVersions) {
+            final VersionedSerializer<VersionedProcessGroup> serializer = serializersByVersion.get(serializerVersion);
+
+            // Serializer version will not be the data model version always.
+            // E.g. higher version of serializer can read the old data model version number if it has the same header structure,
+            // but it does not mean the serializer is compatible with the old format.
+            final int version;
+            try {
+                version = serializer.readDataModelVersion(markSupportedInput);
+                if (!serializersByVersion.containsKey(version)) {
+                    throw new SerializationException(String.format(
+                            "Version %d was returned by %s, but no serializer is registered for that version.", version, serializer));
+                }
+            } catch (SerializationException e) {
+                logger.debug("Deserialization failed with {}", serializer, e);
+                continue;
+            } finally {
+                // Either when continue with the next serializer, or proceed deserialization with the corresponding serializer,
+                // reset the stream position.
+                try {
+                    markSupportedInput.reset();
+                } catch (IOException resetException) {
+                    // Should not happen.
+                    logger.error("Unable to reset the input stream.", resetException);
+                }
+            }
+
+            return serializersByVersion.get(version).deserialize(markSupportedInput);
+        }
+
+        throw new SerializationException("Unable to find a process group serializer compatible with the input.");
+
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/6f26290d/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/serialization/VersionedSerializer.java
----------------------------------------------------------------------
diff --git a/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/serialization/VersionedSerializer.java b/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/serialization/VersionedSerializer.java
new file mode 100644
index 0000000..b3c626f
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/serialization/VersionedSerializer.java
@@ -0,0 +1,65 @@
+/*
+ * 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.serialization;
+
+import java.io.InputStream;
+import java.io.OutputStream;
+
+/**
+ * Serializes and de-serializes objects.
+ * This interface is designed to provide backward compatibility to different versioned serialization formats.
+ * So that serialized data model and format can evolve overtime.
+ */
+public interface VersionedSerializer<T> {
+
+    /**
+     * Serialize the given object into the target output stream with the specified version format.
+     * Implementation classes are responsible to serialize the version to the head of the serialized content,
+     * so that it can be retrieved by {@link #readDataModelVersion(InputStream)} method efficiently
+     * without reading the entire byte array.
+     *
+     * @param dataModelVersion the data model version
+     * @param t the object to serialize
+     * @param out the target output stream
+     * @throws SerializationException thrown when serialization failed
+     */
+    void serialize(int dataModelVersion, T t, OutputStream out) throws SerializationException;
+
+    /**
+     * Read data model version from the given InputStream.
+     * <p>
+     * Even if an implementation serializer was able to read a version, it does not necessary mean
+     * the same serializers {@link #deserialize(InputStream)} method will be called.
+     * For example, when the header structure has not been changed, the newer version of serializer may be able to
+     * read older data model version. But deserialization should be done with the older serializer.
+     * </p>
+     * @param input the input stream to read version from
+     * @return the read data model version
+     * @throws SerializationException thrown when reading version failed
+     */
+    int readDataModelVersion(InputStream input) throws SerializationException;
+
+    /**
+     * Deserializes the given InputStream back to an object of the given type.
+     *
+     * @param input the InputStream to deserialize,
+     *              the position of input is reset to the the beginning of the stream when this method is called
+     * @return the deserialized object
+     * @throws SerializationException thrown when deserialization failed
+     */
+    T deserialize(InputStream input) throws SerializationException;
+}

http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/6f26290d/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/serialization/jackson/JacksonSerializer.java
----------------------------------------------------------------------
diff --git a/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/serialization/jackson/JacksonSerializer.java b/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/serialization/jackson/JacksonSerializer.java
new file mode 100644
index 0000000..4098c77
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/serialization/jackson/JacksonSerializer.java
@@ -0,0 +1,127 @@
+/*
+ * 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.serialization.jackson;
+
+import com.fasterxml.jackson.core.type.TypeReference;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import org.apache.nifi.registry.serialization.SerializationException;
+import org.apache.nifi.registry.serialization.VersionedSerializer;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.nio.charset.StandardCharsets;
+import java.util.Collections;
+import java.util.HashMap;
+
+import static org.apache.nifi.registry.serialization.VersionedProcessGroupSerializer.MAX_HEADER_BYTES;
+
+/**
+ * A Serializer that uses Jackson for serializing/deserializing.
+ */
+public abstract class JacksonSerializer<T> implements VersionedSerializer<T> {
+
+    private static final Logger logger = LoggerFactory.getLogger(JacksonSerializer.class);
+
+    private static final String JSON_HEADER = "\"header\"";
+    private static final String DATA_MODEL_VERSION = "dataModelVersion";
+
+    private final ObjectMapper objectMapper = ObjectMapperProvider.getMapper();
+
+    @Override
+    public void serialize(int dataModelVersion, T t, OutputStream out) throws SerializationException {
+        if (t == null) {
+            throw new IllegalArgumentException("The object to serialize cannot be null");
+        }
+
+        if (out == null) {
+            throw new IllegalArgumentException("OutputStream cannot be null");
+        }
+
+        final SerializationContainer<T> container = new SerializationContainer<>();
+        container.setHeader(Collections.singletonMap(DATA_MODEL_VERSION, String.valueOf(dataModelVersion)));
+        container.setContent(t);
+
+        try {
+            objectMapper.writerWithDefaultPrettyPrinter().writeValue(out, container);
+        } catch (IOException e) {
+            throw new SerializationException("Unable to serialize object", e);
+        }
+    }
+
+    @Override
+    public T deserialize(InputStream input) throws SerializationException {
+        final TypeReference<SerializationContainer<T>> typeRef = getDeserializeTypeRef();
+        try {
+            final SerializationContainer<T> container = objectMapper.readValue(input, typeRef);
+            return container.getContent();
+        } catch (IOException e) {
+            throw new SerializationException("Unable to deserialize object", e);
+        }
+    }
+
+    abstract TypeReference<SerializationContainer<T>> getDeserializeTypeRef() throws SerializationException;
+
+    @Override
+    public int readDataModelVersion(InputStream input) throws SerializationException {
+        final byte[] headerBytes = new byte[MAX_HEADER_BYTES];
+        final int readHeaderBytes;
+        try {
+            readHeaderBytes = input.read(headerBytes);
+        } catch (IOException e) {
+            throw new SerializationException("Could not read additional bytes to parse as serialization version 2 or later. "
+                    + e.getMessage(), e);
+        }
+
+        // Seek '"header"'.
+        final String headerStr = new String(headerBytes, 0, readHeaderBytes, StandardCharsets.UTF_8);
+        final int headerIndex = headerStr.indexOf(JSON_HEADER);
+        if (headerIndex < 0) {
+            throw new SerializationException(String.format("Could not find %s in the first %d bytes",
+                    JSON_HEADER, readHeaderBytes));
+        }
+
+        final int headerStart = headerStr.indexOf("{", headerIndex);
+        if (headerStart < 0) {
+            throw new SerializationException(String.format("Could not find '{' starting header object in the first %d bytes.", readHeaderBytes));
+        }
+
+        final int headerEnd = headerStr.indexOf("}", headerStart);
+        if (headerEnd < 0) {
+            throw new SerializationException(String.format("Could not find '}' ending header object in the first %d bytes.", readHeaderBytes));
+        }
+
+        final String headerObjectStr = headerStr.substring(headerStart, headerEnd + 1);
+        logger.debug("headerObjectStr={}", headerObjectStr);
+
+        try {
+            final TypeReference<HashMap<String, String>> typeRef = new TypeReference<HashMap<String, String>>() {};
+            final HashMap<String, String> header = objectMapper.readValue(headerObjectStr, typeRef);
+            if (!header.containsKey(DATA_MODEL_VERSION)) {
+                throw new SerializationException("Missing " + DATA_MODEL_VERSION);
+            }
+
+            return Integer.parseInt(header.get(DATA_MODEL_VERSION));
+        } catch (IOException e) {
+            throw new SerializationException(String.format("Failed to parse header string '%s' due to %s", headerObjectStr, e), e);
+        } catch (NumberFormatException e) {
+            throw new SerializationException(String.format("Failed to parse version string due to %s", e.getMessage()), e);
+        }
+    }
+}

http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/6f26290d/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/serialization/jackson/JacksonVersionedProcessGroupSerializer.java
----------------------------------------------------------------------
diff --git a/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/serialization/jackson/JacksonVersionedProcessGroupSerializer.java b/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/serialization/jackson/JacksonVersionedProcessGroupSerializer.java
new file mode 100644
index 0000000..21bdecc
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/serialization/jackson/JacksonVersionedProcessGroupSerializer.java
@@ -0,0 +1,33 @@
+/*
+ * 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.serialization.jackson;
+
+import com.fasterxml.jackson.core.type.TypeReference;
+import org.apache.nifi.registry.flow.VersionedProcessGroup;
+import org.apache.nifi.registry.serialization.SerializationException;
+
+/**
+ * A Jackson serializer for VersionedFlowSnapshots.
+ */
+public class JacksonVersionedProcessGroupSerializer extends JacksonSerializer<VersionedProcessGroup> {
+
+
+    @Override
+    TypeReference<SerializationContainer<VersionedProcessGroup>> getDeserializeTypeRef() throws SerializationException {
+        return new TypeReference<SerializationContainer<VersionedProcessGroup>>() {};
+    }
+}

http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/6f26290d/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/serialization/jackson/ObjectMapperProvider.java
----------------------------------------------------------------------
diff --git a/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/serialization/jackson/ObjectMapperProvider.java b/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/serialization/jackson/ObjectMapperProvider.java
new file mode 100644
index 0000000..080d258
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/serialization/jackson/ObjectMapperProvider.java
@@ -0,0 +1,43 @@
+/*
+ * 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.serialization.jackson;
+
+import com.fasterxml.jackson.annotation.JsonInclude;
+import com.fasterxml.jackson.databind.DeserializationFeature;
+import com.fasterxml.jackson.databind.MapperFeature;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.module.jaxb.JaxbAnnotationIntrospector;
+
+/**
+ * Provides a singleton ObjectMapper.
+ */
+public abstract class ObjectMapperProvider {
+
+    private static final ObjectMapper mapper = new ObjectMapper();
+
+    static {
+        mapper.setSerializationInclusion(JsonInclude.Include.NON_NULL);
+        mapper.setDefaultPropertyInclusion(JsonInclude.Value.construct(JsonInclude.Include.NON_NULL, JsonInclude.Include.NON_NULL));
+        mapper.setAnnotationIntrospector(new JaxbAnnotationIntrospector(mapper.getTypeFactory()));
+        mapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
+        mapper.configure(MapperFeature.SORT_PROPERTIES_ALPHABETICALLY, true);
+    }
+
+    public static ObjectMapper getMapper() {
+        return mapper;
+    }
+}

http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/6f26290d/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/serialization/jackson/SerializationContainer.java
----------------------------------------------------------------------
diff --git a/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/serialization/jackson/SerializationContainer.java b/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/serialization/jackson/SerializationContainer.java
new file mode 100644
index 0000000..8c4d474
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/serialization/jackson/SerializationContainer.java
@@ -0,0 +1,50 @@
+/*
+ * 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.serialization.jackson;
+
+import io.swagger.annotations.ApiModelProperty;
+
+import javax.xml.bind.annotation.XmlRootElement;
+import javax.xml.bind.annotation.XmlType;
+import java.util.Map;
+
+@XmlRootElement
+@XmlType(propOrder = {"header", "content"})
+public class SerializationContainer<T> {
+
+    private Map<String, String> header;
+    private T content;
+
+    @ApiModelProperty("The serialization headers")
+    public Map<String, String> getHeader() {
+        return header;
+    }
+
+    public void setHeader(Map<String, String> header) {
+        this.header = header;
+    }
+
+    @ApiModelProperty("The serialized content")
+    public T getContent() {
+        return content;
+    }
+
+    public void setContent(T content) {
+        this.content = content;
+    }
+}

http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/6f26290d/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/serialization/jaxb/JAXBSerializer.java
----------------------------------------------------------------------
diff --git a/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/serialization/jaxb/JAXBSerializer.java b/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/serialization/jaxb/JAXBSerializer.java
new file mode 100644
index 0000000..5290fb5
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/serialization/jaxb/JAXBSerializer.java
@@ -0,0 +1,127 @@
+/*
+ * 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.serialization.jaxb;
+
+import org.apache.nifi.registry.serialization.SerializationException;
+import org.apache.nifi.registry.serialization.VersionedSerializer;
+
+import javax.xml.bind.JAXBContext;
+import javax.xml.bind.JAXBException;
+import javax.xml.bind.Marshaller;
+import javax.xml.bind.Unmarshaller;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.nio.ByteBuffer;
+import java.nio.charset.StandardCharsets;
+
+/**
+ * A Serializer that uses JAXB for serializing/deserializing.
+ */
+public class JAXBSerializer<T> implements VersionedSerializer<T> {
+
+    private static final String MAGIC_HEADER = "Flows";
+    private static final byte[] MAGIC_HEADER_BYTES = MAGIC_HEADER.getBytes(StandardCharsets.UTF_8);
+
+    private final JAXBContext jaxbContext;
+
+    /**
+     * Load the JAXBContext.
+     */
+    public JAXBSerializer(final Class<T> clazz) {
+        try {
+            this.jaxbContext = JAXBContext.newInstance(clazz);
+        } catch (JAXBException e) {
+            throw new RuntimeException("Unable to create JAXBContext: " + e.getMessage(), e);
+        }
+    }
+
+    @Override
+    public void serialize(final int dataModelVersion, final T t, final OutputStream out) throws SerializationException {
+        if (t == null) {
+            throw new IllegalArgumentException("The object to serialize cannot be null");
+        }
+
+        if (out == null) {
+            throw new IllegalArgumentException("OutputStream cannot be null");
+        }
+
+        final ByteBuffer byteBuffer = ByteBuffer.allocate(9);
+        byteBuffer.put(MAGIC_HEADER_BYTES);
+        byteBuffer.putInt(dataModelVersion);
+
+        try {
+            out.write(byteBuffer.array());
+        } catch (final IOException e) {
+            throw new SerializationException("Unable to write header while serializing process group", e);
+        }
+
+        try {
+            final Marshaller marshaller = jaxbContext.createMarshaller();
+            marshaller.setProperty(Marshaller.JAXB_FORMATTED_OUTPUT, true);
+            marshaller.marshal(t, out);
+        } catch (JAXBException e) {
+            throw new SerializationException("Unable to serialize object", e);
+        }
+    }
+
+    @Override
+    public T deserialize(final InputStream input) throws SerializationException {
+        if (input == null) {
+            throw new IllegalArgumentException("InputStream cannot be null");
+        }
+
+        try {
+            // Consume the header bytes.
+            readDataModelVersion(input);
+            final Unmarshaller unmarshaller = jaxbContext.createUnmarshaller();
+            return (T) unmarshaller.unmarshal(input);
+        } catch (JAXBException e) {
+            throw new SerializationException("Unable to deserialize object", e);
+        }
+    }
+
+    @Override
+    public int readDataModelVersion(InputStream input) throws SerializationException {
+        final int headerLength = 9;
+        final byte[] buffer = new byte[headerLength];
+
+        int bytesRead = -1;
+        try {
+            bytesRead = input.read(buffer, 0, headerLength);
+        } catch (final IOException e) {
+            throw new SerializationException("Unable to read header while deserializing process group", e);
+        }
+
+        if (bytesRead < headerLength) {
+            throw new SerializationException("Unable to read header while deserializing process group, expected"
+                    + headerLength + " bytes, but found " + bytesRead);
+        }
+
+        final ByteBuffer bb = ByteBuffer.wrap(buffer);
+        final byte[] magicHeaderBytes = new byte[MAGIC_HEADER_BYTES.length];
+        bb.get(magicHeaderBytes);
+        for (int i = 0; i < MAGIC_HEADER_BYTES.length; i++) {
+            if (MAGIC_HEADER_BYTES[i] != magicHeaderBytes[i]) {
+                throw new SerializationException("Unable to read header while deserializing process group." +
+                        " Header byte sequence does not match");
+            }
+        }
+
+        return bb.getInt(MAGIC_HEADER_BYTES.length);
+    }
+}

http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/6f26290d/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/serialization/jaxb/JAXBVersionedProcessGroupSerializer.java
----------------------------------------------------------------------
diff --git a/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/serialization/jaxb/JAXBVersionedProcessGroupSerializer.java b/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/serialization/jaxb/JAXBVersionedProcessGroupSerializer.java
new file mode 100644
index 0000000..3efdd33
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/serialization/jaxb/JAXBVersionedProcessGroupSerializer.java
@@ -0,0 +1,30 @@
+/*
+ * 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.serialization.jaxb;
+
+import org.apache.nifi.registry.flow.VersionedProcessGroup;
+
+/**
+ * A JAXB serializer for VersionedFlowSnapshots.
+ */
+public class JAXBVersionedProcessGroupSerializer extends JAXBSerializer<VersionedProcessGroup> {
+
+    public JAXBVersionedProcessGroupSerializer() {
+        super(VersionedProcessGroup.class);
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/6f26290d/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/service/AuthorizationService.java
----------------------------------------------------------------------
diff --git a/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/service/AuthorizationService.java b/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/service/AuthorizationService.java
new file mode 100644
index 0000000..204f966
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/service/AuthorizationService.java
@@ -0,0 +1,811 @@
+/*
+ * 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.service;
+
+import org.apache.nifi.registry.authorization.AccessPolicy;
+import org.apache.nifi.registry.authorization.AccessPolicySummary;
+import org.apache.nifi.registry.authorization.CurrentUser;
+import org.apache.nifi.registry.authorization.Permissions;
+import org.apache.nifi.registry.authorization.Resource;
+import org.apache.nifi.registry.authorization.ResourcePermissions;
+import org.apache.nifi.registry.authorization.Tenant;
+import org.apache.nifi.registry.authorization.User;
+import org.apache.nifi.registry.authorization.UserGroup;
+import org.apache.nifi.registry.bucket.Bucket;
+import org.apache.nifi.registry.security.authorization.AccessPolicyProvider;
+import org.apache.nifi.registry.security.authorization.AccessPolicyProviderInitializationContext;
+import org.apache.nifi.registry.security.authorization.AuthorizableLookup;
+import org.apache.nifi.registry.security.authorization.Authorizer;
+import org.apache.nifi.registry.security.authorization.AuthorizerCapabilityDetection;
+import org.apache.nifi.registry.security.authorization.AuthorizerConfigurationContext;
+import org.apache.nifi.registry.security.authorization.ConfigurableAccessPolicyProvider;
+import org.apache.nifi.registry.security.authorization.ConfigurableUserGroupProvider;
+import org.apache.nifi.registry.security.authorization.Group;
+import org.apache.nifi.registry.security.authorization.ManagedAuthorizer;
+import org.apache.nifi.registry.security.authorization.RequestAction;
+import org.apache.nifi.registry.security.authorization.UserAndGroups;
+import org.apache.nifi.registry.security.authorization.UserGroupProvider;
+import org.apache.nifi.registry.security.authorization.UserGroupProviderInitializationContext;
+import org.apache.nifi.registry.security.authorization.exception.AccessDeniedException;
+import org.apache.nifi.registry.security.authorization.exception.AuthorizationAccessException;
+import org.apache.nifi.registry.security.authorization.resource.Authorizable;
+import org.apache.nifi.registry.security.authorization.resource.ResourceFactory;
+import org.apache.nifi.registry.security.authorization.resource.ResourceType;
+import org.apache.nifi.registry.security.authorization.user.NiFiUser;
+import org.apache.nifi.registry.security.authorization.user.NiFiUserUtils;
+import org.apache.nifi.registry.security.exception.SecurityProviderCreationException;
+import org.apache.nifi.registry.security.exception.SecurityProviderDestructionException;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.List;
+import java.util.Objects;
+import java.util.Set;
+import java.util.UUID;
+import java.util.concurrent.locks.Lock;
+import java.util.concurrent.locks.ReentrantReadWriteLock;
+import java.util.stream.Collectors;
+
+@Service
+public class AuthorizationService {
+
+    public static final String MSG_NON_MANAGED_AUTHORIZER = "This NiFi Registry is not configured to internally manage users, groups, or policies. Please contact your system administrator.";
+    public static final String MSG_NON_CONFIGURABLE_POLICIES = "This NiFi Registry is not configured to allow configurable policies. Please contact your system administrator.";
+    public static final String MSG_NON_CONFIGURABLE_USERS = "This NiFi Registry is not configured to allow configurable users and groups. Please contact your system administrator.";
+
+    private AuthorizableLookup authorizableLookup;
+    private Authorizer authorizer;
+    private RegistryService registryService;
+    private UserGroupProvider userGroupProvider;
+    private AccessPolicyProvider accessPolicyProvider;
+
+    private final ReentrantReadWriteLock lock = new ReentrantReadWriteLock();
+    private final Lock readLock = lock.readLock();
+    private final Lock writeLock = lock.writeLock();
+
+    @Autowired
+    public AuthorizationService(
+            final AuthorizableLookup authorizableLookup,
+            final Authorizer authorizer,
+            final RegistryService registryService) {
+        this.authorizableLookup = authorizableLookup;
+        this.authorizer = authorizer;
+        this.registryService = registryService;
+
+        if (AuthorizerCapabilityDetection.isManagedAuthorizer(this.authorizer)) {
+            this.accessPolicyProvider = ((ManagedAuthorizer) authorizer).getAccessPolicyProvider();
+        } else {
+            this.accessPolicyProvider = createExceptionThrowingAccessPolicyProvider();
+        }
+        this.userGroupProvider = accessPolicyProvider.getUserGroupProvider();
+    }
+
+
+    // ---------------------- Authorization methods -------------------------------------
+
+    public AuthorizableLookup getAuthorizableLookup() {
+        return authorizableLookup;
+    }
+
+    public Authorizer getAuthorizer() {
+        return authorizer;
+    }
+
+    public void authorize(Authorizable authorizable, RequestAction action) throws AccessDeniedException {
+        authorizable.authorize(authorizer, action, NiFiUserUtils.getNiFiUser());
+    }
+
+    // ---------------------- Permissions methods ---------------------------------------
+
+    public CurrentUser getCurrentUser() {
+        final NiFiUser user = NiFiUserUtils.getNiFiUser();
+        final CurrentUser currentUser = new CurrentUser();
+        currentUser.setIdentity(user.getIdentity());
+        currentUser.setAnonymous(user.isAnonymous());
+        currentUser.setResourcePermissions(getTopLevelPermissions());
+        return currentUser;
+    }
+
+    public Permissions getPermissionsForResource(Authorizable authorizableResource) {
+        NiFiUser user = NiFiUserUtils.getNiFiUser();
+        final Permissions permissions = new Permissions();
+        permissions.setCanRead(authorizableResource.isAuthorized(authorizer, RequestAction.READ, user));
+        permissions.setCanWrite(authorizableResource.isAuthorized(authorizer, RequestAction.WRITE, user));
+        permissions.setCanDelete(authorizableResource.isAuthorized(authorizer, RequestAction.DELETE, user));
+        return permissions;
+    }
+
+    public Permissions getPermissionsForResource(Authorizable authorizableResource, Permissions knownParentAuthorizablePermissions) {
+        if (knownParentAuthorizablePermissions == null) {
+            return getPermissionsForResource(authorizableResource);
+        }
+
+        final Permissions permissions = new Permissions(knownParentAuthorizablePermissions);
+        NiFiUser user = NiFiUserUtils.getNiFiUser();
+
+        if (!permissions.getCanRead()) {
+            permissions.setCanRead(authorizableResource.isAuthorized(authorizer, RequestAction.READ, user));
+        }
+
+        if (!permissions.getCanWrite()) {
+            permissions.setCanWrite(authorizableResource.isAuthorized(authorizer, RequestAction.WRITE, user));
+        }
+
+        if (!permissions.getCanDelete()) {
+            permissions.setCanDelete(authorizableResource.isAuthorized(authorizer, RequestAction.DELETE, user));
+        }
+
+        return permissions;
+    }
+
+    private ResourcePermissions getTopLevelPermissions() {
+
+        NiFiUser user = NiFiUserUtils.getNiFiUser();
+        ResourcePermissions resourcePermissions = new ResourcePermissions();
+
+        final Permissions bucketsPermissions = getPermissionsForResource(authorizableLookup.getBucketsAuthorizable());
+        resourcePermissions.setBuckets(bucketsPermissions);
+
+        final Permissions policiesPermissions = getPermissionsForResource(authorizableLookup.getPoliciesAuthorizable());
+        resourcePermissions.setPolicies(policiesPermissions);
+
+        final Permissions tenantsPermissions = getPermissionsForResource(authorizableLookup.getTenantsAuthorizable());
+        resourcePermissions.setTenants(tenantsPermissions);
+
+        final Permissions proxyPermissions = getPermissionsForResource(authorizableLookup.getProxyAuthorizable());
+        resourcePermissions.setProxy(proxyPermissions);
+
+        return resourcePermissions;
+    }
+
+    // ---------------------- User methods ----------------------------------------------
+
+    public User createUser(User user) {
+        verifyUserGroupProviderIsConfigurable();
+        writeLock.lock();
+        try {
+            final org.apache.nifi.registry.security.authorization.User createdUser =
+                ((ConfigurableUserGroupProvider) userGroupProvider).addUser(userFromDTO(user));
+            return userToDTO(createdUser);
+        } finally {
+            writeLock.unlock();
+        }
+    }
+
+    public List<User> getUsers() {
+        this.readLock.lock();
+        try {
+            return userGroupProvider.getUsers().stream().map(this::userToDTO).collect(Collectors.toList());
+        } finally {
+            this.readLock.unlock();
+        }
+    }
+
+    public User getUser(String identifier) {
+        this.readLock.lock();
+        try {
+            return userToDTO(userGroupProvider.getUser(identifier));
+        } finally {
+            this.readLock.unlock();
+        }
+    }
+
+    public User getUserByIdentity(String identity) {
+        this.readLock.lock();
+        try {
+            return userToDTO(userGroupProvider.getUserByIdentity(identity));
+        } finally {
+            this.readLock.unlock();
+        }
+    }
+
+    public User updateUser(User user) {
+        verifyUserGroupProviderIsConfigurable();
+        this.writeLock.lock();
+        try {
+            final org.apache.nifi.registry.security.authorization.User updatedUser =
+                    ((ConfigurableUserGroupProvider) userGroupProvider).updateUser(userFromDTO(user));
+            if (updatedUser == null) {
+                return null;
+            }
+            return userToDTO(updatedUser);
+        } finally {
+            this.writeLock.unlock();
+        }
+    }
+
+    public User deleteUser(String identifier) {
+        verifyUserGroupProviderIsConfigurable();
+        this.writeLock.lock();
+        try {
+            User deletedUserDTO = getUser(identifier);
+            if (deletedUserDTO != null) {
+                ((ConfigurableUserGroupProvider) userGroupProvider).deleteUser(identifier);
+            }
+            return deletedUserDTO;
+        } finally {
+            this.writeLock.unlock();
+        }
+    }
+
+
+    // ---------------------- User Group methods --------------------------------------
+
+    public UserGroup createUserGroup(UserGroup userGroup) {
+        verifyUserGroupProviderIsConfigurable();
+        writeLock.lock();
+        try {
+            final org.apache.nifi.registry.security.authorization.Group createdGroup =
+                    ((ConfigurableUserGroupProvider) userGroupProvider).addGroup(userGroupFromDTO(userGroup));
+            return userGroupToDTO(createdGroup);
+        } finally {
+            writeLock.unlock();
+        }
+    }
+
+    public List<UserGroup> getUserGroups() {
+        this.readLock.lock();
+        try {
+            return userGroupProvider.getGroups().stream().map(this::userGroupToDTO).collect(Collectors.toList());
+        } finally {
+            this.readLock.unlock();
+        }
+    }
+
+    public UserGroup getUserGroup(String identifier) {
+        this.readLock.lock();
+        try {
+            return userGroupToDTO(userGroupProvider.getGroup(identifier));
+        } finally {
+            this.readLock.unlock();
+        }
+    }
+
+    public UserGroup updateUserGroup(UserGroup userGroup) {
+        verifyUserGroupProviderIsConfigurable();
+        writeLock.lock();
+        try {
+            final org.apache.nifi.registry.security.authorization.Group updatedGroup =
+                    ((ConfigurableUserGroupProvider) userGroupProvider).updateGroup(userGroupFromDTO(userGroup));
+            if (updatedGroup == null) {
+                return null;
+            }
+            return userGroupToDTO(updatedGroup);
+        } finally {
+            writeLock.unlock();
+        }
+    }
+
+    public UserGroup deleteUserGroup(String identifier) {
+        verifyUserGroupProviderIsConfigurable();
+        writeLock.lock();
+        try {
+            final UserGroup userGroupDTO = getUserGroup(identifier);
+            if (userGroupDTO != null) {
+                ((ConfigurableUserGroupProvider) userGroupProvider).deleteGroup(identifier);
+            }
+            return userGroupDTO;
+        } finally {
+            writeLock.unlock();
+        }
+    }
+
+
+    // ---------------------- Access Policy methods ----------------------------------------
+
+    public AccessPolicy createAccessPolicy(AccessPolicy accessPolicy) {
+        verifyAccessPolicyProviderIsConfigurable();
+        writeLock.lock();
+        try {
+            org.apache.nifi.registry.security.authorization.AccessPolicy createdAccessPolicy =
+                    ((ConfigurableAccessPolicyProvider) accessPolicyProvider).addAccessPolicy(accessPolicyFromDTO(accessPolicy));
+            return accessPolicyToDTO(createdAccessPolicy);
+        } finally {
+            writeLock.unlock();
+        }
+    }
+
+    public AccessPolicy getAccessPolicy(String identifier) {
+        readLock.lock();
+        try {
+            return accessPolicyToDTO(accessPolicyProvider.getAccessPolicy(identifier));
+        } finally {
+            readLock.unlock();
+        }
+    }
+
+    public AccessPolicy getAccessPolicy(String resource, RequestAction action) {
+        readLock.lock();
+        try {
+            return accessPolicyToDTO(accessPolicyProvider.getAccessPolicy(resource, action));
+        } finally {
+            readLock.unlock();
+        }
+    }
+
+    public List<AccessPolicy> getAccessPolicies() {
+        readLock.lock();
+        try {
+            return accessPolicyProvider.getAccessPolicies().stream().map(this::accessPolicyToDTO).collect(Collectors.toList());
+        } finally {
+            readLock.unlock();
+        }
+    }
+
+    public List<AccessPolicySummary> getAccessPolicySummaries() {
+        readLock.lock();
+        try {
+            return accessPolicyProvider.getAccessPolicies().stream().map(this::accessPolicyToSummaryDTO).collect(Collectors.toList());
+        } finally {
+            readLock.unlock();
+        }
+    }
+
+    private List<AccessPolicySummary> getAccessPolicySummariesForUser(String userIdentifier) {
+        readLock.lock();
+        try {
+            return accessPolicyProvider.getAccessPolicies().stream()
+                    .filter(accessPolicy -> accessPolicy.getUsers().contains(userIdentifier))
+                    .map(this::accessPolicyToSummaryDTO)
+                    .collect(Collectors.toList());
+        } finally {
+            readLock.unlock();
+        }
+    }
+
+    private List<AccessPolicySummary> getAccessPolicySummariesForUserGroup(String userGroupIdentifier) {
+        readLock.lock();
+        try {
+            return accessPolicyProvider.getAccessPolicies().stream()
+                    .filter(accessPolicy -> accessPolicy.getGroups().contains(userGroupIdentifier))
+                    .map(this::accessPolicyToSummaryDTO)
+                    .collect(Collectors.toList());
+        } finally {
+            readLock.unlock();
+        }
+    }
+
+    public AccessPolicy updateAccessPolicy(AccessPolicy accessPolicy) {
+        verifyAccessPolicyProviderIsConfigurable();
+        writeLock.lock();
+        try {
+            // Don't allow changing action or resource of existing policy (should only be adding/removing users/groups)
+            org.apache.nifi.registry.security.authorization.AccessPolicy currentAccessPolicy =
+                    accessPolicyProvider.getAccessPolicy(accessPolicy.getIdentifier());
+            if (currentAccessPolicy == null) {
+                return null;
+            }
+            accessPolicy.setResource(currentAccessPolicy.getResource());
+            accessPolicy.setAction(currentAccessPolicy.getAction().toString());
+
+            org.apache.nifi.registry.security.authorization.AccessPolicy updatedAccessPolicy =
+                    ((ConfigurableAccessPolicyProvider) accessPolicyProvider).updateAccessPolicy(accessPolicyFromDTO(accessPolicy));
+            if (updatedAccessPolicy == null) {
+                return null;
+            }
+            return accessPolicyToDTO(updatedAccessPolicy);
+        } finally {
+            writeLock.unlock();
+        }
+    }
+
+    public AccessPolicy deleteAccessPolicy(String identifier) {
+        verifyAccessPolicyProviderIsConfigurable();
+        writeLock.lock();
+        try {
+            AccessPolicy deletedAccessPolicyDTO = getAccessPolicy(identifier);
+            if (deletedAccessPolicyDTO != null) {
+                ((ConfigurableAccessPolicyProvider) accessPolicyProvider).deleteAccessPolicy(identifier);
+            }
+            return deletedAccessPolicyDTO;
+        } finally {
+            writeLock.unlock();
+        }
+    }
+
+
+    // ---------------------- Resource Lookup methods --------------------------------------
+
+    public List<Resource> getResources() {
+        final List<Resource> dtoResources =
+                getAuthorizableResources()
+                        .stream()
+                        .map(AuthorizationService::resourceToDTO)
+                        .collect(Collectors.toList());
+        return dtoResources;
+    }
+
+    public List<Resource> getAuthorizedResources(RequestAction actionType) {
+        return getAuthorizedResources(actionType, null);
+    }
+
+    public List<Resource> getAuthorizedResources(RequestAction actionType, ResourceType resourceType) {
+        final List<Resource> authorizedResources =
+                getAuthorizableResources(resourceType)
+                        .stream()
+                        .filter(resource -> {
+                            String resourceId = resource.getIdentifier();
+                            try {
+                                authorizableLookup
+                                        .getAuthorizableByResource(resource.getIdentifier())
+                                        .authorize(authorizer, actionType, NiFiUserUtils.getNiFiUser());
+                                return true;
+                            } catch (AccessDeniedException e) {
+                                return false;
+                            }
+                        })
+                        .map(AuthorizationService::resourceToDTO)
+                        .collect(Collectors.toList());
+
+        return authorizedResources;
+    }
+
+    // ---------------------- Private Helper methods --------------------------------------
+
+    private void verifyUserGroupProviderIsConfigurable() {
+        if (!(userGroupProvider instanceof ConfigurableUserGroupProvider)) {
+            throw new IllegalStateException(MSG_NON_CONFIGURABLE_USERS);
+        }
+    }
+
+    private void verifyAccessPolicyProviderIsConfigurable() {
+        if (!(accessPolicyProvider instanceof ConfigurableAccessPolicyProvider)) {
+            throw new IllegalStateException(MSG_NON_CONFIGURABLE_POLICIES);
+        }
+    }
+
+    private ResourcePermissions getTopLevelPermissions(String tenantIdentifier) {
+        ResourcePermissions resourcePermissions = new ResourcePermissions();
+
+        final Permissions bucketsPermissions = getPermissionsForResource(tenantIdentifier, ResourceFactory.getBucketsResource());
+        resourcePermissions.setBuckets(bucketsPermissions);
+
+        final Permissions policiesPermissions = getPermissionsForResource(tenantIdentifier, ResourceFactory.getPoliciesResource());
+        resourcePermissions.setPolicies(policiesPermissions);
+
+        final Permissions tenantsPermissions = getPermissionsForResource(tenantIdentifier, ResourceFactory.getTenantsResource());
+        resourcePermissions.setTenants(tenantsPermissions);
+
+        final Permissions proxyPermissions = getPermissionsForResource(tenantIdentifier, ResourceFactory.getProxyResource());
+        resourcePermissions.setProxy(proxyPermissions);
+
+        return resourcePermissions;
+    }
+
+    private Permissions getPermissionsForResource(String tenantIdentifier, org.apache.nifi.registry.security.authorization.Resource resource) {
+
+        Permissions permissions = new Permissions();
+        permissions.setCanRead(checkTenantBelongsToPolicy(tenantIdentifier, resource, RequestAction.READ));
+        permissions.setCanWrite(checkTenantBelongsToPolicy(tenantIdentifier, resource, RequestAction.WRITE));
+        permissions.setCanDelete(checkTenantBelongsToPolicy(tenantIdentifier, resource, RequestAction.DELETE));
+        return permissions;
+
+    }
+
+    private boolean checkTenantBelongsToPolicy(String tenantIdentifier, org.apache.nifi.registry.security.authorization.Resource resource, RequestAction action) {
+        org.apache.nifi.registry.security.authorization.AccessPolicy policy =
+                accessPolicyProvider.getAccessPolicy(resource.getIdentifier(), action);
+
+        if (policy == null) {
+            return false;
+        }
+
+        boolean tenantInPolicy = policy.getUsers().contains(tenantIdentifier) || policy.getGroups().contains(tenantIdentifier);
+        return tenantInPolicy;
+    }
+
+    private List<org.apache.nifi.registry.security.authorization.Resource> getAuthorizableResources() {
+        return getAuthorizableResources(null);
+    }
+
+    private List<org.apache.nifi.registry.security.authorization.Resource> getAuthorizableResources(ResourceType includeFilter) {
+
+        final List<org.apache.nifi.registry.security.authorization.Resource> resources = new ArrayList<>();
+
+        if (includeFilter == null || includeFilter.equals(ResourceType.Policy)) {
+            resources.add(ResourceFactory.getPoliciesResource());
+        }
+        if (includeFilter == null || includeFilter.equals(ResourceType.Tenant)) {
+            resources.add(ResourceFactory.getTenantsResource());
+        }
+        if (includeFilter == null || includeFilter.equals(ResourceType.Proxy)) {
+            resources.add(ResourceFactory.getProxyResource());
+        }
+        if (includeFilter == null || includeFilter.equals(ResourceType.Actuator)) {
+            resources.add(ResourceFactory.getActuatorResource());
+        }
+        if (includeFilter == null || includeFilter.equals(ResourceType.Swagger)) {
+            resources.add(ResourceFactory.getSwaggerResource());
+        }
+        if (includeFilter == null || includeFilter.equals(ResourceType.Bucket)) {
+            resources.add(ResourceFactory.getBucketsResource());
+            // add all buckets
+            for (final Bucket bucket : registryService.getBuckets()) {
+                resources.add(ResourceFactory.getBucketResource(bucket.getIdentifier(), bucket.getName()));
+            }
+        }
+
+        return resources;
+    }
+
+    private User userToDTO(
+            final org.apache.nifi.registry.security.authorization.User user) {
+        if (user == null) {
+            return null;
+        }
+        String userIdentifier = user.getIdentifier();
+
+        Collection<Tenant> groupsContainingUser = userGroupProvider.getGroups().stream()
+                .filter(group -> group.getUsers().contains(userIdentifier))
+                .map(this::tenantToDTO)
+                .collect(Collectors.toList());
+        Collection<AccessPolicySummary> accessPolicySummaries = getAccessPolicySummariesForUser(userIdentifier);
+
+        User userDTO = new User(user.getIdentifier(), user.getIdentity());
+        userDTO.setConfigurable(AuthorizerCapabilityDetection.isUserConfigurable(authorizer, user));
+        userDTO.setResourcePermissions(getTopLevelPermissions(userDTO.getIdentifier()));
+        userDTO.addUserGroups(groupsContainingUser);
+        userDTO.addAccessPolicies(accessPolicySummaries);
+        return userDTO;
+    }
+
+    private UserGroup userGroupToDTO(
+            final org.apache.nifi.registry.security.authorization.Group userGroup) {
+        if (userGroup == null) {
+            return null;
+        }
+
+        Collection<Tenant> userTenants = userGroup.getUsers() != null
+                ? userGroup.getUsers().stream().map(this::tenantIdToDTO).collect(Collectors.toSet()) : null;
+        Collection<AccessPolicySummary> accessPolicySummaries = getAccessPolicySummariesForUserGroup(userGroup.getIdentifier());
+
+        UserGroup userGroupDTO = new UserGroup(userGroup.getIdentifier(), userGroup.getName());
+        userGroupDTO.setConfigurable(AuthorizerCapabilityDetection.isGroupConfigurable(authorizer, userGroup));
+        userGroupDTO.setResourcePermissions(getTopLevelPermissions(userGroupDTO.getIdentifier()));
+        userGroupDTO.addUsers(userTenants);
+        userGroupDTO.addAccessPolicies(accessPolicySummaries);
+        return userGroupDTO;
+    }
+
+    private AccessPolicy accessPolicyToDTO(
+            final org.apache.nifi.registry.security.authorization.AccessPolicy accessPolicy) {
+        if (accessPolicy == null) {
+            return null;
+        }
+
+        Collection<Tenant> users = accessPolicy.getUsers() != null
+                ? accessPolicy.getUsers().stream().map(this::tenantIdToDTO).filter(Objects::nonNull).collect(Collectors.toList()) : null;
+        Collection<Tenant> userGroups = accessPolicy.getGroups() != null
+                ? accessPolicy.getGroups().stream().map(this::tenantIdToDTO).filter(Objects::nonNull).collect(Collectors.toList()) : null;
+
+        Boolean isConfigurable = AuthorizerCapabilityDetection.isAccessPolicyConfigurable(authorizer, accessPolicy);
+
+        return accessPolicyToDTO(accessPolicy, userGroups, users, isConfigurable);
+    }
+
+    private Tenant tenantIdToDTO(String identifier) {
+        this.readLock.lock();
+        try {
+            org.apache.nifi.registry.security.authorization.User user = userGroupProvider.getUser(identifier);
+            if (user != null) {
+                return tenantToDTO(user);
+            } else {
+                org.apache.nifi.registry.security.authorization.Group group = userGroupProvider.getGroup(identifier);
+                return tenantToDTO(group);
+            }
+        } finally {
+            this.readLock.unlock();
+        }
+    }
+
+    private AccessPolicySummary accessPolicyToSummaryDTO(
+            final org.apache.nifi.registry.security.authorization.AccessPolicy accessPolicy) {
+        if (accessPolicy == null) {
+            return null;
+        }
+
+        Boolean isConfigurable = AuthorizerCapabilityDetection.isAccessPolicyConfigurable(authorizer, accessPolicy);
+
+        final AccessPolicySummary accessPolicySummaryDTO = new AccessPolicySummary();
+        accessPolicySummaryDTO.setIdentifier(accessPolicy.getIdentifier());
+        accessPolicySummaryDTO.setAction(accessPolicy.getAction().toString());
+        accessPolicySummaryDTO.setResource(accessPolicy.getResource());
+        accessPolicySummaryDTO.setConfigurable(isConfigurable);
+        return accessPolicySummaryDTO;
+    }
+
+    private Tenant tenantToDTO(org.apache.nifi.registry.security.authorization.User user) {
+        if (user == null) {
+            return null;
+        }
+        Tenant tenantDTO = new Tenant(user.getIdentifier(), user.getIdentity());
+        tenantDTO.setConfigurable(AuthorizerCapabilityDetection.isUserConfigurable(authorizer, user));
+        return tenantDTO;
+    }
+
+    private Tenant tenantToDTO(org.apache.nifi.registry.security.authorization.Group group) {
+        if (group == null) {
+            return null;
+        }
+        Tenant tenantDTO = new Tenant(group.getIdentifier(), group.getName());
+        tenantDTO.setConfigurable(AuthorizerCapabilityDetection.isGroupConfigurable(authorizer, group));
+        return tenantDTO;
+    }
+
+    private static Resource resourceToDTO(org.apache.nifi.registry.security.authorization.Resource resource) {
+        if (resource == null) {
+            return null;
+        }
+        Resource resourceDto = new Resource();
+        resourceDto.setIdentifier(resource.getIdentifier());
+        resourceDto.setName(resource.getName());
+        return resourceDto;
+    }
+
+    private static org.apache.nifi.registry.security.authorization.User userFromDTO(
+            final User userDTO) {
+        if (userDTO == null) {
+            return null;
+        }
+        return new org.apache.nifi.registry.security.authorization.User.Builder()
+                .identifier(userDTO.getIdentifier() != null ? userDTO.getIdentifier() : UUID.randomUUID().toString())
+                .identity(userDTO.getIdentity())
+                .build();
+    }
+
+    private static org.apache.nifi.registry.security.authorization.Group userGroupFromDTO(
+            final UserGroup userGroupDTO) {
+        if (userGroupDTO == null) {
+            return null;
+        }
+        org.apache.nifi.registry.security.authorization.Group.Builder groupBuilder = new org.apache.nifi.registry.security.authorization.Group.Builder()
+                .identifier(userGroupDTO.getIdentifier() != null ? userGroupDTO.getIdentifier() : UUID.randomUUID().toString())
+                .name(userGroupDTO.getIdentity());
+        Set<Tenant> users = userGroupDTO.getUsers();
+        if (users != null) {
+            groupBuilder.addUsers(users.stream().map(Tenant::getIdentifier).collect(Collectors.toSet()));
+        }
+        return groupBuilder.build();
+    }
+
+    private static org.apache.nifi.registry.security.authorization.AccessPolicy accessPolicyFromDTO(
+            final AccessPolicy accessPolicyDTO) {
+        org.apache.nifi.registry.security.authorization.AccessPolicy.Builder accessPolicyBuilder =
+                new org.apache.nifi.registry.security.authorization.AccessPolicy.Builder()
+                        .identifier(accessPolicyDTO.getIdentifier() != null ? accessPolicyDTO.getIdentifier() : UUID.randomUUID().toString())
+                        .resource(accessPolicyDTO.getResource())
+                        .action(RequestAction.valueOfValue(accessPolicyDTO.getAction()));
+
+        Set<Tenant> dtoUsers = accessPolicyDTO.getUsers();
+        if (accessPolicyDTO.getUsers() != null) {
+            accessPolicyBuilder.addUsers(dtoUsers.stream().map(Tenant::getIdentifier).collect(Collectors.toSet()));
+        }
+
+        Set<Tenant> dtoUserGroups = accessPolicyDTO.getUserGroups();
+        if (dtoUserGroups != null) {
+            accessPolicyBuilder.addGroups(dtoUserGroups.stream().map(Tenant::getIdentifier).collect(Collectors.toSet()));
+        }
+
+        return accessPolicyBuilder.build();
+    }
+
+    private static AccessPolicy accessPolicyToDTO(
+            final org.apache.nifi.registry.security.authorization.AccessPolicy accessPolicy,
+            final Collection<? extends Tenant> userGroups,
+            final Collection<? extends Tenant> users,
+            final Boolean isConfigurable) {
+
+        if (accessPolicy == null) {
+            return null;
+        }
+
+        final AccessPolicy accessPolicyDTO = new AccessPolicy();
+        accessPolicyDTO.setIdentifier(accessPolicy.getIdentifier());
+        accessPolicyDTO.setAction(accessPolicy.getAction().toString());
+        accessPolicyDTO.setResource(accessPolicy.getResource());
+        accessPolicyDTO.setConfigurable(isConfigurable);
+        accessPolicyDTO.addUsers(users);
+        accessPolicyDTO.addUserGroups(userGroups);
+        return accessPolicyDTO;
+    }
+
+    private static AccessPolicyProvider createExceptionThrowingAccessPolicyProvider() {
+
+        return new AccessPolicyProvider() {
+            @Override
+            public Set<org.apache.nifi.registry.security.authorization.AccessPolicy> getAccessPolicies() throws AuthorizationAccessException {
+                throw new IllegalStateException(MSG_NON_MANAGED_AUTHORIZER);
+            }
+
+            @Override
+            public org.apache.nifi.registry.security.authorization.AccessPolicy getAccessPolicy(String identifier) throws AuthorizationAccessException {
+                throw new IllegalStateException(MSG_NON_MANAGED_AUTHORIZER);
+            }
+
+            @Override
+            public org.apache.nifi.registry.security.authorization.AccessPolicy getAccessPolicy(String resourceIdentifier, RequestAction action) throws AuthorizationAccessException {
+                throw new IllegalStateException(MSG_NON_MANAGED_AUTHORIZER);
+            }
+
+            @Override
+            public UserGroupProvider getUserGroupProvider() {
+                return new UserGroupProvider() {
+                    @Override
+                    public Set<org.apache.nifi.registry.security.authorization.User> getUsers() throws AuthorizationAccessException {
+                        throw new IllegalStateException(MSG_NON_MANAGED_AUTHORIZER);
+                    }
+
+                    @Override
+                    public org.apache.nifi.registry.security.authorization.User getUser(String identifier) throws AuthorizationAccessException {
+                        throw new IllegalStateException(MSG_NON_MANAGED_AUTHORIZER);
+                    }
+
+                    @Override
+                    public org.apache.nifi.registry.security.authorization.User getUserByIdentity(String identity) throws AuthorizationAccessException {
+                        throw new IllegalStateException(MSG_NON_MANAGED_AUTHORIZER);
+                    }
+
+                    @Override
+                    public Set<Group> getGroups() throws AuthorizationAccessException {
+                        throw new IllegalStateException(MSG_NON_MANAGED_AUTHORIZER);
+                    }
+
+                    @Override
+                    public Group getGroup(String identifier) throws AuthorizationAccessException {
+                        throw new IllegalStateException(MSG_NON_MANAGED_AUTHORIZER);
+                    }
+
+                    @Override
+                    public UserAndGroups getUserAndGroups(String identity) throws AuthorizationAccessException {
+                        throw new IllegalStateException(MSG_NON_MANAGED_AUTHORIZER);
+                    }
+
+                    @Override
+                    public void initialize(UserGroupProviderInitializationContext initializationContext) throws SecurityProviderCreationException {
+
+                    }
+
+                    @Override
+                    public void onConfigured(AuthorizerConfigurationContext configurationContext) throws SecurityProviderCreationException {
+
+                    }
+
+                    @Override
+                    public void preDestruction() throws SecurityProviderDestructionException {
+
+                    }
+                };
+            }
+
+            @Override
+            public void initialize(AccessPolicyProviderInitializationContext initializationContext) throws SecurityProviderCreationException {
+            }
+
+            @Override
+            public void onConfigured(AuthorizerConfigurationContext configurationContext) throws SecurityProviderCreationException {
+            }
+
+            @Override
+            public void preDestruction() throws SecurityProviderDestructionException {
+            }
+        };
+
+    }
+
+}


[14/51] [partial] nifi-registry git commit: NIFIREG-201 Refactoring project structure to better isolate extensions

Posted by kd...@apache.org.
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/security/authentication/kerberos/KerberosIdentityProvider.java
----------------------------------------------------------------------
diff --git a/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/security/authentication/kerberos/KerberosIdentityProvider.java b/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/security/authentication/kerberos/KerberosIdentityProvider.java
new file mode 100644
index 0000000..5e6e7bb
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/security/authentication/kerberos/KerberosIdentityProvider.java
@@ -0,0 +1,111 @@
+/*
+ * 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.security.authentication.kerberos;
+
+import org.apache.commons.lang3.StringUtils;
+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.IdentityProviderConfigurationContext;
+import org.apache.nifi.registry.security.authentication.exception.IdentityAccessException;
+import org.apache.nifi.registry.security.authentication.exception.InvalidCredentialsException;
+import org.apache.nifi.registry.security.exception.SecurityProviderCreationException;
+import org.apache.nifi.registry.security.exception.SecurityProviderDestructionException;
+import org.apache.nifi.registry.util.FormatUtils;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
+import org.springframework.security.core.Authentication;
+import org.springframework.security.core.AuthenticationException;
+import org.springframework.security.kerberos.authentication.KerberosAuthenticationProvider;
+import org.springframework.security.kerberos.authentication.sun.SunJaasKerberosClient;
+
+import java.util.concurrent.TimeUnit;
+
+public class KerberosIdentityProvider extends BasicAuthIdentityProvider {
+
+    private static final Logger logger = LoggerFactory.getLogger(KerberosIdentityProvider.class);
+    private static final String issuer = KerberosIdentityProvider.class.getSimpleName();
+    private static final String default_expiration = "12 hours";
+
+    private KerberosAuthenticationProvider provider;
+
+    private long expiration;
+
+    @Override
+    public void onConfigured(IdentityProviderConfigurationContext configurationContext) throws SecurityProviderCreationException {
+
+        String rawDebug = configurationContext.getProperty("Enable Debug");
+        boolean enableDebug = (rawDebug != null && rawDebug.equalsIgnoreCase("true"));
+
+        String rawExpiration = configurationContext.getProperty("Authentication Expiration");
+        if (StringUtils.isBlank(rawExpiration)) {
+            rawExpiration = default_expiration;
+            logger.info("No Authentication Expiration specified, defaulting to " + default_expiration);
+        }
+
+        try {
+            expiration = FormatUtils.getTimeDuration(rawExpiration, TimeUnit.MILLISECONDS);
+        } catch (final IllegalArgumentException iae) {
+            throw new SecurityProviderCreationException(
+                    String.format("The Expiration Duration '%s' is not a valid time duration", rawExpiration));
+        }
+
+        provider = new KerberosAuthenticationProvider();
+        SunJaasKerberosClient client = new SunJaasKerberosClient();
+        client.setDebug(enableDebug);
+        provider.setKerberosClient(client);
+        provider.setUserDetailsService(new KerberosUserDetailsService());
+
+    }
+
+    @Override
+    public AuthenticationResponse authenticate(AuthenticationRequest authenticationRequest) throws InvalidCredentialsException, IdentityAccessException {
+
+        if (provider == null) {
+            throw new IdentityAccessException("The Kerberos authentication provider is not initialized.");
+        }
+
+        try {
+            // perform the authentication
+            final String username = authenticationRequest.getUsername();
+            final Object credentials = authenticationRequest.getCredentials();
+            final String password = credentials != null && credentials instanceof String ? (String) credentials : null;
+
+            // perform the authentication
+            final UsernamePasswordAuthenticationToken token = new UsernamePasswordAuthenticationToken(username, credentials);
+            logger.debug("Created authentication token " + token.toString());
+
+            final Authentication authentication = provider.authenticate(token);
+            logger.debug("Ran provider.authenticate(token) and returned authentication for " +
+                    "principal={} with name={} and isAuthenticated={}",
+                    authentication.getPrincipal(),
+                    authentication.getName(),
+                    authentication.isAuthenticated());
+
+            return new AuthenticationResponse(authentication.getName(), username, expiration, issuer);
+        } catch (final AuthenticationException e) {
+            throw new InvalidCredentialsException(e.getMessage(), e);
+        }
+
+    }
+
+    @Override
+    public void preDestruction() throws SecurityProviderDestructionException {
+
+    }
+}

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/security/authentication/kerberos/KerberosSpnegoFactory.java
----------------------------------------------------------------------
diff --git a/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/security/authentication/kerberos/KerberosSpnegoFactory.java b/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/security/authentication/kerberos/KerberosSpnegoFactory.java
new file mode 100644
index 0000000..16211ed
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/security/authentication/kerberos/KerberosSpnegoFactory.java
@@ -0,0 +1,67 @@
+/*
+ * 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.security.authentication.kerberos;
+
+import org.apache.nifi.registry.properties.NiFiRegistryProperties;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.security.kerberos.authentication.KerberosServiceAuthenticationProvider;
+import org.springframework.security.kerberos.authentication.KerberosTicketValidator;
+
+@Configuration
+public class KerberosSpnegoFactory {
+
+    @Autowired
+    private NiFiRegistryProperties properties;
+
+    @Autowired(required = false)
+    private KerberosTicketValidator kerberosTicketValidator;
+
+    private KerberosServiceAuthenticationProvider kerberosServiceAuthenticationProvider;
+    private KerberosSpnegoIdentityProvider kerberosSpnegoIdentityProvider;
+
+    @Bean
+    public KerberosSpnegoIdentityProvider kerberosSpnegoIdentityProvider() throws Exception {
+
+        if (kerberosSpnegoIdentityProvider == null && properties.isKerberosSpnegoSupportEnabled()) {
+            kerberosSpnegoIdentityProvider = new KerberosSpnegoIdentityProvider(
+                    kerberosServiceAuthenticationProvider(),
+                    properties);
+        }
+
+        return kerberosSpnegoIdentityProvider;
+    }
+
+
+    private KerberosServiceAuthenticationProvider kerberosServiceAuthenticationProvider() throws Exception {
+
+        if (kerberosServiceAuthenticationProvider == null && properties.isKerberosSpnegoSupportEnabled()) {
+
+            KerberosServiceAuthenticationProvider ksap = new KerberosServiceAuthenticationProvider();
+            ksap.setTicketValidator(kerberosTicketValidator);
+            ksap.setUserDetailsService(new KerberosUserDetailsService());
+            ksap.afterPropertiesSet();
+
+            kerberosServiceAuthenticationProvider = ksap;
+
+        }
+
+        return kerberosServiceAuthenticationProvider;
+    }
+
+}

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/security/authentication/kerberos/KerberosSpnegoIdentityProvider.java
----------------------------------------------------------------------
diff --git a/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/security/authentication/kerberos/KerberosSpnegoIdentityProvider.java b/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/security/authentication/kerberos/KerberosSpnegoIdentityProvider.java
new file mode 100644
index 0000000..e611b53
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/security/authentication/kerberos/KerberosSpnegoIdentityProvider.java
@@ -0,0 +1,184 @@
+/*
+ * 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.security.authentication.kerberos;
+
+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.IdentityProvider;
+import org.apache.nifi.registry.security.authentication.IdentityProviderConfigurationContext;
+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.exception.SecurityProviderCreationException;
+import org.apache.nifi.registry.security.exception.SecurityProviderDestructionException;
+import org.apache.nifi.registry.security.util.CryptoUtils;
+import org.apache.nifi.registry.util.FormatUtils;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.lang.Nullable;
+import org.springframework.security.authentication.AuthenticationDetailsSource;
+import org.springframework.security.core.Authentication;
+import org.springframework.security.core.AuthenticationException;
+import org.springframework.security.crypto.codec.Base64;
+import org.springframework.security.kerberos.authentication.KerberosServiceAuthenticationProvider;
+import org.springframework.security.kerberos.authentication.KerberosServiceRequestToken;
+import org.springframework.security.web.authentication.WebAuthenticationDetailsSource;
+import org.springframework.stereotype.Component;
+
+import javax.servlet.http.HttpServletRequest;
+import java.nio.charset.StandardCharsets;
+import java.util.concurrent.TimeUnit;
+
+@Component
+public class KerberosSpnegoIdentityProvider implements IdentityProvider {
+
+    private static final Logger logger = LoggerFactory.getLogger(KerberosSpnegoIdentityProvider.class);
+
+    private static final String issuer = KerberosSpnegoIdentityProvider.class.getSimpleName();
+
+    private static final IdentityProviderUsage usage = new IdentityProviderUsage() {
+        @Override
+        public String getText() {
+            return "The Kerberos user credentials must be passed in the HTTP Authorization header as specified by SPNEGO-based Kerberos. " +
+                    "That is: 'Authorization: Negotiate <kerberosTicket>', " +
+                    "where <kerberosTicket> is a value that will be validated by this identity provider against a Kerberos cluster.";
+        }
+
+        @Override
+        public AuthType getAuthType() {
+            return AuthType.NEGOTIATE;
+        }
+    };
+
+    private static final String AUTHORIZATION = "Authorization";
+    private static final String AUTHORIZATION_NEGOTIATE = "Negotiate";
+
+    private long expiration = TimeUnit.MILLISECONDS.convert(12, TimeUnit.HOURS);;
+    private KerberosServiceAuthenticationProvider kerberosServiceAuthenticationProvider;
+    private AuthenticationDetailsSource<HttpServletRequest, ?> authenticationDetailsSource;
+
+    @Autowired
+    public KerberosSpnegoIdentityProvider(
+            @Nullable  KerberosServiceAuthenticationProvider kerberosServiceAuthenticationProvider,
+            NiFiRegistryProperties properties) {
+        this.kerberosServiceAuthenticationProvider = kerberosServiceAuthenticationProvider;
+        authenticationDetailsSource = new WebAuthenticationDetailsSource();
+
+        final String expirationFromProperties = properties.getKerberosSpnegoAuthenticationExpiration();
+        if (expirationFromProperties != null) {
+            long expiration = FormatUtils.getTimeDuration(expirationFromProperties, TimeUnit.MILLISECONDS);
+        }
+    }
+
+    @Override
+    public IdentityProviderUsage getUsageInstructions() {
+        return usage;
+    }
+
+    @Override
+    public AuthenticationRequest extractCredentials(HttpServletRequest request) {
+
+        // Only support Kerberos authentication when running securely
+        if (!request.isSecure()) {
+            return null;
+        }
+
+        String headerValue = request.getHeader(AUTHORIZATION);
+
+        if (!isValidKerberosHeader(headerValue)) {
+            return null;
+        }
+
+        logger.debug("Detected 'Authorization: Negotiate header in request {}", request.getRequestURL());
+        byte[] base64Token = headerValue.substring(headerValue.indexOf(" ") + 1).getBytes(StandardCharsets.UTF_8);
+        byte[] kerberosTicket = Base64.decode(base64Token);
+        if (kerberosTicket != null) {
+            logger.debug("Successfully decoded SPNEGO/Kerberos ticket passed in Authorization: Negotiate <ticket> header.", request.getRequestURL());
+        }
+
+        return new AuthenticationRequest(null, kerberosTicket, authenticationDetailsSource.buildDetails(request));
+
+    }
+
+    @Override
+    public AuthenticationResponse authenticate(AuthenticationRequest authenticationRequest) throws InvalidCredentialsException, IdentityAccessException {
+
+        if (authenticationRequest == null) {
+            logger.info("Cannot authenticate null authenticationRequest, returning null.");
+            return null;
+        }
+
+        final Object credentials = authenticationRequest.getCredentials();
+        byte[] kerberosTicket = credentials != null && credentials instanceof byte[] ? (byte[]) authenticationRequest.getCredentials() : null;
+
+        if (credentials == null) {
+            logger.info("Kerberos Ticket not found in authenticationRequest credentials, returning null.");
+            return null;
+        }
+
+        if (kerberosServiceAuthenticationProvider == null) {
+            throw new IdentityAccessException("The Kerberos authentication provider is not initialized.");
+        }
+
+        try {
+            KerberosServiceRequestToken kerberosServiceRequestToken = new KerberosServiceRequestToken(kerberosTicket);
+            kerberosServiceRequestToken.setDetails(authenticationRequest.getDetails());
+            Authentication authentication = kerberosServiceAuthenticationProvider.authenticate(kerberosServiceRequestToken);
+            if (authentication == null) {
+                throw new InvalidCredentialsException("Kerberos credentials could not be authenticated.");
+            }
+
+            final String kerberosPrincipal = authentication.getName();
+
+            return new AuthenticationResponse(kerberosPrincipal, kerberosPrincipal, expiration, issuer);
+
+        } catch (AuthenticationException e) {
+            String authFailedMessage = "Kerberos credentials could not be authenticated.";
+
+            /* Kerberos uses encryption with up to AES-256, specifically AES256-CTS-HMAC-SHA1-96.
+             * That is not available in every JRE, particularly if Unlimited Strength Encryption
+             * policies are not installed in the Java home lib dir. The Kerberos lib does not
+             * differentiate between failures due to decryption and those due to bad credentials
+             * without walking the causes of the exception, so this check puts something
+             * potentially useful in the logs for those troubleshooting Kerberos authentication. */
+            if (!Boolean.FALSE.equals(CryptoUtils.isCryptoRestricted())) {
+                authFailedMessage += " This Java Runtime does not support unlimited strength encryption. " +
+                        "This could cause Kerberos authentication to fail as it can require AES-256.";
+            }
+
+            logger.info(authFailedMessage);
+            throw new InvalidCredentialsException(authFailedMessage, e);
+        }
+
+    }
+
+    @Override
+    public void onConfigured(IdentityProviderConfigurationContext configurationContext) throws SecurityProviderCreationException {
+        throw new SecurityProviderCreationException(KerberosSpnegoIdentityProvider.class.getSimpleName() +
+                " does not currently support being loaded via IdentityProviderFactory");
+    }
+
+    @Override
+    public void preDestruction() throws SecurityProviderDestructionException {
+    }
+
+    public boolean isValidKerberosHeader(String headerValue) {
+        return headerValue != null && (headerValue.startsWith(AUTHORIZATION_NEGOTIATE + " ") || headerValue.startsWith("Kerberos "));
+    }
+}

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/security/authentication/kerberos/KerberosTicketValidatorFactory.java
----------------------------------------------------------------------
diff --git a/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/security/authentication/kerberos/KerberosTicketValidatorFactory.java b/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/security/authentication/kerberos/KerberosTicketValidatorFactory.java
new file mode 100644
index 0000000..ed3e6eb
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/security/authentication/kerberos/KerberosTicketValidatorFactory.java
@@ -0,0 +1,69 @@
+/*
+ * 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.security.authentication.kerberos;
+
+import org.apache.nifi.registry.properties.NiFiRegistryProperties;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.core.io.FileSystemResource;
+import org.springframework.security.kerberos.authentication.KerberosTicketValidator;
+import org.springframework.security.kerberos.authentication.sun.GlobalSunJaasKerberosConfig;
+import org.springframework.security.kerberos.authentication.sun.SunJaasKerberosTicketValidator;
+
+import java.io.File;
+
+@Configuration
+public class KerberosTicketValidatorFactory {
+
+    private NiFiRegistryProperties properties;
+
+    private KerberosTicketValidator kerberosTicketValidator;
+
+    @Autowired
+    public KerberosTicketValidatorFactory(NiFiRegistryProperties properties) {
+        this.properties = properties;
+    }
+
+    @Bean
+    public KerberosTicketValidator kerberosTicketValidator() throws Exception {
+
+        if (kerberosTicketValidator == null && properties.isKerberosSpnegoSupportEnabled()) {
+
+            // Configure SunJaasKerberos (global)
+            final File krb5ConfigFile = properties.getKerberosConfigurationFile();
+            if (krb5ConfigFile != null) {
+                final GlobalSunJaasKerberosConfig krb5Config = new GlobalSunJaasKerberosConfig();
+                krb5Config.setKrbConfLocation(krb5ConfigFile.getAbsolutePath());
+                krb5Config.afterPropertiesSet();
+            }
+
+            // Create ticket validator to inject into KerberosServiceAuthenticationProvider
+            SunJaasKerberosTicketValidator ticketValidator = new SunJaasKerberosTicketValidator();
+            ticketValidator.setServicePrincipal(properties.getKerberosSpnegoPrincipal());
+            ticketValidator.setKeyTabLocation(new FileSystemResource(properties.getKerberosSpnegoKeytabLocation()));
+            ticketValidator.afterPropertiesSet();
+
+            kerberosTicketValidator = ticketValidator;
+
+        }
+
+        return kerberosTicketValidator;
+
+    }
+
+}

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/security/authentication/kerberos/KerberosUserDetailsService.java
----------------------------------------------------------------------
diff --git a/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/security/authentication/kerberos/KerberosUserDetailsService.java b/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/security/authentication/kerberos/KerberosUserDetailsService.java
new file mode 100644
index 0000000..5471906
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/security/authentication/kerberos/KerberosUserDetailsService.java
@@ -0,0 +1,38 @@
+/*
+ * 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.security.authentication.kerberos;
+
+import org.springframework.security.core.authority.AuthorityUtils;
+import org.springframework.security.core.userdetails.User;
+import org.springframework.security.core.userdetails.UserDetails;
+import org.springframework.security.core.userdetails.UserDetailsService;
+import org.springframework.security.core.userdetails.UsernameNotFoundException;
+
+public class KerberosUserDetailsService implements UserDetailsService {
+
+    @Override
+    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
+        return new User(
+                username,
+                "notUsed",
+                true,
+                true,
+                true,
+                true,
+                AuthorityUtils.createAuthorityList("ROLE_USER"));
+    }
+}

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/security/authentication/x509/SubjectDnX509PrincipalExtractor.java
----------------------------------------------------------------------
diff --git a/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/security/authentication/x509/SubjectDnX509PrincipalExtractor.java b/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/security/authentication/x509/SubjectDnX509PrincipalExtractor.java
new file mode 100644
index 0000000..a9deae1
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/security/authentication/x509/SubjectDnX509PrincipalExtractor.java
@@ -0,0 +1,35 @@
+/*
+ * 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.security.authentication.x509;
+
+import org.springframework.security.web.authentication.preauth.x509.X509PrincipalExtractor;
+import org.springframework.stereotype.Component;
+
+import java.security.cert.X509Certificate;
+
+/**
+ * Principal extractor for extracting a DN.
+ */
+@Component
+public class SubjectDnX509PrincipalExtractor implements X509PrincipalExtractor {
+
+    @Override
+    public Object extractPrincipal(X509Certificate cert) {
+        return cert.getSubjectDN().getName().trim();
+    }
+
+}

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/security/authentication/x509/X509CertificateExtractor.java
----------------------------------------------------------------------
diff --git a/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/security/authentication/x509/X509CertificateExtractor.java b/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/security/authentication/x509/X509CertificateExtractor.java
new file mode 100644
index 0000000..34ceada
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/security/authentication/x509/X509CertificateExtractor.java
@@ -0,0 +1,55 @@
+/*
+ * 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.security.authentication.x509;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.stereotype.Component;
+
+import javax.servlet.http.HttpServletRequest;
+import java.security.cert.X509Certificate;
+
+/**
+ * Extracts client certificates from Http requests.
+ */
+@Component
+public class X509CertificateExtractor {
+
+    private final Logger logger = LoggerFactory.getLogger(getClass());
+
+    /**
+     * Extract the client certificate from the specified HttpServletRequest or
+     * null if none is specified.
+     *
+     * @param request http request
+     * @return cert
+     */
+    public X509Certificate[] extractClientCertificate(HttpServletRequest request) {
+        X509Certificate[] certs = (X509Certificate[]) request.getAttribute("javax.servlet.request.X509Certificate");
+
+        if (certs != null && certs.length > 0) {
+            return certs;
+        }
+
+        if (logger.isDebugEnabled()) {
+            logger.debug("No client certificate found in request.");
+        }
+
+        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/security/authentication/x509/X509IdentityAuthenticationProvider.java
----------------------------------------------------------------------
diff --git a/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/security/authentication/x509/X509IdentityAuthenticationProvider.java b/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/security/authentication/x509/X509IdentityAuthenticationProvider.java
new file mode 100644
index 0000000..aefdd5b
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/security/authentication/x509/X509IdentityAuthenticationProvider.java
@@ -0,0 +1,131 @@
+/*
+ * 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.security.authentication.x509;
+
+import org.apache.commons.lang3.StringUtils;
+import org.apache.nifi.registry.properties.NiFiRegistryProperties;
+import org.apache.nifi.registry.security.authentication.AuthenticationRequest;
+import org.apache.nifi.registry.web.security.authentication.AuthenticationRequestToken;
+import org.apache.nifi.registry.security.authentication.AuthenticationResponse;
+import org.apache.nifi.registry.security.authentication.IdentityProvider;
+import org.apache.nifi.registry.web.security.authentication.IdentityAuthenticationProvider;
+import org.apache.nifi.registry.web.security.authentication.AuthenticationSuccessToken;
+import org.apache.nifi.registry.security.authorization.Authorizer;
+import org.apache.nifi.registry.security.authorization.RequestAction;
+import org.apache.nifi.registry.security.authorization.Resource;
+import org.apache.nifi.registry.security.authorization.exception.AccessDeniedException;
+import org.apache.nifi.registry.security.authorization.resource.Authorizable;
+import org.apache.nifi.registry.security.authorization.resource.ResourceFactory;
+import org.apache.nifi.registry.security.authorization.user.NiFiUser;
+import org.apache.nifi.registry.security.authorization.user.NiFiUserDetails;
+import org.apache.nifi.registry.security.authorization.user.StandardNiFiUser;
+import org.apache.nifi.registry.security.util.ProxiedEntitiesUtils;
+import org.apache.nifi.registry.web.security.authentication.exception.UntrustedProxyException;
+
+import java.util.List;
+import java.util.ListIterator;
+import java.util.Set;
+
+public class X509IdentityAuthenticationProvider extends IdentityAuthenticationProvider {
+
+    private static final Authorizable PROXY_AUTHORIZABLE = new Authorizable() {
+        @Override
+        public Authorizable getParentAuthorizable() {
+            return null;
+        }
+
+        @Override
+        public Resource getResource() {
+            return ResourceFactory.getProxyResource();
+        }
+    };
+
+    public X509IdentityAuthenticationProvider(NiFiRegistryProperties properties, Authorizer authorizer, IdentityProvider identityProvider) {
+        super(properties, authorizer, identityProvider);
+    }
+
+    @Override
+    protected AuthenticationSuccessToken buildAuthenticatedToken(
+            AuthenticationRequestToken requestToken,
+            AuthenticationResponse response) {
+
+        AuthenticationRequest authenticationRequest = requestToken.getAuthenticationRequest();
+
+        String proxiedEntitiesChain = authenticationRequest.getDetails() != null
+                ? (String)authenticationRequest.getDetails()
+                : null;
+
+        if (StringUtils.isBlank(proxiedEntitiesChain)) {
+            return super.buildAuthenticatedToken(requestToken, response);
+        }
+
+        // build the entire proxy chain if applicable - <end-user><proxy1><proxy2>
+        final List<String> proxyChain = ProxiedEntitiesUtils.tokenizeProxiedEntitiesChain(proxiedEntitiesChain);
+        proxyChain.add(response.getIdentity());
+
+        // add the chain as appropriate to each proxy
+        NiFiUser proxy = null;
+        for (final ListIterator<String> chainIter = proxyChain.listIterator(proxyChain.size()); chainIter.hasPrevious(); ) {
+            String identity = chainIter.previous();
+
+            // determine if the user is anonymous
+            final boolean isAnonymous = StringUtils.isBlank(identity);
+            if (isAnonymous) {
+                identity = StandardNiFiUser.ANONYMOUS_IDENTITY;
+            } else {
+                identity = mapIdentity(identity);
+            }
+
+            final Set<String> groups = getUserGroups(identity);
+
+            // Only set the client address for client making the request because we don't know the clientAddress of the proxied entities
+            String clientAddress = (proxy == null) ? requestToken.getClientAddress() : null;
+            proxy = createUser(identity, groups, proxy, clientAddress, isAnonymous);
+
+            if (chainIter.hasPrevious()) {
+                try {
+                    PROXY_AUTHORIZABLE.authorize(authorizer, RequestAction.WRITE, proxy);
+                } catch (final AccessDeniedException e) {
+                    throw new UntrustedProxyException(String.format("Untrusted proxy [%s].", identity));
+                }
+            }
+        }
+
+        return new AuthenticationSuccessToken(new NiFiUserDetails(proxy));
+
+    }
+
+    /**
+     * Returns a regular user populated with the provided values, or if the user should be anonymous, a well-formed instance of the anonymous user with the provided values.
+     *
+     * @param identity      the user's identity
+     * @param chain         the proxied entities
+     * @param clientAddress the requesting IP address
+     * @param isAnonymous   if true, an anonymous user will be returned (identity will be ignored)
+     * @return the populated user
+     */
+    private static NiFiUser createUser(String identity, Set<String> groups, NiFiUser chain, String clientAddress, boolean isAnonymous) {
+        if (isAnonymous) {
+            return StandardNiFiUser.populateAnonymousUser(chain, clientAddress);
+        } else {
+            return new StandardNiFiUser.Builder().identity(identity).groups(groups).chain(chain).clientAddress(clientAddress).build();
+        }
+    }
+
+
+
+}

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/security/authentication/x509/X509IdentityProvider.java
----------------------------------------------------------------------
diff --git a/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/security/authentication/x509/X509IdentityProvider.java b/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/security/authentication/x509/X509IdentityProvider.java
new file mode 100644
index 0000000..2a1856e
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/security/authentication/x509/X509IdentityProvider.java
@@ -0,0 +1,174 @@
+/*
+ * 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.security.authentication.x509;
+
+import org.apache.nifi.registry.security.authentication.AuthenticationRequest;
+import org.apache.nifi.registry.security.authentication.AuthenticationResponse;
+import org.apache.nifi.registry.security.authentication.IdentityProvider;
+import org.apache.nifi.registry.security.authentication.IdentityProviderConfigurationContext;
+import org.apache.nifi.registry.security.authentication.IdentityProviderUsage;
+import org.apache.nifi.registry.security.authentication.exception.InvalidCredentialsException;
+import org.apache.nifi.registry.security.exception.SecurityProviderCreationException;
+import org.apache.nifi.registry.security.exception.SecurityProviderDestructionException;
+import org.apache.nifi.registry.security.util.ProxiedEntitiesUtils;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.security.web.authentication.preauth.x509.X509PrincipalExtractor;
+import org.springframework.stereotype.Component;
+
+import javax.servlet.http.HttpServletRequest;
+import java.security.cert.CertificateExpiredException;
+import java.security.cert.CertificateNotYetValidException;
+import java.security.cert.X509Certificate;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * Identity provider for extract the authenticating a ServletRequest with a X509Certificate.
+ */
+@Component
+public class X509IdentityProvider implements IdentityProvider {
+
+    private static final Logger logger = LoggerFactory.getLogger(X509IdentityProvider.class);
+
+    private static final String issuer = X509IdentityProvider.class.getSimpleName();
+
+    private static final long expiration = TimeUnit.MILLISECONDS.convert(12, TimeUnit.HOURS);
+
+    private static final IdentityProviderUsage usage = new IdentityProviderUsage() {
+        @Override
+        public String getText() {
+            return "The client must connect over HTTPS and must provide a client certificate during the TLS handshake. " +
+                    "Additionally, the client may declare itself a proxy for another user identity by populating the " +
+                    ProxiedEntitiesUtils.PROXY_ENTITIES_CHAIN + " HTTP header field with a value of the format " +
+                    "'<end-user-identity><proxy1-identity><proxy2-identity>...<proxyN-identity>'" +
+                    "for all identities in the chain prior to this client. If the " + ProxiedEntitiesUtils.PROXY_ENTITIES_CHAIN +
+                    " header is present in the request, this client's identity will be extracted from the client certificate " +
+                    "used for TLS and added to the end of the chain, and then the entire chain will be authorized. Each proxy " +
+                    "will be authorized to have 'write' access to '/proxy', and the originating user identity will be " +
+                    "authorized for access to the resource being accessed in the request.";
+        }
+
+        @Override
+        public AuthType getAuthType() {
+            return AuthType.OTHER.httpAuthScheme("TLS-client-cert");
+        }
+    };
+
+    private X509PrincipalExtractor principalExtractor;
+    private X509CertificateExtractor certificateExtractor;
+
+    @Autowired
+    public X509IdentityProvider(X509PrincipalExtractor principalExtractor, X509CertificateExtractor certificateExtractor) {
+        this.principalExtractor = principalExtractor;
+        this.certificateExtractor = certificateExtractor;
+    }
+
+    @Override
+    public IdentityProviderUsage getUsageInstructions() {
+        return usage;
+    }
+
+    /**
+     * Extracts certificate-based credentials from an {@link HttpServletRequest}.
+     *
+     * The resulting {@link AuthenticationRequest} will be populated as:
+     *  - username: principal DN from first client cert
+     *  - credentials: first client certificate (X509Certificate)
+     *  - details: proxied-entities chain (String)
+     *
+     * @param servletRequest the {@link HttpServletRequest} request that may contain credentials understood by this IdentityProvider
+     * @return a populated AuthenticationRequest or null if the credentials could not be found.
+     */
+    @Override
+    public AuthenticationRequest extractCredentials(HttpServletRequest servletRequest) {
+
+        // only support x509 login when running securely
+        if (!servletRequest.isSecure()) {
+            return null;
+        }
+
+        // look for a client certificate
+        final X509Certificate[] certificates = certificateExtractor.extractClientCertificate(servletRequest);
+        if (certificates == null || certificates.length == 0) {
+            return null;
+        }
+
+        // extract the principal
+        final Object certificatePrincipal = principalExtractor.extractPrincipal(certificates[0]);
+        final String principal = certificatePrincipal.toString();
+
+        // extract the proxiedEntitiesChain header value from the servletRequest
+        String proxiedEntitiesChainHeader = servletRequest.getHeader(ProxiedEntitiesUtils.PROXY_ENTITIES_CHAIN);
+
+        return new AuthenticationRequest(principal, certificates[0], proxiedEntitiesChainHeader);
+
+    }
+
+    /**
+     * For a given {@link AuthenticationRequest}, this validates the client certificate and creates a populated {@link AuthenticationResponse}.
+     *
+     * The {@link AuthenticationRequest} authenticationRequest paramenter is expected to be populated as:
+     *  - username: principal DN from first client cert
+     *  - credentials: first client certificate (X509Certificate)
+     *  - details: proxied-entities chain (String)
+     *
+     * @param authenticationRequest the request, containing identity claim credentials for the IdentityProvider to authenticate and determine an identity
+     */
+    @Override
+    public AuthenticationResponse authenticate(AuthenticationRequest authenticationRequest) throws InvalidCredentialsException {
+
+        if (authenticationRequest == null || authenticationRequest.getUsername() == null) {
+            return null;
+        }
+
+        String principal = authenticationRequest.getUsername();
+
+        try {
+            X509Certificate clientCertificate = (X509Certificate)authenticationRequest.getCredentials();
+            validateClientCertificate(clientCertificate);
+        } catch (CertificateExpiredException cee) {
+            final String message = String.format("Client certificate for (%s) is expired.", principal);
+            logger.warn(message, cee);
+            throw new InvalidCredentialsException(message, cee);
+        } catch (CertificateNotYetValidException cnyve) {
+            final String message = String.format("Client certificate for (%s) is not yet valid.", principal);
+            logger.warn(message, cnyve);
+            throw new InvalidCredentialsException(message, cnyve);
+        } catch (final Exception e) {
+            logger.warn(e.getMessage(), e);
+        }
+
+        // build the authentication response
+        return new AuthenticationResponse(principal, principal, expiration, issuer);
+    }
+
+    @Override
+    public void onConfigured(IdentityProviderConfigurationContext configurationContext) throws SecurityProviderCreationException {
+        throw new SecurityProviderCreationException(X509IdentityProvider.class.getSimpleName() +
+                " does not currently support being loaded via IdentityProviderFactory");
+    }
+
+    @Override
+    public void preDestruction() throws SecurityProviderDestructionException {}
+
+
+    private void validateClientCertificate(X509Certificate certificate) throws CertificateExpiredException, CertificateNotYetValidException {
+        certificate.checkValidity();
+    }
+
+}

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/security/authorization/HttpMethodAuthorizationRules.java
----------------------------------------------------------------------
diff --git a/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/security/authorization/HttpMethodAuthorizationRules.java b/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/security/authorization/HttpMethodAuthorizationRules.java
new file mode 100644
index 0000000..c940359
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/security/authorization/HttpMethodAuthorizationRules.java
@@ -0,0 +1,48 @@
+/*
+ * 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.security.authorization;
+
+import org.apache.nifi.registry.security.authorization.RequestAction;
+import org.springframework.http.HttpMethod;
+
+public interface HttpMethodAuthorizationRules {
+
+    default boolean requiresAuthorization(HttpMethod httpMethod) {
+        return true;
+    }
+
+    default RequestAction mapHttpMethodToAction(HttpMethod httpMethod) {
+
+        switch (httpMethod) {
+            case TRACE:
+            case OPTIONS:
+            case HEAD:
+            case GET:
+                return RequestAction.READ;
+            case POST:
+            case PUT:
+            case PATCH:
+                return RequestAction.WRITE;
+            case DELETE:
+                return RequestAction.DELETE;
+            default:
+                throw new IllegalArgumentException("Unknown http method: " + httpMethod);
+        }
+
+    }
+
+}

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/security/authorization/ResourceAuthorizationFilter.java
----------------------------------------------------------------------
diff --git a/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/security/authorization/ResourceAuthorizationFilter.java b/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/security/authorization/ResourceAuthorizationFilter.java
new file mode 100644
index 0000000..6e551e1
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/security/authorization/ResourceAuthorizationFilter.java
@@ -0,0 +1,218 @@
+/*
+ * 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.security.authorization;
+
+import org.apache.nifi.registry.security.authorization.AuthorizableLookup;
+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.security.authorization.resource.ResourceType;
+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.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.http.HttpMethod;
+import org.springframework.web.filter.GenericFilterBean;
+
+import javax.servlet.FilterChain;
+import javax.servlet.ServletException;
+import javax.servlet.ServletRequest;
+import javax.servlet.ServletResponse;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+import java.io.IOException;
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * This filter is designed to perform a resource authorization check in the Spring Security filter chain.
+ *
+ * It authorizes the current authenticated user for the {@link RequestAction} (based on the HttpMethod) requested
+ * on the {@link ResourceType} (based on the URI path).
+ *
+ * This filter is designed to be place after any authentication and before any application endpoints.
+ *
+ * This filter can be used in place of or in addition to authorization checks that occur in the application
+ * downstream of this filter.
+ *
+ * To configure this filter, provide an {@link AuthorizationService} that will be used to perform the authorization
+ * check, as well as a set of rules that control which resource and HTTP methods are handled by this filter.
+ *
+ * Any (ResourceType, HttpMethod) pair that is not configured to require authorization by this filter will be
+ * allowed to proceed in the filter chain without an authorization check.
+ *
+ * Any (ResourceType, HttpMethod) pair that is configured to require authorization by this filter will map
+ * the HttpMethod to a NiFi Registry RequestAction (configurable when creating this filter), and the
+ * (Resource Authorizable, RequestAction) pair will be sent to the AuthorizationService, which will use the
+ * configured Authorizer to authorize the current user for the action on the requested resource.
+ */
+public class ResourceAuthorizationFilter extends GenericFilterBean {
+
+    private static final Logger logger = LoggerFactory.getLogger(ResourceAuthorizationFilter.class);
+
+    private Map<ResourceType, HttpMethodAuthorizationRules> resourceTypeAuthorizationRules;
+    private AuthorizationService authorizationService;
+    private AuthorizableLookup authorizableLookup;
+
+    ResourceAuthorizationFilter(Builder builder) {
+        if (builder.getAuthorizationService() == null || builder.getResourceTypeAuthorizationRules() == null) {
+            throw new IllegalArgumentException("Builder is missing one or more required fields [authorizationService, resourceTypeAuthorizationRules].");
+        }
+        this.resourceTypeAuthorizationRules = builder.getResourceTypeAuthorizationRules();
+        this.authorizationService = builder.getAuthorizationService();
+        this.authorizableLookup = this.authorizationService.getAuthorizableLookup();
+    }
+
+    @Override
+    public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
+
+        HttpServletRequest httpServletRequest = (HttpServletRequest) servletRequest;
+        HttpServletResponse httpServletResponse = (HttpServletResponse) servletResponse;
+
+        boolean authorizationCheckIsRequired = false;
+        String resourcePath = null;
+        RequestAction action = null;
+
+        // Only require authorization if the NiFi Registry is running securely.
+        if (servletRequest.isSecure()) {
+
+            // Only require authorization for resources for which this filter has been configured
+            resourcePath = httpServletRequest.getServletPath();
+            if (resourcePath != null) {
+                final ResourceType resourceType = ResourceType.mapFullResourcePathToResourceType(resourcePath);
+                final HttpMethodAuthorizationRules authorizationRules = resourceTypeAuthorizationRules.get(resourceType);
+                if (authorizationRules != null) {
+                    final String httpMethodStr = httpServletRequest.getMethod().toUpperCase();
+                    HttpMethod httpMethod = HttpMethod.resolve(httpMethodStr);
+
+                    // Only require authorization for HTTP methods included in this resource type's rule set
+                    if (httpMethod != null && authorizationRules.requiresAuthorization(httpMethod)) {
+                        authorizationCheckIsRequired = true;
+                        action = authorizationRules.mapHttpMethodToAction(httpMethod);
+                    }
+                }
+            }
+        }
+
+        if (!authorizationCheckIsRequired) {
+            forwardRequestWithoutAuthorizationCheck(httpServletRequest, httpServletResponse, filterChain);
+            return;
+        }
+
+        // Perform authorization check
+        try {
+            authorizeAccess(resourcePath, action);
+            successfulAuthorization(httpServletRequest, httpServletResponse, filterChain);
+        } catch (Exception e) {
+            logger.debug("Exception occurred while performing authorization check.", e);
+            failedAuthorization(httpServletRequest, httpServletResponse, filterChain, e);
+        }
+    }
+
+    private boolean userIsAuthenticated() {
+        NiFiUser user = NiFiUserUtils.getNiFiUser();
+        return (user != null && !user.isAnonymous());
+    }
+
+    private void authorizeAccess(String path, RequestAction action) throws AccessDeniedException {
+
+        if (path == null || action == null) {
+            throw new IllegalArgumentException("Authorization is required, but a required input [resource, action] is absent.");
+        }
+
+        Authorizable authorizable = authorizableLookup.getAuthorizableByResource(path);
+
+        if (authorizable == null) {
+            throw new IllegalStateException("Resource Authorization Filter configured for non-authorizable resource: " + path);
+        }
+
+        // throws AccessDeniedException if current user is not authorized to perform requested action on resource
+        authorizationService.authorize(authorizable, action);
+    }
+
+    private void forwardRequestWithoutAuthorizationCheck(HttpServletRequest req, HttpServletResponse res, FilterChain chain) throws IOException, ServletException {
+        logger.debug("Request filter authorization check is not required for this HTTP Method on this resource. " +
+                "Allowing request to proceed. An additional authorization check might be performed downstream of this filter.");
+        chain.doFilter(req, res);
+    }
+
+    private void successfulAuthorization(HttpServletRequest req, HttpServletResponse res, FilterChain chain) throws IOException, ServletException {
+        logger.debug("Request filter authorization check passed. Allowing request to proceed.");
+        chain.doFilter(req, res);
+    }
+
+    private void failedAuthorization(HttpServletRequest request, HttpServletResponse response, FilterChain chain, Exception failure) throws IOException, ServletException {
+        logger.debug("Request filter authorization check failed. Blocking access.");
+
+        NiFiUser user = NiFiUserUtils.getNiFiUser();
+        final String identity = (user != null) ? user.toString() : "<no user found>";
+        final int status = !userIsAuthenticated() ? HttpServletResponse.SC_UNAUTHORIZED : HttpServletResponse.SC_FORBIDDEN;
+
+        logger.info("{} does not have permission to perform this action on the requested resource. {} Returning {} response.", identity, failure.getMessage(), status);
+        logger.debug("", failure);
+
+        if (!response.isCommitted()) {
+            response.setStatus(status);
+            response.setContentType("text/plain");
+            response.getWriter().println(String.format("Access is denied due to: %s Contact the system administrator.", failure.getLocalizedMessage()));
+        }
+    }
+
+    public static Builder builder() {
+        return new Builder();
+    }
+
+    public static class Builder {
+
+        private AuthorizationService authorizationService;
+        final private Map<ResourceType, HttpMethodAuthorizationRules> resourceTypeAuthorizationRules;
+
+        // create via ResourceAuthorizationFilter.builder()
+        private Builder() {
+            this.resourceTypeAuthorizationRules = new HashMap<>();
+        }
+
+        public AuthorizationService getAuthorizationService() {
+            return authorizationService;
+        }
+
+        public Builder setAuthorizationService(AuthorizationService authorizationService) {
+            this.authorizationService = authorizationService;
+            return this;
+        }
+
+        public Map<ResourceType, HttpMethodAuthorizationRules> getResourceTypeAuthorizationRules() {
+            return resourceTypeAuthorizationRules;
+        }
+
+        public Builder addResourceType(ResourceType resourceType) {
+            this.resourceTypeAuthorizationRules.put(resourceType, new HttpMethodAuthorizationRules() {});
+            return this;
+        }
+
+        public Builder addResourceType(ResourceType resourceType, HttpMethodAuthorizationRules authorizationRules) {
+            this.resourceTypeAuthorizationRules.put(resourceType, authorizationRules);
+            return this;
+        }
+
+        public ResourceAuthorizationFilter build() {
+            return new ResourceAuthorizationFilter(this);
+        }
+    }
+
+}

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/security/authorization/StandardHttpMethodAuthorizationRules.java
----------------------------------------------------------------------
diff --git a/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/security/authorization/StandardHttpMethodAuthorizationRules.java b/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/security/authorization/StandardHttpMethodAuthorizationRules.java
new file mode 100644
index 0000000..daa5a37
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/security/authorization/StandardHttpMethodAuthorizationRules.java
@@ -0,0 +1,40 @@
+/*
+ * 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.security.authorization;
+
+import org.springframework.http.HttpMethod;
+
+import java.util.EnumSet;
+import java.util.Set;
+
+public class StandardHttpMethodAuthorizationRules implements HttpMethodAuthorizationRules {
+
+    final private Set<HttpMethod> methodsRequiringAuthorization;
+
+    public StandardHttpMethodAuthorizationRules() {
+        this(EnumSet.allOf(HttpMethod.class));
+    }
+
+    public StandardHttpMethodAuthorizationRules(Set<HttpMethod> methodsRequiringAuthorization) {
+        this.methodsRequiringAuthorization = methodsRequiringAuthorization;
+    }
+
+    @Override
+    public boolean requiresAuthorization(HttpMethod httpMethod) {
+        return methodsRequiringAuthorization.contains(httpMethod);
+    }
+}

http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/6f26290d/nifi-registry-core/nifi-registry-web-api/src/main/resources/META-INF/LICENSE
----------------------------------------------------------------------
diff --git a/nifi-registry-core/nifi-registry-web-api/src/main/resources/META-INF/LICENSE b/nifi-registry-core/nifi-registry-web-api/src/main/resources/META-INF/LICENSE
new file mode 100644
index 0000000..cd25b0a
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-web-api/src/main/resources/META-INF/LICENSE
@@ -0,0 +1,352 @@
+
+                                 Apache License
+                           Version 2.0, January 2004
+                        http://www.apache.org/licenses/
+
+   TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+   1. Definitions.
+
+      "License" shall mean the terms and conditions for use, reproduction,
+      and distribution as defined by Sections 1 through 9 of this document.
+
+      "Licensor" shall mean the copyright owner or entity authorized by
+      the copyright owner that is granting the License.
+
+      "Legal Entity" shall mean the union of the acting entity and all
+      other entities that control, are controlled by, or are under common
+      control with that entity. For the purposes of this definition,
+      "control" means (i) the power, direct or indirect, to cause the
+      direction or management of such entity, whether by contract or
+      otherwise, or (ii) ownership of fifty percent (50%) or more of the
+      outstanding shares, or (iii) beneficial ownership of such entity.
+
+      "You" (or "Your") shall mean an individual or Legal Entity
+      exercising permissions granted by this License.
+
+      "Source" form shall mean the preferred form for making modifications,
+      including but not limited to software source code, documentation
+      source, and configuration files.
+
+      "Object" form shall mean any form resulting from mechanical
+      transformation or translation of a Source form, including but
+      not limited to compiled object code, generated documentation,
+      and conversions to other media types.
+
+      "Work" shall mean the work of authorship, whether in Source or
+      Object form, made available under the License, as indicated by a
+      copyright notice that is included in or attached to the work
+      (an example is provided in the Appendix below).
+
+      "Derivative Works" shall mean any work, whether in Source or Object
+      form, that is based on (or derived from) the Work and for which the
+      editorial revisions, annotations, elaborations, or other modifications
+      represent, as a whole, an original work of authorship. For the purposes
+      of this License, Derivative Works shall not include works that remain
+      separable from, or merely link (or bind by name) to the interfaces of,
+      the Work and Derivative Works thereof.
+
+      "Contribution" shall mean any work of authorship, including
+      the original version of the Work and any modifications or additions
+      to that Work or Derivative Works thereof, that is intentionally
+      submitted to Licensor for inclusion in the Work by the copyright owner
+      or by an individual or Legal Entity authorized to submit on behalf of
+      the copyright owner. For the purposes of this definition, "submitted"
+      means any form of electronic, verbal, or written communication sent
+      to the Licensor or its representatives, including but not limited to
+      communication on electronic mailing lists, source code control systems,
+      and issue tracking systems that are managed by, or on behalf of, the
+      Licensor for the purpose of discussing and improving the Work, but
+      excluding communication that is conspicuously marked or otherwise
+      designated in writing by the copyright owner as "Not a Contribution."
+
+      "Contributor" shall mean Licensor and any individual or Legal Entity
+      on behalf of whom a Contribution has been received by Licensor and
+      subsequently incorporated within the Work.
+
+   2. Grant of Copyright License. Subject to the terms and conditions of
+      this License, each Contributor hereby grants to You a perpetual,
+      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+      copyright license to reproduce, prepare Derivative Works of,
+      publicly display, publicly perform, sublicense, and distribute the
+      Work and such Derivative Works in Source or Object form.
+
+   3. Grant of Patent License. Subject to the terms and conditions of
+      this License, each Contributor hereby grants to You a perpetual,
+      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+      (except as stated in this section) patent license to make, have made,
+      use, offer to sell, sell, import, and otherwise transfer the Work,
+      where such license applies only to those patent claims licensable
+      by such Contributor that are necessarily infringed by their
+      Contribution(s) alone or by combination of their Contribution(s)
+      with the Work to which such Contribution(s) was submitted. If You
+      institute patent litigation against any entity (including a
+      cross-claim or counterclaim in a lawsuit) alleging that the Work
+      or a Contribution incorporated within the Work constitutes direct
+      or contributory patent infringement, then any patent licenses
+      granted to You under this License for that Work shall terminate
+      as of the date such litigation is filed.
+
+   4. Redistribution. You may reproduce and distribute copies of the
+      Work or Derivative Works thereof in any medium, with or without
+      modifications, and in Source or Object form, provided that You
+      meet the following conditions:
+
+      (a) You must give any other recipients of the Work or
+          Derivative Works a copy of this License; and
+
+      (b) You must cause any modified files to carry prominent notices
+          stating that You changed the files; and
+
+      (c) You must retain, in the Source form of any Derivative Works
+          that You distribute, all copyright, patent, trademark, and
+          attribution notices from the Source form of the Work,
+          excluding those notices that do not pertain to any part of
+          the Derivative Works; and
+
+      (d) If the Work includes a "NOTICE" text file as part of its
+          distribution, then any Derivative Works that You distribute must
+          include a readable copy of the attribution notices contained
+          within such NOTICE file, excluding those notices that do not
+          pertain to any part of the Derivative Works, in at least one
+          of the following places: within a NOTICE text file distributed
+          as part of the Derivative Works; within the Source form or
+          documentation, if provided along with the Derivative Works; or,
+          within a display generated by the Derivative Works, if and
+          wherever such third-party notices normally appear. The contents
+          of the NOTICE file are for informational purposes only and
+          do not modify the License. You may add Your own attribution
+          notices within Derivative Works that You distribute, alongside
+          or as an addendum to the NOTICE text from the Work, provided
+          that such additional attribution notices cannot be construed
+          as modifying the License.
+
+      You may add Your own copyright statement to Your modifications and
+      may provide additional or different license terms and conditions
+      for use, reproduction, or distribution of Your modifications, or
+      for any such Derivative Works as a whole, provided Your use,
+      reproduction, and distribution of the Work otherwise complies with
+      the conditions stated in this License.
+
+   5. Submission of Contributions. Unless You explicitly state otherwise,
+      any Contribution intentionally submitted for inclusion in the Work
+      by You to the Licensor shall be under the terms and conditions of
+      this License, without any additional terms or conditions.
+      Notwithstanding the above, nothing herein shall supersede or modify
+      the terms of any separate license agreement you may have executed
+      with Licensor regarding such Contributions.
+
+   6. Trademarks. This License does not grant permission to use the trade
+      names, trademarks, service marks, or product names of the Licensor,
+      except as required for reasonable and customary use in describing the
+      origin of the Work and reproducing the content of the NOTICE file.
+
+   7. Disclaimer of Warranty. Unless required by applicable law or
+      agreed to in writing, Licensor provides the Work (and each
+      Contributor provides its Contributions) on an "AS IS" BASIS,
+      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+      implied, including, without limitation, any warranties or conditions
+      of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+      PARTICULAR PURPOSE. You are solely responsible for determining the
+      appropriateness of using or redistributing the Work and assume any
+      risks associated with Your exercise of permissions under this License.
+
+   8. Limitation of Liability. In no event and under no legal theory,
+      whether in tort (including negligence), contract, or otherwise,
+      unless required by applicable law (such as deliberate and grossly
+      negligent acts) or agreed to in writing, shall any Contributor be
+      liable to You for damages, including any direct, indirect, special,
+      incidental, or consequential damages of any character arising as a
+      result of this License or out of the use or inability to use the
+      Work (including but not limited to damages for loss of goodwill,
+      work stoppage, computer failure or malfunction, or any and all
+      other commercial damages or losses), even if such Contributor
+      has been advised of the possibility of such damages.
+
+   9. Accepting Warranty or Additional Liability. While redistributing
+      the Work or Derivative Works thereof, You may choose to offer,
+      and charge a fee for, acceptance of support, warranty, indemnity,
+      or other liability obligations and/or rights consistent with this
+      License. However, in accepting such obligations, You may act only
+      on Your own behalf and on Your sole responsibility, not on behalf
+      of any other Contributor, and only if You agree to indemnify,
+      defend, and hold each Contributor harmless for any liability
+      incurred by, or claims asserted against, such Contributor by reason
+      of your accepting any such warranty or additional liability.
+
+   END OF TERMS AND CONDITIONS
+
+   APPENDIX: How to apply the Apache License to your work.
+
+      To apply the Apache License to your work, attach the following
+      boilerplate notice, with the fields enclosed by brackets "[]"
+      replaced with your own identifying information. (Don't include
+      the brackets!)  The text should be enclosed in the appropriate
+      comment syntax for the file format. We also recommend that a
+      file or class name and description of purpose be included on the
+      same "printed page" as the copyright notice for easier
+      identification within third-party archives.
+
+   Copyright [yyyy] [name of copyright owner]
+
+   Licensed under the Apache License, Version 2.0 (the "License");
+   you may not use this file except in compliance with the License.
+   You may obtain a copy of the License at
+
+       http://www.apache.org/licenses/LICENSE-2.0
+
+   Unless required by applicable law or agreed to in writing, software
+   distributed under the License is distributed on an "AS IS" BASIS,
+   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+   See the License for the specific language governing permissions and
+   limitations under the License.
+
+This product bundles 'asm' which is available under a 3-Clause BSD style license.
+For details see http://asm.ow2.org/asmdex-license.html
+
+    Copyright (c) 2012 France Télécom
+    All rights reserved.
+
+    Redistribution and use in source and binary forms, with or without
+    modification, are permitted provided that the following conditions
+    are met:
+    1. Redistributions of source code must retain the above copyright
+       notice, this list of conditions and the following disclaimer.
+    2. Redistributions in binary form must reproduce the above copyright
+       notice, this list of conditions and the following disclaimer in the
+       documentation and/or other materials provided with the distribution.
+    3. Neither the name of the copyright holders nor the names of its
+       contributors may be used to endorse or promote products derived from
+       this software without specific prior written permission.
+
+    THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+    AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+    IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+    ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
+    LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+    CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+    SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+    INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
+    CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+    ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
+    THE POSSIBILITY OF SUCH DAMAGE.
+
+The binary distribution of this product bundles 'Antlr 3' which is available
+under a "3-clause BSD" license.  For details see http://www.antlr3.org/license.html
+
+    Copyright (c) 2010 Terence Parr
+    All rights reserved.
+
+    Redistribution and use in source and binary forms, with or without
+    modification, are permitted provided that the following conditions are met:
+
+    Redistributions of source code must retain the above copyright notice, this
+    list of conditions and the following disclaimer.
+    Redistributions in binary form must reproduce the above copyright notice,
+    this list of conditions and the following disclaimer in the documentation
+    and/or other materials provided with the distribution.
+    Neither the name of the author nor the names of its contributors may be used
+    to endorse or promote products derived from this software without specific
+    prior written permission.
+    THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+    AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+    IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+    ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
+    LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+    CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+    SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+    INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
+    CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+    ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
+    THE POSSIBILITY OF SUCH DAMAGE.
+
+The binary distribution of this product bundles 'Bouncy Castle JDK 1.5'
+under an MIT style license.
+
+    Copyright (c) 2000 - 2015 The Legion of the Bouncy Castle Inc. (http://www.bouncycastle.org)
+
+    Permission is hereby granted, free of charge, to any person obtaining a copy
+    of this software and associated documentation files (the "Software"), to deal
+    in the Software without restriction, including without limitation the rights
+    to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+    copies of the Software, and to permit persons to whom the Software is
+    furnished to do so, subject to the following conditions:
+
+    The above copyright notice and this permission notice shall be included in
+    all copies or substantial portions of the Software.
+
+    THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+    IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+    FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+    AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+    LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+    OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+    THE SOFTWARE.
+
+The binary distribution of this product bundles 'Slf4j' which is available under
+an MIT license.
+
+    Copyright (c) 2004-2013 QOS.ch
+     All rights reserved.
+
+     Permission is hereby granted, free  of charge, to any person obtaining
+     a  copy  of this  software  and  associated  documentation files  (the
+     "Software"), to  deal in  the Software without  restriction, including
+     without limitation  the rights to  use, copy, modify,  merge, publish,
+     distribute,  sublicense, and/or sell  copies of  the Software,  and to
+     permit persons to whom the Software  is furnished to do so, subject to
+     the following conditions:
+
+     The  above  copyright  notice  and  this permission  notice  shall  be
+     included in all copies or substantial portions of the Software.
+
+     THE  SOFTWARE IS  PROVIDED  "AS  IS", WITHOUT  WARRANTY  OF ANY  KIND,
+     EXPRESS OR  IMPLIED, INCLUDING  BUT NOT LIMITED  TO THE  WARRANTIES OF
+     MERCHANTABILITY,    FITNESS    FOR    A   PARTICULAR    PURPOSE    AND
+     NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
+     LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+     OF CONTRACT, TORT OR OTHERWISE,  ARISING FROM, OUT OF OR IN CONNECTION
+     WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+
+The binary distribution of this product bundles 'dom4j' which is available under
+a "3-Clause BSD" license. For details: https://github.com/dom4j/dom4j/blob/master/LICENSE
+
+    Copyright 2001-2016 (C) MetaStuff, Ltd. and DOM4J contributors. All Rights Reserved.
+
+    Redistribution and use of this software and associated documentation
+    ("Software"), with or without modification, are permitted provided
+    that the following conditions are met:
+
+    1. Redistributions of source code must retain copyright
+       statements and notices.  Redistributions must also contain a
+       copy of this document.
+
+    2. Redistributions in binary form must reproduce the
+       above copyright notice, this list of conditions and the
+       following disclaimer in the documentation and/or other
+       materials provided with the distribution.
+
+    3. The name "DOM4J" must not be used to endorse or promote
+       products derived from this Software without prior written
+       permission of MetaStuff, Ltd.  For written permission,
+       please contact dom4j-info@metastuff.com.
+
+    4. Products derived from this Software may not be called "DOM4J"
+       nor may "DOM4J" appear in their names without prior written
+       permission of MetaStuff, Ltd. DOM4J is a registered
+       trademark of MetaStuff, Ltd.
+
+    5. Due credit should be given to the DOM4J Project - https://dom4j.github.io/
+
+    THIS SOFTWARE IS PROVIDED BY METASTUFF, LTD. AND CONTRIBUTORS
+    ``AS IS'' AND ANY EXPRESSED OR IMPLIED WARRANTIES, INCLUDING, BUT
+    NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND
+    FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.  IN NO EVENT SHALL
+    METASTUFF, LTD. OR ITS CONTRIBUTORS BE LIABLE FOR ANY DIRECT,
+    INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+    (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+    SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
+    HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
+    STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+    ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED
+    OF THE POSSIBILITY OF SUCH DAMAGE.
\ No newline at end of file


[05/51] [partial] nifi-registry git commit: NIFIREG-201 Refactoring project structure to better isolate extensions

Posted by kd...@apache.org.
http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/6f26290d/nifi-registry-core/nifi-registry-web-ui/src/main/frontend/package.json
----------------------------------------------------------------------
diff --git a/nifi-registry-core/nifi-registry-web-ui/src/main/frontend/package.json b/nifi-registry-core/nifi-registry-web-ui/src/main/frontend/package.json
new file mode 100644
index 0000000..5129eb4
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-web-ui/src/main/frontend/package.json
@@ -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.",
+  "name": "nifi-registry",
+  "version": "0.0.1",
+  "description": "",
+  "scripts": {
+    "test": "./node_modules/protractor/bin/webdriver-manager update --gecko false && karma start karma.conf.js --single-run",
+    "test:dev": "./node_modules/protractor/bin/webdriver-manager update --gecko false && karma start karma.conf.js"
+  },
+  "keywords": [],
+  "author": "",
+  "repository": {
+    "type": "git",
+    "url": "https://github.com/apache/nifi-registry"
+  },
+  "dependencies": {
+    "querystring": "0.2.0",
+    "reset-css": "4.0.1",
+    "rxjs": "5.5.6",
+    "superagent": "3.8.3",
+    "systemjs": "0.21.3",
+    "systemjs-plugin-text": "0.0.11",
+    "zone.js": "0.8.26",
+    "tslib": "1.8.0",
+    "material-design-icons": "3.0.1",
+    "jquery": "3.3.1",
+    "@angular/animations": "5.2.0",
+    "@angular/cdk": "5.2.0",
+    "@angular/common": "5.2.0",
+    "@angular/compiler": "5.2.0",
+    "@angular/core": "5.2.0",
+    "@angular/flex-layout": "5.0.0-beta.14",
+    "@angular/forms": "5.2.0",
+    "@angular/http": "5.2.0",
+    "@angular/material": "5.2.0",
+    "@angular/platform-browser": "5.2.0",
+    "@angular/platform-browser-dynamic": "5.2.0",
+    "@angular/router": "5.2.0",
+    "angular2-jwt": "0.2.3",
+    "@covalent/core": "1.0.0",
+    "@nifi-fds/core": "0.1.0",
+    "angular2-moment": "1.9.0",
+    "font-awesome": "4.7.0",
+    "moment": "2.22.1",
+    "hammerjs": "2.0.8",
+    "roboto-fontface": "0.9.0"
+  },
+  "devDependencies": {
+    "grunt": "1.0.3",
+    "grunt-cli": "1.3.0",
+    "grunt-contrib-compress": "1.4.3",
+    "grunt-sass": "3.0.1",
+    "node-sass": "4.9.3",
+    "grunt-systemjs-builder": "1.0.0",
+    "jasmine-core": "3.2.1",
+    "karma": "3.0.0",
+    "karma-chrome-launcher": "2.2.0",
+    "karma-cli": "1.0.1",
+    "karma-coverage": "1.1.2",
+    "karma-jasmine": "1.1.2",
+    "karma-jasmine-html-reporter": "1.3.0",
+    "karma-spec-reporter": "0.0.32",
+    "load-grunt-tasks": "4.0.0",
+    "protractor": "5.4.0"
+  },
+  "bundleDependencies": [],
+  "private": true
+}

http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/6f26290d/nifi-registry-core/nifi-registry-web-ui/src/main/locale/messages.es.xlf
----------------------------------------------------------------------
diff --git a/nifi-registry-core/nifi-registry-web-ui/src/main/locale/messages.es.xlf b/nifi-registry-core/nifi-registry-web-ui/src/main/locale/messages.es.xlf
new file mode 100644
index 0000000..2bfe354
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-web-ui/src/main/locale/messages.es.xlf
@@ -0,0 +1,130 @@
+<?xml version="1.0" encoding="UTF-8" ?>
+<!--
+  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.
+-->
+
+<xliff version="1.2" xmlns="urn:oasis:names:tc:xliff:document:1.2">
+  <file source-language="es" datatype="plaintext" original="ng2.template">
+    <body>
+      <trans-unit id="nf-admin-general-tab-title" datatype="html">
+         <source>general</source>
+         <target state="new">general</target>
+        <note priority="1" from="description">A description of the type of administration options available.</note>
+        <note priority="1" from="meaning">General administration tab</note>
+       </trans-unit>
+      <trans-unit id="nf-admin-users-tab-title" datatype="html">
+         <source>users</source>
+         <target state="new">Usuarios</target>
+        <note priority="1" from="description">A description of the type of administration options available.</note>
+        <note priority="1" from="meaning">Users administration tab</note>
+       </trans-unit>
+      <trans-unit id="nf-admin-workflow-tab-title" datatype="html">
+         <source>Workflow</source>
+         <target state="new">Flujo de trabajo</target>
+        <note priority="1" from="description">A description of the type of administration options available.</note>
+        <note priority="1" from="meaning">Workflow administration tab</note>
+       </trans-unit>
+      <trans-unit id="nf-admin-user-management-sidenav-membership-tab-title" datatype="html">
+         <source>Membership</source>
+         <target state="new">afiliación</target>
+        <note priority="1" from="description">View the groups to which this user belongs.</note>
+        <note priority="1" from="meaning">Group membership tab, user management sidenav</note>
+       </trans-unit>
+      <trans-unit id="nf-admin-group-management-sidenav-membership-tab-title" datatype="html">
+         <source>Membership</source>
+         <target state="new">afiliación</target>
+        <note priority="1" from="description">View the users that belong to this group.</note>
+        <note priority="1" from="meaning">User membership tab, group management sidenav</note>
+       </trans-unit>
+      <trans-unit id="nf-admin-user-management-sidenav-policies-tab-title" datatype="html">
+         <source>Policies</source>
+         <target state="new">Políticas</target>
+        <note priority="1" from="description">View the policies grated this user.</note>
+        <note priority="1" from="meaning">User policy tab, user management sidenav</note>
+       </trans-unit>
+      <trans-unit id="nf-admin-workflow-add-user-button" datatype="html">
+         <source>Add</source>
+         <target state="new">añadir</target>
+        <note priority="1" from="description">A button for adding a new user in the registry.</note>
+        <note priority="1" from="meaning">Add new user button</note>
+       </trans-unit>
+      <trans-unit id="nf-admin-workflow-cancel-add-user-button" datatype="html">
+         <source>Cancel</source>
+         <target state="new">Cancelar</target>
+        <note priority="1" from="description">A button for cancelling the creation of a new user in the registry.</note>
+        <note priority="1" from="meaning">Cancel creation of new user</note>
+       </trans-unit>
+      <trans-unit id="nf-admin-workflow-cancel-create-bucket-button" datatype="html">
+         <source>Cancel</source>
+         <target state="new">Cancelar</target>
+        <note priority="1" from="description">A button for cancelling the creation of a new bucket in the registry.</note>
+        <note priority="1" from="meaning">Cancel creation of new bucket</note>
+       </trans-unit>
+      <trans-unit id="nf-admin-workflow-cancel-bucket-policy-creation-button" datatype="html">
+         <source>Cancel</source>
+         <target state="new">Cancelar</target>
+        <note priority="1" from="description">A button for cancelling the creation of a new bucket policy in the registry.</note>
+        <note priority="1" from="meaning">Cancel creation of new bucket policy</note>
+       </trans-unit>
+      <trans-unit id="nf-clear-user-login-button" datatype="html">
+         <source>Clear</source>
+         <target state="new">Claro</target>
+        <note priority="1" from="description">A button for clearing the login form.</note>
+        <note priority="1" from="meaning">Clear log in form</note>
+       </trans-unit>
+      <trans-unit id="nf-user-login-button" datatype="html">
+         <source>Log In</source>
+         <target state="new">Iniciar sesión</target>
+        <note priority="1" from="description">A button for attempting to authenticate with the registry.</note>
+        <note priority="1" from="meaning">Log in form</note>
+       </trans-unit>
+      <trans-unit id="nf-admin-workflow-create-new-group-button" datatype="html">
+         <source>Create</source>
+         <target state="new">Crear</target>
+        <note priority="1" from="description">A button for creating a new group in the registry.</note>
+        <note priority="1" from="meaning">Create new group button</note>
+       </trans-unit>
+      <trans-unit id="nf-admin-workflow-create-bucket-button" datatype="html">
+         <source>Create</source>
+         <target state="new">Crear</target>
+        <note priority="1" from="description">A button for creating a new bucket in the registry.</note>
+        <note priority="1" from="meaning">Create new bucket button</note>
+       </trans-unit>
+      <trans-unit id="nf-admin-workflow-apply-policy-to-bucket-button" datatype="html">
+         <source>Apply</source>
+         <target state="new">Aplicar</target>
+        <note priority="1" from="description">A button for applying a new bucket policy in the registry.</note>
+        <note priority="1" from="meaning">Apply new bucket policy button</note>
+       </trans-unit>
+      <trans-unit id="nf-admin-workflow-cancel-create-new-group-button" datatype="html">
+         <source>Cancel</source>
+         <target state="new">Cancelar</target>
+        <note priority="1" from="description">A button for cancelling the creation of a new group in the registry.</note>
+        <note priority="1" from="meaning">Cancel creation of new group</note>
+       </trans-unit>
+      <trans-unit id="nf-admin-workflow-add-selected-users-to-group-button" datatype="html">
+         <source>Add</source>
+         <target state="new">añadir</target>
+        <note priority="1" from="description">A button for adding users to an existing group in the registry.</note>
+        <note priority="1" from="meaning">Add selected users to group button</note>
+       </trans-unit>
+      <trans-unit id="nf-admin-workflow-cancel-add-selected-users-to-group-button" datatype="html">
+         <source>Cancel</source>
+         <target state="new">Cancelar</target>
+        <note priority="1" from="description">A button for cancelling the addition of selected users to a group in the registry.</note>
+        <note priority="1" from="meaning">Cancel addition of selected users to group</note>
+       </trans-unit>
+    </body>
+  </file>
+</xliff>

http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/6f26290d/nifi-registry-core/nifi-registry-web-ui/src/main/resources/META-INF/LICENSE
----------------------------------------------------------------------
diff --git a/nifi-registry-core/nifi-registry-web-ui/src/main/resources/META-INF/LICENSE b/nifi-registry-core/nifi-registry-web-ui/src/main/resources/META-INF/LICENSE
new file mode 100644
index 0000000..5c33b8f
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-web-ui/src/main/resources/META-INF/LICENSE
@@ -0,0 +1,1152 @@
+
+                                 Apache License
+                           Version 2.0, January 2004
+                        http://www.apache.org/licenses/
+
+   TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+   1. Definitions.
+
+      "License" shall mean the terms and conditions for use, reproduction,
+      and distribution as defined by Sections 1 through 9 of this document.
+
+      "Licensor" shall mean the copyright owner or entity authorized by
+      the copyright owner that is granting the License.
+
+      "Legal Entity" shall mean the union of the acting entity and all
+      other entities that control, are controlled by, or are under common
+      control with that entity. For the purposes of this definition,
+      "control" means (i) the power, direct or indirect, to cause the
+      direction or management of such entity, whether by contract or
+      otherwise, or (ii) ownership of fifty percent (50%) or more of the
+      outstanding shares, or (iii) beneficial ownership of such entity.
+
+      "You" (or "Your") shall mean an individual or Legal Entity
+      exercising permissions granted by this License.
+
+      "Source" form shall mean the preferred form for making modifications,
+      including but not limited to software source code, documentation
+      source, and configuration files.
+
+      "Object" form shall mean any form resulting from mechanical
+      transformation or translation of a Source form, including but
+      not limited to compiled object code, generated documentation,
+      and conversions to other media types.
+
+      "Work" shall mean the work of authorship, whether in Source or
+      Object form, made available under the License, as indicated by a
+      copyright notice that is included in or attached to the work
+      (an example is provided in the Appendix below).
+
+      "Derivative Works" shall mean any work, whether in Source or Object
+      form, that is based on (or derived from) the Work and for which the
+      editorial revisions, annotations, elaborations, or other modifications
+      represent, as a whole, an original work of authorship. For the purposes
+      of this License, Derivative Works shall not include works that remain
+      separable from, or merely link (or bind by name) to the interfaces of,
+      the Work and Derivative Works thereof.
+
+      "Contribution" shall mean any work of authorship, including
+      the original version of the Work and any modifications or additions
+      to that Work or Derivative Works thereof, that is intentionally
+      submitted to Licensor for inclusion in the Work by the copyright owner
+      or by an individual or Legal Entity authorized to submit on behalf of
+      the copyright owner. For the purposes of this definition, "submitted"
+      means any form of electronic, verbal, or written communication sent
+      to the Licensor or its representatives, including but not limited to
+      communication on electronic mailing lists, source code control systems,
+      and issue tracking systems that are managed by, or on behalf of, the
+      Licensor for the purpose of discussing and improving the Work, but
+      excluding communication that is conspicuously marked or otherwise
+      designated in writing by the copyright owner as "Not a Contribution."
+
+      "Contributor" shall mean Licensor and any individual or Legal Entity
+      on behalf of whom a Contribution has been received by Licensor and
+      subsequently incorporated within the Work.
+
+   2. Grant of Copyright License. Subject to the terms and conditions of
+      this License, each Contributor hereby grants to You a perpetual,
+      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+      copyright license to reproduce, prepare Derivative Works of,
+      publicly display, publicly perform, sublicense, and distribute the
+      Work and such Derivative Works in Source or Object form.
+
+   3. Grant of Patent License. Subject to the terms and conditions of
+      this License, each Contributor hereby grants to You a perpetual,
+      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+      (except as stated in this section) patent license to make, have made,
+      use, offer to sell, sell, import, and otherwise transfer the Work,
+      where such license applies only to those patent claims licensable
+      by such Contributor that are necessarily infringed by their
+      Contribution(s) alone or by combination of their Contribution(s)
+      with the Work to which such Contribution(s) was submitted. If You
+      institute patent litigation against any entity (including a
+      cross-claim or counterclaim in a lawsuit) alleging that the Work
+      or a Contribution incorporated within the Work constitutes direct
+      or contributory patent infringement, then any patent licenses
+      granted to You under this License for that Work shall terminate
+      as of the date such litigation is filed.
+
+   4. Redistribution. You may reproduce and distribute copies of the
+      Work or Derivative Works thereof in any medium, with or without
+      modifications, and in Source or Object form, provided that You
+      meet the following conditions:
+
+      (a) You must give any other recipients of the Work or
+          Derivative Works a copy of this License; and
+
+      (b) You must cause any modified files to carry prominent notices
+          stating that You changed the files; and
+
+      (c) You must retain, in the Source form of any Derivative Works
+          that You distribute, all copyright, patent, trademark, and
+          attribution notices from the Source form of the Work,
+          excluding those notices that do not pertain to any part of
+          the Derivative Works; and
+
+      (d) If the Work includes a "NOTICE" text file as part of its
+          distribution, then any Derivative Works that You distribute must
+          include a readable copy of the attribution notices contained
+          within such NOTICE file, excluding those notices that do not
+          pertain to any part of the Derivative Works, in at least one
+          of the following places: within a NOTICE text file distributed
+          as part of the Derivative Works; within the Source form or
+          documentation, if provided along with the Derivative Works; or,
+          within a display generated by the Derivative Works, if and
+          wherever such third-party notices normally appear. The contents
+          of the NOTICE file are for informational purposes only and
+          do not modify the License. You may add Your own attribution
+          notices within Derivative Works that You distribute, alongside
+          or as an addendum to the NOTICE text from the Work, provided
+          that such additional attribution notices cannot be construed
+          as modifying the License.
+
+      You may add Your own copyright statement to Your modifications and
+      may provide additional or different license terms and conditions
+      for use, reproduction, or distribution of Your modifications, or
+      for any such Derivative Works as a whole, provided Your use,
+      reproduction, and distribution of the Work otherwise complies with
+      the conditions stated in this License.
+
+   5. Submission of Contributions. Unless You explicitly state otherwise,
+      any Contribution intentionally submitted for inclusion in the Work
+      by You to the Licensor shall be under the terms and conditions of
+      this License, without any additional terms or conditions.
+      Notwithstanding the above, nothing herein shall supersede or modify
+      the terms of any separate license agreement you may have executed
+      with Licensor regarding such Contributions.
+
+   6. Trademarks. This License does not grant permission to use the trade
+      names, trademarks, service marks, or product names of the Licensor,
+      except as required for reasonable and customary use in describing the
+      origin of the Work and reproducing the content of the NOTICE file.
+
+   7. Disclaimer of Warranty. Unless required by applicable law or
+      agreed to in writing, Licensor provides the Work (and each
+      Contributor provides its Contributions) on an "AS IS" BASIS,
+      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+      implied, including, without limitation, any warranties or conditions
+      of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+      PARTICULAR PURPOSE. You are solely responsible for determining the
+      appropriateness of using or redistributing the Work and assume any
+      risks associated with Your exercise of permissions under this License.
+
+   8. Limitation of Liability. In no event and under no legal theory,
+      whether in tort (including negligence), contract, or otherwise,
+      unless required by applicable law (such as deliberate and grossly
+      negligent acts) or agreed to in writing, shall any Contributor be
+      liable to You for damages, including any direct, indirect, special,
+      incidental, or consequential damages of any character arising as a
+      result of this License or out of the use or inability to use the
+      Work (including but not limited to damages for loss of goodwill,
+      work stoppage, computer failure or malfunction, or any and all
+      other commercial damages or losses), even if such Contributor
+      has been advised of the possibility of such damages.
+
+   9. Accepting Warranty or Additional Liability. While redistributing
+      the Work or Derivative Works thereof, You may choose to offer,
+      and charge a fee for, acceptance of support, warranty, indemnity,
+      or other liability obligations and/or rights consistent with this
+      License. However, in accepting such obligations, You may act only
+      on Your own behalf and on Your sole responsibility, not on behalf
+      of any other Contributor, and only if You agree to indemnify,
+      defend, and hold each Contributor harmless for any liability
+      incurred by, or claims asserted against, such Contributor by reason
+      of your accepting any such warranty or additional liability.
+
+   END OF TERMS AND CONDITIONS
+
+   APPENDIX: How to apply the Apache License to your work.
+
+      To apply the Apache License to your work, attach the following
+      boilerplate notice, with the fields enclosed by brackets "[]"
+      replaced with your own identifying information. (Don't include
+      the brackets!)  The text should be enclosed in the appropriate
+      comment syntax for the file format. We also recommend that a
+      file or class name and description of purpose be included on the
+      same "printed page" as the copyright notice for easier
+      identification within third-party archives.
+
+   Copyright [yyyy] [name of copyright owner]
+
+   Licensed under the Apache License, Version 2.0 (the "License");
+   you may not use this file except in compliance with the License.
+   You may obtain a copy of the License at
+
+       http://www.apache.org/licenses/LICENSE-2.0
+
+   Unless required by applicable law or agreed to in writing, software
+   distributed under the License is distributed on an "AS IS" BASIS,
+   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+   See the License for the specific language governing permissions and
+   limitations under the License.
+
+This product bundles 'Angular Quickstart' which is available under an MIT license.
+
+    Copyright (c) 2010-2016 Google, Inc. http://angularjs.org
+
+    Permission is hereby granted, free of charge, to any person obtaining a copy
+    of this software and associated documentation files (the "Software"), to deal
+    in the Software without restriction, including without limitation the rights
+    to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+    copies of the Software, and to permit persons to whom the Software is
+    furnished to do so, subject to the following conditions:
+
+    The above copyright notice and this permission notice shall be included in
+    all copies or substantial portions of the Software.
+
+    THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+    IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+    FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+    AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+    LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+    OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+    THE SOFTWARE.
+
+This product bundles 'Angular Core' which is available under an MIT license.
+
+    Copyright (c) 2014-2017 Google, Inc. http://angular.io
+
+    Permission is hereby granted, free of charge, to any person obtaining a copy
+    of this software and associated documentation files (the "Software"), to deal
+    in the Software without restriction, including without limitation the rights
+    to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+    copies of the Software, and to permit persons to whom the Software is
+    furnished to do so, subject to the following conditions:
+
+    The above copyright notice and this permission notice shall be included in
+    all copies or substantial portions of the Software.
+
+    THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+    IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+    FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+    AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+    LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+    OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+    THE SOFTWARE.
+
+This product bundles 'Angular Common' which is available under an MIT license.
+
+    Copyright (c) 2014-2017 Google, Inc. http://angular.io
+
+    Permission is hereby granted, free of charge, to any person obtaining a copy
+    of this software and associated documentation files (the "Software"), to deal
+    in the Software without restriction, including without limitation the rights
+    to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+    copies of the Software, and to permit persons to whom the Software is
+    furnished to do so, subject to the following conditions:
+
+    The above copyright notice and this permission notice shall be included in
+    all copies or substantial portions of the Software.
+
+    THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+    IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+    FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+    AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+    LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+    OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+    THE SOFTWARE.
+
+This product bundles 'Angular Common Http' which is available under an MIT license.
+
+    Copyright (c) 2014-2017 Google, Inc. http://angular.io
+
+    Permission is hereby granted, free of charge, to any person obtaining a copy
+    of this software and associated documentation files (the "Software"), to deal
+    in the Software without restriction, including without limitation the rights
+    to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+    copies of the Software, and to permit persons to whom the Software is
+    furnished to do so, subject to the following conditions:
+
+    The above copyright notice and this permission notice shall be included in
+    all copies or substantial portions of the Software.
+
+    THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+    IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+    FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+    AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+    LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+    OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+    THE SOFTWARE.
+
+This product bundles 'Angular Platform Browser' which is available under an MIT license.
+
+    Copyright (c) 2014-2017 Google, Inc. http://angular.io
+
+    Permission is hereby granted, free of charge, to any person obtaining a copy
+    of this software and associated documentation files (the "Software"), to deal
+    in the Software without restriction, including without limitation the rights
+    to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+    copies of the Software, and to permit persons to whom the Software is
+    furnished to do so, subject to the following conditions:
+
+    The above copyright notice and this permission notice shall be included in
+    all copies or substantial portions of the Software.
+
+    THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+    IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+    FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+    AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+    LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+    OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+    THE SOFTWARE.
+
+This product bundles 'Angular Platform Browser Dynamic' which is available under an MIT license.
+
+    Copyright (c) 2014-2017 Google, Inc. http://angular.io
+
+    Permission is hereby granted, free of charge, to any person obtaining a copy
+    of this software and associated documentation files (the "Software"), to deal
+    in the Software without restriction, including without limitation the rights
+    to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+    copies of the Software, and to permit persons to whom the Software is
+    furnished to do so, subject to the following conditions:
+
+    The above copyright notice and this permission notice shall be included in
+    all copies or substantial portions of the Software.
+
+    THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+    IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+    FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+    AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+    LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+    OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+    THE SOFTWARE.
+
+This product bundles 'Angular Http' which is available under an MIT license.
+
+    Copyright (c) 2014-2017 Google, Inc. http://angular.io
+
+    Permission is hereby granted, free of charge, to any person obtaining a copy
+    of this software and associated documentation files (the "Software"), to deal
+    in the Software without restriction, including without limitation the rights
+    to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+    copies of the Software, and to permit persons to whom the Software is
+    furnished to do so, subject to the following conditions:
+
+    The above copyright notice and this permission notice shall be included in
+    all copies or substantial portions of the Software.
+
+    THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+    IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+    FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+    AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+    LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+    OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+    THE SOFTWARE.
+
+This product bundles 'Angular Router' which is available under an MIT license.
+
+    Copyright (c) 2014-2017 Google, Inc. http://angular.io
+
+    Permission is hereby granted, free of charge, to any person obtaining a copy
+    of this software and associated documentation files (the "Software"), to deal
+    in the Software without restriction, including without limitation the rights
+    to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+    copies of the Software, and to permit persons to whom the Software is
+    furnished to do so, subject to the following conditions:
+
+    The above copyright notice and this permission notice shall be included in
+    all copies or substantial portions of the Software.
+
+    THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+    IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+    FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+    AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+    LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+    OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+    THE SOFTWARE.
+
+This product bundles 'Angular Forms' which is available under an MIT license.
+
+    Copyright (c) 2014-2017 Google, Inc. http://angular.io
+
+    Permission is hereby granted, free of charge, to any person obtaining a copy
+    of this software and associated documentation files (the "Software"), to deal
+    in the Software without restriction, including without limitation the rights
+    to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+    copies of the Software, and to permit persons to whom the Software is
+    furnished to do so, subject to the following conditions:
+
+    The above copyright notice and this permission notice shall be included in
+    all copies or substantial portions of the Software.
+
+    THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+    IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+    FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+    AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+    LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+    OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+    THE SOFTWARE.
+
+This product bundles 'Angular Flex Layout' which is available under an MIT license.
+
+    Copyright (c) 2017 Google LLC.
+
+    Permission is hereby granted, free of charge, to any person obtaining a copy
+    of this software and associated documentation files (the "Software"), to deal
+    in the Software without restriction, including without limitation the rights
+    to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+    copies of the Software, and to permit persons to whom the Software is
+    furnished to do so, subject to the following conditions:
+
+    The above copyright notice and this permission notice shall be included in
+    all copies or substantial portions of the Software.
+
+    THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+    IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+    FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+    AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+    LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+    OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+    THE SOFTWARE.
+
+This product bundles 'Angular Material' which is available under an MIT license.
+
+    Copyright (c) 2017 Google LLC.
+
+    Permission is hereby granted, free of charge, to any person obtaining a copy
+    of this software and associated documentation files (the "Software"), to deal
+    in the Software without restriction, including without limitation the rights
+    to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+    copies of the Software, and to permit persons to whom the Software is
+    furnished to do so, subject to the following conditions:
+
+    The above copyright notice and this permission notice shall be included in
+    all copies or substantial portions of the Software.
+
+    THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+    IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+    FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+    AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+    LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+    OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+    THE SOFTWARE.
+
+This product bundles 'Angular Platform Browser Animations' which is available under an MIT license.
+
+    Copyright (c) 2014-2017 Google, Inc. http://angular.io
+
+    Permission is hereby granted, free of charge, to any person obtaining a copy
+    of this software and associated documentation files (the "Software"), to deal
+    in the Software without restriction, including without limitation the rights
+    to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+    copies of the Software, and to permit persons to whom the Software is
+    furnished to do so, subject to the following conditions:
+
+    The above copyright notice and this permission notice shall be included in
+    all copies or substantial portions of the Software.
+
+    THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+    IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+    FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+    AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+    LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+    OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+    THE SOFTWARE.
+
+This product bundles 'Angular CDK' which is available under an MIT license.
+
+    Copyright (c) 2017 Google LLC.
+
+    Permission is hereby granted, free of charge, to any person obtaining a copy
+    of this software and associated documentation files (the "Software"), to deal
+    in the Software without restriction, including without limitation the rights
+    to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+    copies of the Software, and to permit persons to whom the Software is
+    furnished to do so, subject to the following conditions:
+
+    The above copyright notice and this permission notice shall be included in
+    all copies or substantial portions of the Software.
+
+    THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+    IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+    FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+    AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+    LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+    OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+    THE SOFTWARE.
+
+This product bundles 'Angular CDK Accordion' which is available under an MIT license.
+
+    Copyright (c) 2017 Google LLC.
+
+    Permission is hereby granted, free of charge, to any person obtaining a copy
+    of this software and associated documentation files (the "Software"), to deal
+    in the Software without restriction, including without limitation the rights
+    to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+    copies of the Software, and to permit persons to whom the Software is
+    furnished to do so, subject to the following conditions:
+
+    The above copyright notice and this permission notice shall be included in
+    all copies or substantial portions of the Software.
+
+    THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+    IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+    FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+    AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+    LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+    OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+    THE SOFTWARE.
+
+This product bundles 'Angular CDK Layout' which is available under an MIT license.
+
+    Copyright (c) 2017 Google LLC.
+
+    Permission is hereby granted, free of charge, to any person obtaining a copy
+    of this software and associated documentation files (the "Software"), to deal
+    in the Software without restriction, including without limitation the rights
+    to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+    copies of the Software, and to permit persons to whom the Software is
+    furnished to do so, subject to the following conditions:
+
+    The above copyright notice and this permission notice shall be included in
+    all copies or substantial portions of the Software.
+
+    THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+    IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+    FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+    AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+    LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+    OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+    THE SOFTWARE.
+
+This product bundles 'Angular CDK A11y' which is available under an MIT license.
+
+    Copyright (c) 2017 Google LLC.
+
+    Permission is hereby granted, free of charge, to any person obtaining a copy
+    of this software and associated documentation files (the "Software"), to deal
+    in the Software without restriction, including without limitation the rights
+    to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+    copies of the Software, and to permit persons to whom the Software is
+    furnished to do so, subject to the following conditions:
+
+    The above copyright notice and this permission notice shall be included in
+    all copies or substantial portions of the Software.
+
+    THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+    IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+    FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+    AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+    LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+    OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+    THE SOFTWARE.
+
+This product bundles 'Angular CDK Collections' which is available under an MIT license.
+
+    Copyright (c) 2017 Google LLC.
+
+    Permission is hereby granted, free of charge, to any person obtaining a copy
+    of this software and associated documentation files (the "Software"), to deal
+    in the Software without restriction, including without limitation the rights
+    to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+    copies of the Software, and to permit persons to whom the Software is
+    furnished to do so, subject to the following conditions:
+
+    The above copyright notice and this permission notice shall be included in
+    all copies or substantial portions of the Software.
+
+    THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+    IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+    FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+    AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+    LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+    OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+    THE SOFTWARE.
+
+This product bundles 'Angular CDK Observers' which is available under an MIT license.
+
+    Copyright (c) 2017 Google LLC.
+
+    Permission is hereby granted, free of charge, to any person obtaining a copy
+    of this software and associated documentation files (the "Software"), to deal
+    in the Software without restriction, including without limitation the rights
+    to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+    copies of the Software, and to permit persons to whom the Software is
+    furnished to do so, subject to the following conditions:
+
+    The above copyright notice and this permission notice shall be included in
+    all copies or substantial portions of the Software.
+
+    THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+    IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+    FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+    AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+    LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+    OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+    THE SOFTWARE.
+
+This product bundles 'Angular CDK Overlay' which is available under an MIT license.
+
+    Copyright (c) 2017 Google LLC.
+
+    Permission is hereby granted, free of charge, to any person obtaining a copy
+    of this software and associated documentation files (the "Software"), to deal
+    in the Software without restriction, including without limitation the rights
+    to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+    copies of the Software, and to permit persons to whom the Software is
+    furnished to do so, subject to the following conditions:
+
+    The above copyright notice and this permission notice shall be included in
+    all copies or substantial portions of the Software.
+
+    THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+    IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+    FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+    AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+    LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+    OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+    THE SOFTWARE.
+
+This product bundles 'Angular Platform' which is available under an MIT license.
+
+    Copyright (c) 2017 Google LLC.
+
+    Permission is hereby granted, free of charge, to any person obtaining a copy
+    of this software and associated documentation files (the "Software"), to deal
+    in the Software without restriction, including without limitation the rights
+    to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+    copies of the Software, and to permit persons to whom the Software is
+    furnished to do so, subject to the following conditions:
+
+    The above copyright notice and this permission notice shall be included in
+    all copies or substantial portions of the Software.
+
+    THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+    IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+    FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+    AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+    LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+    OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+    THE SOFTWARE.
+
+This product bundles 'Angular CDK Portal' which is available under an MIT license.
+
+    Copyright (c) 2017 Google LLC.
+
+    Permission is hereby granted, free of charge, to any person obtaining a copy
+    of this software and associated documentation files (the "Software"), to deal
+    in the Software without restriction, including without limitation the rights
+    to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+    copies of the Software, and to permit persons to whom the Software is
+    furnished to do so, subject to the following conditions:
+
+    The above copyright notice and this permission notice shall be included in
+    all copies or substantial portions of the Software.
+
+    THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+    IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+    FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+    AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+    LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+    OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+    THE SOFTWARE.
+
+This product bundles 'Angular Keycodes' which is available under an MIT license.
+
+    Copyright (c) 2017 Google LLC.
+
+    Permission is hereby granted, free of charge, to any person obtaining a copy
+    of this software and associated documentation files (the "Software"), to deal
+    in the Software without restriction, including without limitation the rights
+    to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+    copies of the Software, and to permit persons to whom the Software is
+    furnished to do so, subject to the following conditions:
+
+    The above copyright notice and this permission notice shall be included in
+    all copies or substantial portions of the Software.
+
+    THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+    IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+    FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+    AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+    LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+    OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+    THE SOFTWARE.
+
+This product bundles 'Angular CDK Bidi' which is available under an MIT license.
+
+    Copyright (c) 2017 Google LLC.
+
+    Permission is hereby granted, free of charge, to any person obtaining a copy
+    of this software and associated documentation files (the "Software"), to deal
+    in the Software without restriction, including without limitation the rights
+    to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+    copies of the Software, and to permit persons to whom the Software is
+    furnished to do so, subject to the following conditions:
+
+    The above copyright notice and this permission notice shall be included in
+    all copies or substantial portions of the Software.
+
+    THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+    IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+    FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+    AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+    LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+    OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+    THE SOFTWARE.
+
+This product bundles 'Angular CDK Coercion' which is available under an MIT license.
+
+    Copyright (c) 2017 Google LLC.
+
+    Permission is hereby granted, free of charge, to any person obtaining a copy
+    of this software and associated documentation files (the "Software"), to deal
+    in the Software without restriction, including without limitation the rights
+    to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+    copies of the Software, and to permit persons to whom the Software is
+    furnished to do so, subject to the following conditions:
+
+    The above copyright notice and this permission notice shall be included in
+    all copies or substantial portions of the Software.
+
+    THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+    IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+    FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+    AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+    LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+    OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+    THE SOFTWARE.
+
+This product bundles 'Angular CDK Table' which is available under an MIT license.
+
+    Copyright (c) 2017 Google LLC.
+
+    Permission is hereby granted, free of charge, to any person obtaining a copy
+    of this software and associated documentation files (the "Software"), to deal
+    in the Software without restriction, including without limitation the rights
+    to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+    copies of the Software, and to permit persons to whom the Software is
+    furnished to do so, subject to the following conditions:
+
+    The above copyright notice and this permission notice shall be included in
+    all copies or substantial portions of the Software.
+
+    THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+    IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+    FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+    AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+    LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+    OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+    THE SOFTWARE.
+
+This product bundles 'Angular CDK RXJS' which is available under an MIT license.
+
+    Copyright (c) 2017 Google LLC.
+
+    Permission is hereby granted, free of charge, to any person obtaining a copy
+    of this software and associated documentation files (the "Software"), to deal
+    in the Software without restriction, including without limitation the rights
+    to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+    copies of the Software, and to permit persons to whom the Software is
+    furnished to do so, subject to the following conditions:
+
+    The above copyright notice and this permission notice shall be included in
+    all copies or substantial portions of the Software.
+
+    THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+    IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+    FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+    AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+    LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+    OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+    THE SOFTWARE.
+
+This product bundles 'Angular CDK Scrolling' which is available under an MIT license.
+
+    Copyright (c) 2017 Google LLC.
+
+    Permission is hereby granted, free of charge, to any person obtaining a copy
+    of this software and associated documentation files (the "Software"), to deal
+    in the Software without restriction, including without limitation the rights
+    to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+    copies of the Software, and to permit persons to whom the Software is
+    furnished to do so, subject to the following conditions:
+
+    The above copyright notice and this permission notice shall be included in
+    all copies or substantial portions of the Software.
+
+    THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+    IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+    FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+    AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+    LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+    OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+    THE SOFTWARE.
+
+This product bundles 'Angular CDK Stepper' which is available under an MIT license.
+
+    Copyright (c) 2017 Google LLC.
+
+    Permission is hereby granted, free of charge, to any person obtaining a copy
+    of this software and associated documentation files (the "Software"), to deal
+    in the Software without restriction, including without limitation the rights
+    to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+    copies of the Software, and to permit persons to whom the Software is
+    furnished to do so, subject to the following conditions:
+
+    The above copyright notice and this permission notice shall be included in
+    all copies or substantial portions of the Software.
+
+    THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+    IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+    FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+    AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+    LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+    OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+    THE SOFTWARE.
+
+This product bundles 'Angular Animations' which is available under an MIT license.
+
+    Copyright (c) 2014-2017 Google, Inc. http://angular.io
+
+    Permission is hereby granted, free of charge, to any person obtaining a copy
+    of this software and associated documentation files (the "Software"), to deal
+    in the Software without restriction, including without limitation the rights
+    to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+    copies of the Software, and to permit persons to whom the Software is
+    furnished to do so, subject to the following conditions:
+
+    The above copyright notice and this permission notice shall be included in
+    all copies or substantial portions of the Software.
+
+    THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+    IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+    FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+    AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+    LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+    OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+    THE SOFTWARE.
+
+This product bundles 'Angular Animations Browser' which is available under an MIT license.
+
+    Copyright (c) 2014-2017 Google, Inc. http://angular.io
+
+    Permission is hereby granted, free of charge, to any person obtaining a copy
+    of this software and associated documentation files (the "Software"), to deal
+    in the Software without restriction, including without limitation the rights
+    to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+    copies of the Software, and to permit persons to whom the Software is
+    furnished to do so, subject to the following conditions:
+
+    The above copyright notice and this permission notice shall be included in
+    all copies or substantial portions of the Software.
+
+    THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+    IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+    FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+    AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+    LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+    OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+    THE SOFTWARE.
+
+This product bundles 'Angular Compiler' which is available under an MIT license.
+
+    Copyright (c) 2014-2017 Google, Inc. http://angular.io
+
+    Permission is hereby granted, free of charge, to any person obtaining a copy
+    of this software and associated documentation files (the "Software"), to deal
+    in the Software without restriction, including without limitation the rights
+    to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+    copies of the Software, and to permit persons to whom the Software is
+    furnished to do so, subject to the following conditions:
+
+    The above copyright notice and this permission notice shall be included in
+    all copies or substantial portions of the Software.
+
+    THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+    IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+    FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+    AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+    LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+    OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+    THE SOFTWARE.
+
+This product bundles 'Hammer JS' which is available under an MIT license.
+
+    Copyright (C) 2011-2017 by Jorik Tangelder (Eight Media)
+
+    Permission is hereby granted, free of charge, to any person obtaining a copy
+    of this software and associated documentation files (the "Software"), to deal
+    in the Software without restriction, including without limitation the rights
+    to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+    copies of the Software, and to permit persons to whom the Software is
+    furnished to do so, subject to the following conditions:
+
+    The above copyright notice and this permission notice shall be included in
+    all copies or substantial portions of the Software.
+
+    THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+    IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+    FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+    AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+    LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+    OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+    THE SOFTWARE.
+
+This product bundles 'Covalent Core' which is available under an MIT license.
+
+    Copyright (c) 2016 by Teradata. All rights reserved. http://teradata.com
+
+    Permission is hereby granted, free of charge, to any person obtaining a copy
+    of this software and associated documentation files (the "Software"), to deal
+    in the Software without restriction, including without limitation the rights
+    to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+    copies of the Software, and to permit persons to whom the Software is
+    furnished to do so, subject to the following conditions:
+
+    The above copyright notice and this permission notice shall be included in
+    all copies or substantial portions of the Software.
+
+    THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+    IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+    FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+    AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+    LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+    OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+    THE SOFTWARE.
+
+This product bundles 'Moment JS' which is available under an MIT license.
+
+    Copyright (c) JS Foundation and other contributors
+
+    Permission is hereby granted, free of charge, to any person obtaining a copy
+    of this software and associated documentation files (the "Software"), to deal
+    in the Software without restriction, including without limitation the rights
+    to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+    copies of the Software, and to permit persons to whom the Software is
+    furnished to do so, subject to the following conditions:
+
+    The above copyright notice and this permission notice shall be included in
+    all copies or substantial portions of the Software.
+
+    THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+    IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+    FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+    AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+    LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+    OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+    THE SOFTWARE.
+
+This product bundles 'Angular2 Moment' which is available under an MIT license.
+
+    Copyright (c) 2013-2017 Uri Shaked and contributors
+
+    Permission is hereby granted, free of charge, to any person obtaining a copy
+    of this software and associated documentation files (the "Software"), to deal
+    in the Software without restriction, including without limitation the rights
+    to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+    copies of the Software, and to permit persons to whom the Software is
+    furnished to do so, subject to the following conditions:
+
+    The above copyright notice and this permission notice shall be included in
+    all copies or substantial portions of the Software.
+
+    THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+    IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+    FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+    AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+    LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+    OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+    THE SOFTWARE.
+
+This product bundles 'Zone JS' which is available under an MIT license.
+
+    Copyright (c) 2016 Google, Inc.
+
+    Permission is hereby granted, free of charge, to any person obtaining a copy
+    of this software and associated documentation files (the "Software"), to deal
+    in the Software without restriction, including without limitation the rights
+    to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+    copies of the Software, and to permit persons to whom the Software is
+    furnished to do so, subject to the following conditions:
+
+    The above copyright notice and this permission notice shall be included in
+    all copies or substantial portions of the Software.
+
+    THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+    IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+    FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+    AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+    LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+    OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+    THE SOFTWARE.
+
+This product bundles 'Core JS' which is available under an MIT license.
+
+    Copyright (c) 2014-2017 Denis Pushkarev
+
+    Permission is hereby granted, free of charge, to any person obtaining a copy
+    of this software and associated documentation files (the "Software"), to deal
+    in the Software without restriction, including without limitation the rights
+    to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+    copies of the Software, and to permit persons to whom the Software is
+    furnished to do so, subject to the following conditions:
+
+    The above copyright notice and this permission notice shall be included in
+    all copies or substantial portions of the Software.
+
+    THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+    IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+    FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+    AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+    LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+    OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+    THE SOFTWARE.
+
+This product bundles 'SuperAgent' which is available under an MIT license.
+
+    Copyright (c) 2014-2016 TJ Holowaychuk <tj...@vision-media.ca>
+
+    Permission is hereby granted, free of charge, to any person obtaining a copy
+    of this software and associated documentation files (the "Software"), to deal
+    in the Software without restriction, including without limitation the rights
+    to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+    copies of the Software, and to permit persons to whom the Software is
+    furnished to do so, subject to the following conditions:
+
+    The above copyright notice and this permission notice shall be included in
+    all copies or substantial portions of the Software.
+
+    THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+    IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+    FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+    AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+    LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+    OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+    THE SOFTWARE.
+
+This product bundles 'Querystring' which is available under an MIT license.
+
+    Copyright 2012 Irakli Gozalishvili
+
+    Permission is hereby granted, free of charge, to any person obtaining a copy
+    of this software and associated documentation files (the "Software"), to deal
+    in the Software without restriction, including without limitation the rights
+    to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+    copies of the Software, and to permit persons to whom the Software is
+    furnished to do so, subject to the following conditions:
+
+    The above copyright notice and this permission notice shall be included in
+    all copies or substantial portions of the Software.
+
+    THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+    IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+    FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+    AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+    LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+    OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+    THE SOFTWARE.
+
+This product bundles 'Querystring' which is available under an MIT license.
+
+    Copyright 2012 Irakli Gozalishvili
+
+    Permission is hereby granted, free of charge, to any person obtaining a copy
+    of this software and associated documentation files (the "Software"), to deal
+    in the Software without restriction, including without limitation the rights
+    to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+    copies of the Software, and to permit persons to whom the Software is
+    furnished to do so, subject to the following conditions:
+
+    The above copyright notice and this permission notice shall be included in
+    all copies or substantial portions of the Software.
+
+    THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+    IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+    FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+    AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+    LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+    OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+    THE SOFTWARE.
+
+This product bundles 'System JS' which is available under an MIT license.
+
+    Copyright (C) 2013-2016 Guy Bedford
+
+    Permission is hereby granted, free of charge, to any person obtaining a copy
+    of this software and associated documentation files (the "Software"), to deal
+    in the Software without restriction, including without limitation the rights
+    to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+    copies of the Software, and to permit persons to whom the Software is
+    furnished to do so, subject to the following conditions:
+
+    The above copyright notice and this permission notice shall be included in
+    all copies or substantial portions of the Software.
+
+    THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+    IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+    FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+    AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+    LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+    OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+    THE SOFTWARE.
+
+This product bundles 'System JS Plugin Text' which is available under an MIT license.
+
+    Copyright (c) 2013 jspm
+
+    Permission is hereby granted, free of charge, to any person obtaining a copy
+    of this software and associated documentation files (the "Software"), to deal
+    in the Software without restriction, including without limitation the rights
+    to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+    copies of the Software, and to permit persons to whom the Software is
+    furnished to do so, subject to the following conditions:
+
+    The above copyright notice and this permission notice shall be included in
+    all copies or substantial portions of the Software.
+
+    THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+    IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+    FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+    AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+    LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+    OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+    THE SOFTWARE.
+
+This product bundles 'jQuery' which is available under an MIT license.
+
+    Copyright JS Foundation and other contributors, https://js.foundation/
+
+    This software consists of voluntary contributions made by many
+    individuals. For exact contribution history, see the revision history
+    available at https://github.com/jquery/jquery
+
+    Permission is hereby granted, free of charge, to any person obtaining a copy
+    of this software and associated documentation files (the "Software"), to deal
+    in the Software without restriction, including without limitation the rights
+    to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+    copies of the Software, and to permit persons to whom the Software is
+    furnished to do so, subject to the following conditions:
+
+    The above copyright notice and this permission notice shall be included in
+    all copies or substantial portions of the Software.
+
+    THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+    IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+    FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+    AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+    LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+    OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+    THE SOFTWARE.
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/6f26290d/nifi-registry-core/nifi-registry-web-ui/src/main/resources/META-INF/NOTICE
----------------------------------------------------------------------
diff --git a/nifi-registry-core/nifi-registry-web-ui/src/main/resources/META-INF/NOTICE b/nifi-registry-core/nifi-registry-web-ui/src/main/resources/META-INF/NOTICE
new file mode 100644
index 0000000..ed7fabe
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-web-ui/src/main/resources/META-INF/NOTICE
@@ -0,0 +1,23 @@
+nifi-registry-web-ui
+Copyright 2014-2017 The Apache Software Foundation
+
+This product includes software developed at
+The Apache Software Foundation (http://www.apache.org/).
+
+===========================================
+Apache Software License v2
+===========================================
+
+The following binary components are provided under the Apache Software License v2
+
+  (ASLv2) tslib
+    The following NOTICE information applies:
+
+      Copyright (c) Microsoft Corporation. All rights reserved.
+
+******************
+SIL OFL 1.1
+******************
+
+The following binary components are provided under the SIL Open Font License 1.1
+  (SIL OFL 1.1) FontAwesome (4.6.1 - http://fortawesome.github.io/Font-Awesome/license/)
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/6f26290d/nifi-registry-core/nifi-registry-web-ui/src/main/resources/filters/registry-min.properties
----------------------------------------------------------------------
diff --git a/nifi-registry-core/nifi-registry-web-ui/src/main/resources/filters/registry-min.properties b/nifi-registry-core/nifi-registry-web-ui/src/main/resources/filters/registry-min.properties
new file mode 100644
index 0000000..b4cc4dd
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-web-ui/src/main/resources/filters/registry-min.properties
@@ -0,0 +1,19 @@
+# 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.
+
+nf.registry.script.tags=<script src="nifi-registry/nf-registry.bundle.min.js?${project.version}"></script>
+nf.registry.style.tags=<link rel="stylesheet" href="nifi-registry/node_modules/@covalent/core/common/platform.css?${project.version}">\n\
+<link rel="stylesheet" href='nifi-registry/node_modules/@nifi-fds/core/common/styles/css/flow-design-system.min.css?${project.version}'/>\n\
+<link rel="stylesheet" href='nifi-registry/css/nf-registry.min.css?${project.version}'/>
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/6f26290d/nifi-registry-core/nifi-registry-web-ui/src/main/resources/filters/registry.properties
----------------------------------------------------------------------
diff --git a/nifi-registry-core/nifi-registry-web-ui/src/main/resources/filters/registry.properties b/nifi-registry-core/nifi-registry-web-ui/src/main/resources/filters/registry.properties
new file mode 100644
index 0000000..704c763
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-web-ui/src/main/resources/filters/registry.properties
@@ -0,0 +1,26 @@
+# 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.
+
+nf.registry.script.tags=<script src="nifi-registry/node_modules/systemjs/dist/system.src.js"></script>\n\
+<script src="nifi-registry/systemjs.spec.config.js?${project.version}"></script>\n\
+<script>\n\
+// bootstrap the app\n\
+System.import('nifi-registry/nf-registry-bootstrap.js?${project.version}').catch(function(err) {\n\
+console.error(err);\n\
+});\n\
+</script>
+nf.registry.style.tags=<link rel="stylesheet" href="nifi-registry/node_modules/@covalent/core/common/platform.css?${project.version}">\n\
+<link rel="stylesheet" href='nifi-registry/node_modules/@nifi-fds/core/common/styles/css/flow-design-system.min.css?${project.version}'/>\n\
+<link rel="stylesheet" href='nifi-registry/css/nf-registry.min.css?${project.version}'/>
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/6f26290d/nifi-registry-core/nifi-registry-web-ui/src/main/webapp/WEB-INF/pages/index.jsp
----------------------------------------------------------------------
diff --git a/nifi-registry-core/nifi-registry-web-ui/src/main/webapp/WEB-INF/pages/index.jsp b/nifi-registry-core/nifi-registry-web-ui/src/main/webapp/WEB-INF/pages/index.jsp
new file mode 100644
index 0000000..3a9a2c9
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-web-ui/src/main/webapp/WEB-INF/pages/index.jsp
@@ -0,0 +1,35 @@
+<%--
+ 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.
+--%>
+<%@ page contentType='text/html' pageEncoding='UTF-8' session='false' %>
+<!DOCTYPE html>
+<html>
+<head>
+    <title>NiFi Registry</title>
+    <base href='/'>
+    <meta charset='UTF-8'>
+    <meta name='viewport' content='width=device-width, initial-scale=1'>
+    <meta http-equiv='Content-Type' content='text/html; charset=UTF-8'/>
+    <link rel='shortcut icon' href='nifi-registry/images/registry-favicon.png' type='image/png'>
+    <link rel='icon' href='nifi-registry/images/registry-favicon.png' type='image/png'>
+    ${nf.registry.style.tags}
+    <link rel='stylesheet' href='nifi-registry/node_modules/font-awesome/css/font-awesome.css'/>
+</head>
+<body>
+<nf-registry-app></nf-registry-app>
+</body>
+${nf.registry.script.tags}
+</html>
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/6f26290d/nifi-registry-core/nifi-registry-web-ui/src/main/webapp/WEB-INF/web.xml
----------------------------------------------------------------------
diff --git a/nifi-registry-core/nifi-registry-web-ui/src/main/webapp/WEB-INF/web.xml b/nifi-registry-core/nifi-registry-web-ui/src/main/webapp/WEB-INF/web.xml
new file mode 100644
index 0000000..6913e5e
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-web-ui/src/main/webapp/WEB-INF/web.xml
@@ -0,0 +1,54 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+  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.
+-->
+<web-app version="3.0" xmlns="http://java.sun.com/xml/ns/javaee" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_3_0.xsd">
+    <display-name>nifi-registry</display-name>
+
+    <!-- servlet to map to login page -->
+    <servlet>
+        <servlet-name>Login</servlet-name>
+        <jsp-file>/WEB-INF/pages/index.jsp</jsp-file>
+    </servlet>
+    <servlet-mapping>
+        <servlet-name>Login</servlet-name>
+        <url-pattern>/login/*</url-pattern>
+    </servlet-mapping>
+
+    <!-- servlet to map to administration page -->
+    <servlet>
+        <servlet-name>Administration</servlet-name>
+        <jsp-file>/WEB-INF/pages/index.jsp</jsp-file>
+    </servlet>
+    <servlet-mapping>
+        <servlet-name>Administration</servlet-name>
+        <url-pattern>/administration/*</url-pattern>
+    </servlet-mapping>
+
+    <!-- servlet to map to explorer page -->
+    <servlet>
+        <servlet-name>Explorer</servlet-name>
+        <jsp-file>/WEB-INF/pages/index.jsp</jsp-file>
+    </servlet>
+    <servlet-mapping>
+        <servlet-name>Explorer</servlet-name>
+        <url-pattern>/explorer/*</url-pattern>
+    </servlet-mapping>
+
+    <welcome-file-list>
+        <welcome-file>index.jsp</welcome-file>
+        <welcome-file>/WEB-INF/pages/index.jsp</welcome-file>
+    </welcome-file-list>
+</web-app>
+


[49/51] [partial] nifi-registry git commit: NIFIREG-201 Refactoring project structure to better isolate extensions

Posted by kd...@apache.org.
http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/6f26290d/nifi-registry-client/src/main/java/org/apache/nifi/registry/client/FlowClient.java
----------------------------------------------------------------------
diff --git a/nifi-registry-client/src/main/java/org/apache/nifi/registry/client/FlowClient.java b/nifi-registry-client/src/main/java/org/apache/nifi/registry/client/FlowClient.java
deleted file mode 100644
index 6dd72e9..0000000
--- a/nifi-registry-client/src/main/java/org/apache/nifi/registry/client/FlowClient.java
+++ /dev/null
@@ -1,119 +0,0 @@
-/*
- * 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.client;
-
-import org.apache.nifi.registry.diff.VersionedFlowDifference;
-import org.apache.nifi.registry.field.Fields;
-import org.apache.nifi.registry.flow.VersionedFlow;
-
-import java.io.IOException;
-import java.util.List;
-
-/**
- * Client for interacting with flows.
- */
-public interface FlowClient {
-
-    /**
-     * Create the given flow in the given bucket.
-     *
-     * @param flow the flow to create
-     * @return the created flow with the identifier populated
-     * @throws NiFiRegistryException if an error is encountered other than IOException
-     * @throws IOException if an I/O error is encountered
-     */
-    VersionedFlow create(VersionedFlow flow) throws NiFiRegistryException, IOException;
-
-    /**
-     * Gets the flow with the given id in the given bucket.
-     *
-     * The list of snapshot metadata will NOT be populated.
-     *
-     * @param bucketId a bucket id
-     * @param flowId a flow id
-     * @return the flow with the given id in the given bucket
-     * @throws NiFiRegistryException if an error is encountered other than IOException
-     * @throws IOException if an I/O error is encountered
-     */
-    VersionedFlow get(String bucketId, String flowId) throws NiFiRegistryException, IOException;
-
-    /**
-     * Gets the flow with the given id.
-     *
-     * @param flowId a flow id
-     * @return the flow with the given id
-     * @throws NiFiRegistryException if an error is encountered other than IOException
-     * @throws IOException if an I/O error is encountered
-     */
-    VersionedFlow get(String flowId) throws NiFiRegistryException, IOException;
-
-    /**
-     * Updates the given flow with in the given bucket.
-     *
-     * The identifier of the flow must be populated in the flow object, and only the name and description can be updated.
-     *
-     * @param bucketId a bucket id
-     * @param flow the flow with updates
-     * @return the updated flow
-     * @throws NiFiRegistryException if an error is encountered other than IOException
-     * @throws IOException if an I/O error is encountered
-     */
-    VersionedFlow update(String bucketId, VersionedFlow flow) throws NiFiRegistryException, IOException;
-
-    /**
-     *  Deletes the flow with the given id in the given bucket.
-     *
-     * @param bucketId a bucket id
-     * @param flowId the id of the flow to delete
-     * @return the deleted flow
-     * @throws NiFiRegistryException if an error is encountered other than IOException
-     * @throws IOException if an I/O error is encountered
-     */
-    VersionedFlow delete(String bucketId, String flowId) throws NiFiRegistryException, IOException;
-
-    /**
-     * Gets the field info for flows.
-     *
-     * @return field info for flows
-     * @throws NiFiRegistryException if an error is encountered other than IOException
-     * @throws IOException if an I/O error is encountered
-     */
-    Fields getFields() throws NiFiRegistryException, IOException;
-
-    /**
-     * Gets the flows for a given bucket.
-     *
-     * @param bucketId a bucket id
-     * @return the flows in the given bucket
-     * @throws NiFiRegistryException if an error is encountered other than IOException
-     * @throws IOException if an I/O error is encountered
-     */
-    List<VersionedFlow> getByBucket(String bucketId) throws NiFiRegistryException, IOException;
-
-    /**
-     *
-     * @param bucketId a bucket id
-     * @param flowId the flow that is under inspection
-     * @param versionA the first version to use in the comparison
-     * @param versionB the second flow to use in the comparison
-     * @return the list of differences between the 2 flow versions grouped by component
-     * @throws NiFiRegistryException if an error is encountered other than IOException
-     * @throws IOException if an I/O error is encountered
-     */
-    VersionedFlowDifference diff(final String bucketId, final String flowId,
-                                 final Integer versionA, final Integer versionB) throws NiFiRegistryException, IOException;
-}

http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/6f26290d/nifi-registry-client/src/main/java/org/apache/nifi/registry/client/FlowSnapshotClient.java
----------------------------------------------------------------------
diff --git a/nifi-registry-client/src/main/java/org/apache/nifi/registry/client/FlowSnapshotClient.java b/nifi-registry-client/src/main/java/org/apache/nifi/registry/client/FlowSnapshotClient.java
deleted file mode 100644
index edf7beb..0000000
--- a/nifi-registry-client/src/main/java/org/apache/nifi/registry/client/FlowSnapshotClient.java
+++ /dev/null
@@ -1,133 +0,0 @@
-/*
- * 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.client;
-
-import org.apache.nifi.registry.flow.VersionedFlowSnapshot;
-import org.apache.nifi.registry.flow.VersionedFlowSnapshotMetadata;
-
-import java.io.IOException;
-import java.util.List;
-
-/**
- * Client for interacting with snapshots.
- */
-public interface FlowSnapshotClient {
-
-    /**
-     * Creates a new snapshot/version for the given flow.
-     *
-     * The snapshot object must have the version populated, and will receive an error if the submitted version is
-     * not the next one-up version.
-     *
-     * @param snapshot the new snapshot
-     * @return the created snapshot
-     * @throws NiFiRegistryException if an error is encountered other than IOException
-     * @throws IOException if an I/O error is encountered
-     */
-    VersionedFlowSnapshot create(VersionedFlowSnapshot snapshot) throws NiFiRegistryException, IOException;
-
-    /**
-     * Gets the snapshot for the given bucket, flow, and version.
-     *
-     * @param bucketId the bucket id
-     * @param flowId the flow id
-     * @param version the version
-     * @return the snapshot with the given version of the given flow in the given bucket
-     * @throws NiFiRegistryException if an error is encountered other than IOException
-     * @throws IOException if an I/O error is encountered
-     */
-    VersionedFlowSnapshot get(String bucketId, String flowId, int version) throws NiFiRegistryException, IOException;
-
-    /**
-     * Gets the snapshot for the given flow and version.
-     *
-     * @param flowId the flow id
-     * @param version the version
-     * @return the snapshot with the given version of the given flow
-     * @throws NiFiRegistryException if an error is encountered other than IOException
-     * @throws IOException if an I/O error is encountered
-     */
-    VersionedFlowSnapshot get(String flowId, int version) throws NiFiRegistryException, IOException;
-
-    /**
-     * Gets the latest snapshot for the given flow.
-     *
-     * @param bucketId the bucket id
-     * @param flowId the flow id
-     * @return the snapshot with the latest version for the given flow
-     * @throws NiFiRegistryException if an error is encountered other than IOException
-     * @throws IOException if an I/O error is encountered
-     */
-    VersionedFlowSnapshot getLatest(String bucketId, String flowId) throws NiFiRegistryException, IOException;
-
-    /**
-     * Gets the latest snapshot for the given flow.
-     *
-     * @param flowId the flow id
-     * @return the snapshot with the latest version for the given flow
-     * @throws NiFiRegistryException if an error is encountered other than IOException
-     * @throws IOException if an I/O error is encountered
-     */
-    VersionedFlowSnapshot getLatest(String flowId) throws NiFiRegistryException, IOException;
-
-    /**
-     * Gets the latest snapshot metadata for the given flow.
-     *
-     * @param bucketId the bucket id
-     * @param flowId the flow id
-     * @return the snapshot metadata for the latest version of the given flow
-     * @throws NiFiRegistryException if an error is encountered other than IOException
-     * @throws IOException if an I/O error is encountered
-     */
-    VersionedFlowSnapshotMetadata getLatestMetadata(String bucketId, String flowId) throws NiFiRegistryException, IOException;
-
-    /**
-     * Gets the latest snapshot metadata for the given flow.
-     *
-     * @param flowId the flow id
-     * @return the snapshot metadata for the latest version of the given flow
-     * @throws NiFiRegistryException if an error is encountered other than IOException
-     * @throws IOException if an I/O error is encountered
-     */
-    VersionedFlowSnapshotMetadata getLatestMetadata(String flowId) throws NiFiRegistryException, IOException;
-
-    /**
-     * Gets a list of the metadata for all snapshots of a given flow.
-     *
-     * The contents of each snapshot are not part of the response.
-     *
-     * @param bucketId the bucket id
-     * @param flowId the flow id
-     * @return the list of snapshot metadata
-     * @throws NiFiRegistryException if an error is encountered other than IOException
-     * @throws IOException if an I/O error is encountered
-     */
-    List<VersionedFlowSnapshotMetadata> getSnapshotMetadata(String bucketId, String flowId) throws NiFiRegistryException, IOException;
-
-    /**
-     * Gets a list of the metadata for all snapshots of a given flow.
-     *
-     * The contents of each snapshot are not part of the response.
-     *
-     * @param flowId the flow id
-     * @return the list of snapshot metadata
-     * @throws NiFiRegistryException if an error is encountered other than IOException
-     * @throws IOException if an I/O error is encountered
-     */
-    List<VersionedFlowSnapshotMetadata> getSnapshotMetadata(String flowId) throws NiFiRegistryException, IOException;
-
-}

http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/6f26290d/nifi-registry-client/src/main/java/org/apache/nifi/registry/client/ItemsClient.java
----------------------------------------------------------------------
diff --git a/nifi-registry-client/src/main/java/org/apache/nifi/registry/client/ItemsClient.java b/nifi-registry-client/src/main/java/org/apache/nifi/registry/client/ItemsClient.java
deleted file mode 100644
index 96fa801..0000000
--- a/nifi-registry-client/src/main/java/org/apache/nifi/registry/client/ItemsClient.java
+++ /dev/null
@@ -1,64 +0,0 @@
-/*
- * 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.client;
-
-import org.apache.nifi.registry.bucket.BucketItem;
-import org.apache.nifi.registry.field.Fields;
-
-import java.io.IOException;
-import java.util.List;
-
-/**
- * Client for interacting with bucket items.
- *
- * Bucket items contain the common fields across anything stored in the registry.
- *
- * Each item contains a type field and a link to the URI of the specific item.
- *
- * i.e. The link field of a flow item would contain the URI to the specific flow.
- */
-public interface ItemsClient {
-
-    /**
-     * Gets all bucket items in the registry.
-     *
-     * @return the list of all bucket items
-     * @throws NiFiRegistryException if an error is encountered other than IOException
-     * @throws IOException if an I/O error is encountered
-     */
-    List<BucketItem> getAll() throws NiFiRegistryException, IOException;
-
-    /**
-     * Gets all bucket items for the given bucket.
-     *
-     * @param bucketId the bucket id
-     * @return the list of items in the given bucket
-     * @throws NiFiRegistryException if an error is encountered other than IOException
-     * @throws IOException if an I/O error is encountered
-     */
-    List<BucketItem> getByBucket(String bucketId) throws NiFiRegistryException, IOException;
-
-    /**
-     * Gets the field info for bucket items.
-     *
-     * @return the list of field info
-     * @throws NiFiRegistryException if an error is encountered other than IOException
-     * @throws IOException if an I/O error is encountered
-     */
-    Fields getFields() throws NiFiRegistryException, IOException;
-
-}

http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/6f26290d/nifi-registry-client/src/main/java/org/apache/nifi/registry/client/NiFiRegistryClient.java
----------------------------------------------------------------------
diff --git a/nifi-registry-client/src/main/java/org/apache/nifi/registry/client/NiFiRegistryClient.java b/nifi-registry-client/src/main/java/org/apache/nifi/registry/client/NiFiRegistryClient.java
deleted file mode 100644
index 07fb817..0000000
--- a/nifi-registry-client/src/main/java/org/apache/nifi/registry/client/NiFiRegistryClient.java
+++ /dev/null
@@ -1,89 +0,0 @@
-/*
- * 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.client;
-
-import java.io.Closeable;
-
-/**
- * A client for interacting with the REST API of a NiFi registry instance.
- */
-public interface NiFiRegistryClient extends Closeable {
-
-    /**
-     * @return the client for interacting with buckets
-     */
-    BucketClient getBucketClient();
-
-    /**
-     * @return the client for interacting with buckets on behalf of the given proxied entities
-     */
-    BucketClient getBucketClient(String ... proxiedEntity);
-
-    /**
-     * @return the client for interacting with flows
-     */
-    FlowClient getFlowClient();
-
-    /**
-     * @return the client for interacting with flows on behalf of the given proxied entities
-     */
-    FlowClient getFlowClient(String ... proxiedEntity);
-
-    /**
-     * @return the client for interacting with flows/snapshots
-     */
-    FlowSnapshotClient getFlowSnapshotClient();
-
-    /**
-     * @return the client for interacting with flows/snapshots on behalf of the given proxied entities
-     */
-    FlowSnapshotClient getFlowSnapshotClient(String ... proxiedEntity);
-
-    /**
-     * @return the client for interacting with bucket items
-     */
-    ItemsClient getItemsClient();
-
-    /**
-     * @return the client for interacting with bucket items on behalf of the given proxied entities
-     */
-    ItemsClient getItemsClient(String ... proxiedEntity);
-
-    /**
-     * @return the client for obtaining information about the current user
-     */
-    UserClient getUserClient();
-
-    /**
-     * @return the client for obtaining information about the current user based on the given proxied entities
-     */
-    UserClient getUserClient(String ... proxiedEntity);
-
-    /**
-     * The builder interface that implementations should provide for obtaining the client.
-     */
-    interface Builder {
-
-        Builder config(NiFiRegistryClientConfig clientConfig);
-
-        NiFiRegistryClientConfig getConfig();
-
-        NiFiRegistryClient build();
-
-    }
-
-}

http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/6f26290d/nifi-registry-client/src/main/java/org/apache/nifi/registry/client/NiFiRegistryClientConfig.java
----------------------------------------------------------------------
diff --git a/nifi-registry-client/src/main/java/org/apache/nifi/registry/client/NiFiRegistryClientConfig.java b/nifi-registry-client/src/main/java/org/apache/nifi/registry/client/NiFiRegistryClientConfig.java
deleted file mode 100644
index de77b51..0000000
--- a/nifi-registry-client/src/main/java/org/apache/nifi/registry/client/NiFiRegistryClientConfig.java
+++ /dev/null
@@ -1,257 +0,0 @@
-/*
- * 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.client;
-
-import org.apache.nifi.registry.security.util.KeyStoreUtils;
-import org.apache.nifi.registry.security.util.KeystoreType;
-
-import javax.net.ssl.HostnameVerifier;
-import javax.net.ssl.KeyManager;
-import javax.net.ssl.KeyManagerFactory;
-import javax.net.ssl.SSLContext;
-import javax.net.ssl.TrustManager;
-import javax.net.ssl.TrustManagerFactory;
-import java.io.File;
-import java.io.FileInputStream;
-import java.io.InputStream;
-import java.security.KeyStore;
-import java.security.SecureRandom;
-
-/**
- * Configuration for a NiFiRegistryClient.
- */
-public class NiFiRegistryClientConfig {
-
-    private final String baseUrl;
-    private final SSLContext sslContext;
-    private final String keystoreFilename;
-    private final String keystorePass;
-    private final String keyPass;
-    private final KeystoreType keystoreType;
-    private final String truststoreFilename;
-    private final String truststorePass;
-    private final KeystoreType truststoreType;
-    private final HostnameVerifier hostnameVerifier;
-    private final Integer readTimeout;
-    private final Integer connectTimeout;
-
-
-    private NiFiRegistryClientConfig(final Builder builder) {
-        this.baseUrl = builder.baseUrl;
-        this.sslContext = builder.sslContext;
-        this.keystoreFilename = builder.keystoreFilename;
-        this.keystorePass = builder.keystorePass;
-        this.keyPass = builder.keyPass;
-        this.keystoreType = builder.keystoreType;
-        this.truststoreFilename = builder.truststoreFilename;
-        this.truststorePass = builder.truststorePass;
-        this.truststoreType = builder.truststoreType;
-        this.hostnameVerifier = builder.hostnameVerifier;
-        this.readTimeout = builder.readTimeout;
-        this.connectTimeout = builder.connectTimeout;
-    }
-
-    public String getBaseUrl() {
-        return baseUrl;
-    }
-
-    public SSLContext getSslContext() {
-        if (sslContext != null) {
-            return sslContext;
-        }
-
-        final KeyManagerFactory keyManagerFactory;
-        if (keystoreFilename != null && keystorePass != null && keystoreType != null) {
-            try {
-                // prepare the keystore
-                final KeyStore keyStore = KeyStoreUtils.getKeyStore(keystoreType.name());
-                try (final InputStream keyStoreStream = new FileInputStream(new File(keystoreFilename))) {
-                    keyStore.load(keyStoreStream, keystorePass.toCharArray());
-                }
-                keyManagerFactory = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm());
-
-                if (keyPass == null) {
-                    keyManagerFactory.init(keyStore, keystorePass.toCharArray());
-                } else {
-                    keyManagerFactory.init(keyStore, keyPass.toCharArray());
-                }
-            } catch (final Exception e) {
-                throw new IllegalStateException("Failed to load Keystore", e);
-            }
-        } else {
-            keyManagerFactory = null;
-        }
-
-        final TrustManagerFactory trustManagerFactory;
-        if (truststoreFilename != null && truststorePass != null && truststoreType != null) {
-            try {
-                // prepare the truststore
-                final KeyStore trustStore = KeyStoreUtils.getTrustStore(truststoreType.name());
-                try (final InputStream trustStoreStream = new FileInputStream(new File(truststoreFilename))) {
-                    trustStore.load(trustStoreStream, truststorePass.toCharArray());
-                }
-                trustManagerFactory = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());
-                trustManagerFactory.init(trustStore);
-            } catch (final Exception e) {
-                throw new IllegalStateException("Failed to load Truststore", e);
-            }
-        } else {
-            trustManagerFactory = null;
-        }
-
-        if (keyManagerFactory != null || trustManagerFactory != null) {
-            try {
-                // initialize the ssl context
-                KeyManager[] keyManagers = keyManagerFactory != null ? keyManagerFactory.getKeyManagers() : null;
-                TrustManager[] trustManagers = trustManagerFactory != null ? trustManagerFactory.getTrustManagers() : null;
-                final SSLContext sslContext = SSLContext.getInstance("TLS");
-                sslContext.init(keyManagers, trustManagers, new SecureRandom());
-                sslContext.getDefaultSSLParameters().setNeedClientAuth(true);
-
-                return sslContext;
-            } catch (final Exception e) {
-                throw new IllegalStateException("Created keystore and truststore but failed to initialize SSLContext", e);
-            }
-        } else {
-            return null;
-        }
-    }
-
-    public String getKeystoreFilename() {
-        return keystoreFilename;
-    }
-
-    public String getKeystorePass() {
-        return keystorePass;
-    }
-
-    public String getKeyPass() {
-        return keyPass;
-    }
-
-    public KeystoreType getKeystoreType() {
-        return keystoreType;
-    }
-
-    public String getTruststoreFilename() {
-        return truststoreFilename;
-    }
-
-    public String getTruststorePass() {
-        return truststorePass;
-    }
-
-    public KeystoreType getTruststoreType() {
-        return truststoreType;
-    }
-
-    public HostnameVerifier getHostnameVerifier() {
-        return hostnameVerifier;
-    }
-
-    public Integer getReadTimeout() {
-        return readTimeout;
-    }
-
-    public Integer getConnectTimeout() {
-        return connectTimeout;
-    }
-
-    /**
-     * Builder for client configuration.
-     */
-    public static class Builder {
-
-        private String baseUrl;
-        private SSLContext sslContext;
-        private String keystoreFilename;
-        private String keystorePass;
-        private String keyPass;
-        private KeystoreType keystoreType;
-        private String truststoreFilename;
-        private String truststorePass;
-        private KeystoreType truststoreType;
-        private HostnameVerifier hostnameVerifier;
-        private Integer readTimeout;
-        private Integer connectTimeout;
-
-        public Builder baseUrl(final String baseUrl) {
-            this.baseUrl = baseUrl;
-            return this;
-        }
-
-        public Builder sslContext(final SSLContext sslContext) {
-            this.sslContext = sslContext;
-            return this;
-        }
-
-        public Builder keystoreFilename(final String keystoreFilename) {
-            this.keystoreFilename = keystoreFilename;
-            return this;
-        }
-
-        public Builder keystorePassword(final String keystorePass) {
-            this.keystorePass = keystorePass;
-            return this;
-        }
-
-        public Builder keyPassword(final String keyPass) {
-            this.keyPass = keyPass;
-            return this;
-        }
-
-        public Builder keystoreType(final KeystoreType keystoreType) {
-            this.keystoreType = keystoreType;
-            return this;
-        }
-
-        public Builder truststoreFilename(final String truststoreFilename) {
-            this.truststoreFilename = truststoreFilename;
-            return this;
-        }
-
-        public Builder truststorePassword(final String truststorePass) {
-            this.truststorePass = truststorePass;
-            return this;
-        }
-
-        public Builder truststoreType(final KeystoreType truststoreType) {
-            this.truststoreType = truststoreType;
-            return this;
-        }
-
-        public Builder hostnameVerifier(final HostnameVerifier hostnameVerifier) {
-            this.hostnameVerifier = hostnameVerifier;
-            return this;
-        }
-
-        public Builder readTimeout(final Integer readTimeout) {
-            this.readTimeout = readTimeout;
-            return this;
-        }
-
-        public Builder connectTimeout(final Integer connectTimeout) {
-            this.connectTimeout = connectTimeout;
-            return this;
-        }
-
-        public NiFiRegistryClientConfig build() {
-            return new NiFiRegistryClientConfig(this);
-        }
-
-    }
-}

http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/6f26290d/nifi-registry-client/src/main/java/org/apache/nifi/registry/client/NiFiRegistryException.java
----------------------------------------------------------------------
diff --git a/nifi-registry-client/src/main/java/org/apache/nifi/registry/client/NiFiRegistryException.java b/nifi-registry-client/src/main/java/org/apache/nifi/registry/client/NiFiRegistryException.java
deleted file mode 100644
index 273a032..0000000
--- a/nifi-registry-client/src/main/java/org/apache/nifi/registry/client/NiFiRegistryException.java
+++ /dev/null
@@ -1,32 +0,0 @@
-/*
- * 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.client;
-
-/**
- * Indicates an error interacting with the NiFi registry for a reason other than IOException.
- */
-public class NiFiRegistryException extends Exception {
-
-    public NiFiRegistryException(final String message) {
-        super(message);
-    }
-
-    public NiFiRegistryException(final String message, final Throwable cause) {
-        super(message, cause);
-    }
-
-}

http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/6f26290d/nifi-registry-client/src/main/java/org/apache/nifi/registry/client/UserClient.java
----------------------------------------------------------------------
diff --git a/nifi-registry-client/src/main/java/org/apache/nifi/registry/client/UserClient.java b/nifi-registry-client/src/main/java/org/apache/nifi/registry/client/UserClient.java
deleted file mode 100644
index 181f7af..0000000
--- a/nifi-registry-client/src/main/java/org/apache/nifi/registry/client/UserClient.java
+++ /dev/null
@@ -1,42 +0,0 @@
-/*
- * 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.client;
-
-import org.apache.nifi.registry.authorization.CurrentUser;
-
-import java.io.IOException;
-
-public interface UserClient {
-
-    /**
-     * Obtains the access status of the current user.
-     *
-     * If the UserClient was obtained with proxied entities, then the access status should represent the status
-     * of the last identity in the chain.
-     *
-     * If the UserClient was obtained without proxied entities, then it would represent the identity of the certificate
-     * in the keystore used by the client.
-     *
-     * If the registry is not in secure mode, the anonymous identity is expected to be returned along with a flag indicating
-     * the user is anonymous.
-     *
-     * @return the access status of the current user
-     * @throws NiFiRegistryException if the proxying user is not a valid proxy or identity claim is otherwise invalid
-     */
-    CurrentUser getAccessStatus() throws NiFiRegistryException, IOException;
-
-}

http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/6f26290d/nifi-registry-client/src/main/java/org/apache/nifi/registry/client/impl/AbstractJerseyClient.java
----------------------------------------------------------------------
diff --git a/nifi-registry-client/src/main/java/org/apache/nifi/registry/client/impl/AbstractJerseyClient.java b/nifi-registry-client/src/main/java/org/apache/nifi/registry/client/impl/AbstractJerseyClient.java
deleted file mode 100644
index 479699e..0000000
--- a/nifi-registry-client/src/main/java/org/apache/nifi/registry/client/impl/AbstractJerseyClient.java
+++ /dev/null
@@ -1,120 +0,0 @@
-/*
- * 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.client.impl;
-
-import org.apache.nifi.registry.client.NiFiRegistryException;
-
-import javax.ws.rs.WebApplicationException;
-import javax.ws.rs.client.Invocation;
-import javax.ws.rs.client.WebTarget;
-import javax.ws.rs.core.Response;
-import java.io.IOException;
-import java.util.Collections;
-import java.util.HashMap;
-import java.util.Map;
-
-/**
- * Base class for the client operations to share exception handling.
- *
- * Sub-classes should always execute a request from getRequestBuilder(target) to ensure proper headers are sent.
- */
-public class AbstractJerseyClient {
-
-    private final Map<String,String> headers;
-
-    public AbstractJerseyClient(final Map<String, String> headers) {
-        this.headers = headers == null ? Collections.emptyMap() : Collections.unmodifiableMap(new HashMap<>(headers));
-    }
-
-    protected Map<String,String> getHeaders() {
-        return headers;
-    }
-
-    /**
-     * Creates a new Invocation.Builder for the given WebTarget with the headers added to the builder.
-     *
-     * @param webTarget the target for the request
-     * @return the builder for the target with the headers added
-     */
-    protected Invocation.Builder getRequestBuilder(final WebTarget webTarget) {
-        final Invocation.Builder requestBuilder = webTarget.request();
-        headers.entrySet().stream().forEach(e -> requestBuilder.header(e.getKey(), e.getValue()));
-        return requestBuilder;
-    }
-
-    /**
-     * Executes the given action and returns the result.
-     *
-     * @param action the action to execute
-     * @param errorMessage the message to use if a NiFiRegistryException is thrown
-     * @param <T> the return type of the action
-     * @return the result of the action
-     * @throws NiFiRegistryException if any exception other than IOException is encountered
-     * @throws IOException if an I/O error occurs communicating with the registry
-     */
-    protected <T> T executeAction(final String errorMessage, final NiFiRegistryAction<T> action) throws NiFiRegistryException, IOException {
-        try {
-            return action.execute();
-        } catch (final Exception e) {
-            final Throwable ioeCause = getIOExceptionCause(e);
-
-            if (ioeCause == null) {
-                final StringBuilder errorMessageBuilder = new StringBuilder(errorMessage);
-
-                // see if we have a WebApplicationException, and if so add the response body to the error message
-                if (e instanceof WebApplicationException) {
-                    final Response response = ((WebApplicationException) e).getResponse();
-                    final String responseBody = response.readEntity(String.class);
-                    errorMessageBuilder.append(": ").append(responseBody);
-                }
-
-                throw new NiFiRegistryException(errorMessageBuilder.toString(), e);
-            } else {
-                throw (IOException) ioeCause;
-            }
-        }
-    }
-
-
-    /**
-     * An action to execute with the given return type.
-     *
-     * @param <T> the return type of the action
-     */
-    protected interface NiFiRegistryAction<T> {
-
-        T execute();
-
-    }
-
-    /**
-     * @param e an exception that was encountered interacting with the registry
-     * @return the IOException that caused this exception, or null if the an IOException did not cause this exception
-     */
-    protected Throwable getIOExceptionCause(final Throwable e) {
-        if (e == null) {
-            return null;
-        }
-
-        if (e instanceof IOException) {
-            return e;
-        }
-
-        return getIOExceptionCause(e.getCause());
-    }
-
-}

http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/6f26290d/nifi-registry-client/src/main/java/org/apache/nifi/registry/client/impl/BucketItemDeserializer.java
----------------------------------------------------------------------
diff --git a/nifi-registry-client/src/main/java/org/apache/nifi/registry/client/impl/BucketItemDeserializer.java b/nifi-registry-client/src/main/java/org/apache/nifi/registry/client/impl/BucketItemDeserializer.java
deleted file mode 100644
index 5640d43..0000000
--- a/nifi-registry-client/src/main/java/org/apache/nifi/registry/client/impl/BucketItemDeserializer.java
+++ /dev/null
@@ -1,76 +0,0 @@
-/*
- * 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.client.impl;
-
-import com.fasterxml.jackson.core.JsonParser;
-import com.fasterxml.jackson.core.JsonProcessingException;
-import com.fasterxml.jackson.databind.DeserializationContext;
-import com.fasterxml.jackson.databind.JsonNode;
-import com.fasterxml.jackson.databind.deser.std.StdDeserializer;
-import org.apache.commons.lang3.StringUtils;
-import org.apache.nifi.registry.bucket.BucketItem;
-import org.apache.nifi.registry.bucket.BucketItemType;
-import org.apache.nifi.registry.flow.VersionedFlow;
-
-import java.io.IOException;
-import java.util.ArrayList;
-import java.util.Iterator;
-import java.util.List;
-
-public class BucketItemDeserializer extends StdDeserializer<BucketItem[]> {
-
-    public BucketItemDeserializer() {
-        super(BucketItem[].class);
-    }
-
-    @Override
-    public BucketItem[] deserialize(JsonParser jsonParser, DeserializationContext deserializationContext) throws IOException, JsonProcessingException {
-        final JsonNode arrayNode = jsonParser.getCodec().readTree(jsonParser);
-
-        final List<BucketItem> bucketItems = new ArrayList<>();
-
-        final Iterator<JsonNode> nodeIter = arrayNode.elements();
-        while (nodeIter.hasNext()) {
-            final JsonNode node = nodeIter.next();
-
-            final String type = node.get("type").asText();
-            if (StringUtils.isBlank(type)) {
-                throw new IllegalStateException("BucketItem type cannot be null or blank");
-            }
-
-            final BucketItemType bucketItemType;
-            try {
-                bucketItemType = BucketItemType.valueOf(type);
-            } catch (Exception e) {
-                throw new IllegalStateException("Unknown type for BucketItem: " + type, e);
-            }
-
-
-            switch (bucketItemType) {
-                case Flow:
-                    final VersionedFlow versionedFlow = jsonParser.getCodec().treeToValue(node, VersionedFlow.class);
-                    bucketItems.add(versionedFlow);
-                    break;
-                default:
-                    throw new IllegalStateException("Unknown type for BucketItem");
-            }
-        }
-
-        return bucketItems.toArray(new BucketItem[bucketItems.size()]);
-    }
-
-}

http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/6f26290d/nifi-registry-client/src/main/java/org/apache/nifi/registry/client/impl/JerseyBucketClient.java
----------------------------------------------------------------------
diff --git a/nifi-registry-client/src/main/java/org/apache/nifi/registry/client/impl/JerseyBucketClient.java b/nifi-registry-client/src/main/java/org/apache/nifi/registry/client/impl/JerseyBucketClient.java
deleted file mode 100644
index f84f8c6..0000000
--- a/nifi-registry-client/src/main/java/org/apache/nifi/registry/client/impl/JerseyBucketClient.java
+++ /dev/null
@@ -1,140 +0,0 @@
-/*
- * 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.client.impl;
-
-import org.apache.commons.lang3.StringUtils;
-import org.apache.nifi.registry.bucket.Bucket;
-import org.apache.nifi.registry.client.BucketClient;
-import org.apache.nifi.registry.client.NiFiRegistryException;
-import org.apache.nifi.registry.field.Fields;
-
-import javax.ws.rs.client.Entity;
-import javax.ws.rs.client.WebTarget;
-import javax.ws.rs.core.MediaType;
-import java.io.IOException;
-import java.util.Arrays;
-import java.util.Collections;
-import java.util.List;
-import java.util.Map;
-
-/**
- * Jersey implementation of BucketClient.
- */
-public class JerseyBucketClient extends AbstractJerseyClient implements BucketClient {
-
-    private final WebTarget bucketsTarget;
-
-
-    public JerseyBucketClient(final WebTarget baseTarget) {
-        this(baseTarget, Collections.emptyMap());
-    }
-
-    public JerseyBucketClient(final WebTarget baseTarget, final Map<String,String> headers) {
-        super(headers);
-        this.bucketsTarget = baseTarget.path("/buckets");
-    }
-
-    @Override
-    public Bucket create(final Bucket bucket) throws NiFiRegistryException, IOException {
-        if (bucket == null) {
-            throw new IllegalArgumentException("Bucket cannot be null");
-        }
-
-        return executeAction("Error creating bucket", () -> {
-            return getRequestBuilder(bucketsTarget)
-                    .post(
-                            Entity.entity(bucket, MediaType.APPLICATION_JSON),
-                            Bucket.class
-                    );
-        });
-
-    }
-
-    @Override
-    public Bucket get(final String bucketId) throws NiFiRegistryException, IOException {
-        if (StringUtils.isBlank(bucketId)) {
-            throw new IllegalArgumentException("Bucket ID cannot be blank");
-        }
-
-        return executeAction("Error retrieving bucket", () -> {
-            final WebTarget target = bucketsTarget
-                    .path("/{bucketId}")
-                    .resolveTemplate("bucketId", bucketId);
-
-            return getRequestBuilder(target).get(Bucket.class);
-        });
-
-    }
-
-    @Override
-    public Bucket update(final Bucket bucket) throws NiFiRegistryException, IOException {
-        if (bucket == null) {
-            throw new IllegalArgumentException("Bucket cannot be null");
-        }
-
-        if (StringUtils.isBlank(bucket.getIdentifier())) {
-            throw new IllegalArgumentException("Bucket Identifier must be provided");
-        }
-
-        return executeAction("Error updating bucket", () -> {
-            final WebTarget target = bucketsTarget
-                    .path("/{bucketId}")
-                    .resolveTemplate("bucketId", bucket.getIdentifier());
-
-            return getRequestBuilder(target)
-                    .put(
-                            Entity.entity(bucket, MediaType.APPLICATION_JSON),
-                            Bucket.class
-                    );
-
-        });
-    }
-
-    @Override
-    public Bucket delete(final String bucketId) throws NiFiRegistryException, IOException {
-        if (StringUtils.isBlank(bucketId)) {
-            throw new IllegalArgumentException("Bucket ID cannot be blank");
-        }
-
-        return executeAction("Error deleting bucket", () -> {
-            final WebTarget target = bucketsTarget
-                    .path("/{bucketId}")
-                    .resolveTemplate("bucketId", bucketId);
-
-            return getRequestBuilder(target).delete(Bucket.class);
-        });
-    }
-
-    @Override
-    public Fields getFields() throws NiFiRegistryException, IOException {
-        return executeAction("Error retrieving bucket field info", () -> {
-            final WebTarget target = bucketsTarget
-                    .path("/fields");
-
-            return getRequestBuilder(target).get(Fields.class);
-        });
-    }
-
-    @Override
-    public List<Bucket> getAll() throws NiFiRegistryException, IOException {
-        return executeAction("Error retrieving all buckets", () -> {
-            final Bucket[] buckets = getRequestBuilder(bucketsTarget).get(Bucket[].class);
-            return buckets == null ? Collections.emptyList() : Arrays.asList(buckets);
-        });
-    }
-
-}

http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/6f26290d/nifi-registry-client/src/main/java/org/apache/nifi/registry/client/impl/JerseyFlowClient.java
----------------------------------------------------------------------
diff --git a/nifi-registry-client/src/main/java/org/apache/nifi/registry/client/impl/JerseyFlowClient.java b/nifi-registry-client/src/main/java/org/apache/nifi/registry/client/impl/JerseyFlowClient.java
deleted file mode 100644
index 486a20a..0000000
--- a/nifi-registry-client/src/main/java/org/apache/nifi/registry/client/impl/JerseyFlowClient.java
+++ /dev/null
@@ -1,205 +0,0 @@
-/*
- * 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.client.impl;
-
-import org.apache.commons.lang3.StringUtils;
-import org.apache.nifi.registry.client.FlowClient;
-import org.apache.nifi.registry.client.NiFiRegistryException;
-import org.apache.nifi.registry.diff.VersionedFlowDifference;
-import org.apache.nifi.registry.field.Fields;
-import org.apache.nifi.registry.flow.VersionedFlow;
-
-import javax.ws.rs.client.Entity;
-import javax.ws.rs.client.WebTarget;
-import javax.ws.rs.core.MediaType;
-import java.io.IOException;
-import java.util.Arrays;
-import java.util.Collections;
-import java.util.List;
-import java.util.Map;
-
-/**
- * Jersey implementation of FlowClient.
- */
-public class JerseyFlowClient extends AbstractJerseyClient  implements FlowClient {
-
-    private final WebTarget flowsTarget;
-    private final WebTarget bucketFlowsTarget;
-
-    public JerseyFlowClient(final WebTarget baseTarget) {
-        this(baseTarget, Collections.emptyMap());
-    }
-
-    public JerseyFlowClient(final WebTarget baseTarget, final Map<String,String> headers) {
-        super(headers);
-        this.flowsTarget = baseTarget.path("/flows");
-        this.bucketFlowsTarget = baseTarget.path("/buckets/{bucketId}/flows");
-    }
-
-    @Override
-    public VersionedFlow create(final VersionedFlow flow) throws NiFiRegistryException, IOException {
-        if (flow == null) {
-            throw new IllegalArgumentException("VersionedFlow cannot be null");
-        }
-
-        final String bucketId = flow.getBucketIdentifier();
-        if (StringUtils.isBlank(bucketId)) {
-            throw new IllegalArgumentException("Bucket Identifier cannot be blank");
-        }
-
-        return executeAction("Error creating flow", () -> {
-            final WebTarget target = bucketFlowsTarget
-                    .resolveTemplate("bucketId", bucketId);
-
-            return getRequestBuilder(target)
-                    .post(
-                            Entity.entity(flow, MediaType.APPLICATION_JSON),
-                            VersionedFlow.class
-                    );
-        });
-    }
-
-    @Override
-    public VersionedFlow get(final String bucketId, final String flowId) throws NiFiRegistryException, IOException {
-        if (StringUtils.isBlank(bucketId)) {
-            throw new IllegalArgumentException("Bucket Identifier cannot be blank");
-        }
-
-        if (StringUtils.isBlank(flowId)) {
-            throw new IllegalArgumentException("Flow Identifier cannot be blank");
-        }
-
-        return executeAction("Error retrieving flow", () -> {
-            final WebTarget target = bucketFlowsTarget
-                    .path("/{flowId}")
-                    .resolveTemplate("bucketId", bucketId)
-                    .resolveTemplate("flowId", flowId);
-
-            return  getRequestBuilder(target).get(VersionedFlow.class);
-        });
-    }
-
-    @Override
-    public VersionedFlow get(final String flowId) throws NiFiRegistryException, IOException {
-        if (StringUtils.isBlank(flowId)) {
-            throw new IllegalArgumentException("Flow Identifier cannot be blank");
-        }
-
-        // this uses the flowsTarget because its calling /flows/{flowId} without knowing a bucketId
-        return executeAction("Error retrieving flow", () -> {
-            final WebTarget target = flowsTarget
-                    .path("/{flowId}")
-                    .resolveTemplate("flowId", flowId);
-
-            return  getRequestBuilder(target).get(VersionedFlow.class);
-        });
-    }
-
-    @Override
-    public VersionedFlow update(final String bucketId, final VersionedFlow flow) throws NiFiRegistryException, IOException {
-        if (StringUtils.isBlank(bucketId)) {
-            throw new IllegalArgumentException("Bucket Identifier cannot be blank");
-        }
-
-        if (flow == null) {
-            throw new IllegalArgumentException("VersionedFlow cannot be null");
-        }
-
-        if (StringUtils.isBlank(flow.getIdentifier())) {
-            throw new IllegalArgumentException("VersionedFlow identifier must be provided");
-        }
-
-        return executeAction("Error updating flow", () -> {
-            final WebTarget target = bucketFlowsTarget
-                    .path("/{flowId}")
-                    .resolveTemplate("bucketId", bucketId)
-                    .resolveTemplate("flowId", flow.getIdentifier());
-
-            return  getRequestBuilder(target)
-                    .put(
-                            Entity.entity(flow, MediaType.APPLICATION_JSON),
-                            VersionedFlow.class
-                    );
-        });
-    }
-
-    @Override
-    public VersionedFlow delete(final String bucketId, final String flowId) throws NiFiRegistryException, IOException {
-        if (StringUtils.isBlank(bucketId)) {
-            throw new IllegalArgumentException("Bucket Identifier cannot be blank");
-        }
-
-        if (StringUtils.isBlank(flowId)) {
-            throw new IllegalArgumentException("Flow Identifier cannot be blank");
-        }
-
-        return executeAction("Error deleting flow", () -> {
-            final WebTarget target = bucketFlowsTarget
-                    .path("/{flowId}")
-                    .resolveTemplate("bucketId", bucketId)
-                    .resolveTemplate("flowId", flowId);
-
-            return getRequestBuilder(target).delete(VersionedFlow.class);
-        });
-    }
-
-    @Override
-    public Fields getFields() throws NiFiRegistryException, IOException {
-        return executeAction("Error retrieving fields info for flows", () -> {
-            final WebTarget target = flowsTarget.path("/fields");
-            return getRequestBuilder(target).get(Fields.class);
-        });
-    }
-
-    @Override
-    public List<VersionedFlow> getByBucket(final String bucketId) throws NiFiRegistryException, IOException {
-        if (StringUtils.isBlank(bucketId)) {
-            throw new IllegalArgumentException("Bucket Identifier cannot be blank");
-        }
-
-        return executeAction("Error getting flows for bucket", () -> {
-            WebTarget target = bucketFlowsTarget;
-            target = target.resolveTemplate("bucketId", bucketId);
-
-            final VersionedFlow[] versionedFlows = getRequestBuilder(target).get(VersionedFlow[].class);
-            return  versionedFlows == null ? Collections.emptyList() : Arrays.asList(versionedFlows);
-        });
-    }
-
-    @Override
-    public VersionedFlowDifference diff(final String bucketId, final String flowId,
-                                        final Integer versionA, final Integer versionB) throws NiFiRegistryException, IOException {
-        if (StringUtils.isBlank(bucketId)) {
-            throw new IllegalArgumentException("Bucket Identifier cannot be blank");
-        }
-
-        if (StringUtils.isBlank(flowId)) {
-            throw new IllegalArgumentException("Flow Identifier cannot be blank");
-        }
-
-        return executeAction("Error retrieving flow", () -> {
-            final WebTarget target = bucketFlowsTarget
-                    .path("/{flowId}/diff/{versionA}/{versionB}")
-                    .resolveTemplate("bucketId", bucketId)
-                    .resolveTemplate("flowId", flowId)
-                    .resolveTemplate("versionA", versionA)
-                    .resolveTemplate("versionB", versionB);
-
-            return  getRequestBuilder(target).get(VersionedFlowDifference.class);
-        });
-    }
-}

http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/6f26290d/nifi-registry-client/src/main/java/org/apache/nifi/registry/client/impl/JerseyFlowSnapshotClient.java
----------------------------------------------------------------------
diff --git a/nifi-registry-client/src/main/java/org/apache/nifi/registry/client/impl/JerseyFlowSnapshotClient.java b/nifi-registry-client/src/main/java/org/apache/nifi/registry/client/impl/JerseyFlowSnapshotClient.java
deleted file mode 100644
index befe389..0000000
--- a/nifi-registry-client/src/main/java/org/apache/nifi/registry/client/impl/JerseyFlowSnapshotClient.java
+++ /dev/null
@@ -1,246 +0,0 @@
-/*
- * 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.client.impl;
-
-import org.apache.commons.lang3.StringUtils;
-import org.apache.nifi.registry.client.FlowSnapshotClient;
-import org.apache.nifi.registry.client.NiFiRegistryException;
-import org.apache.nifi.registry.flow.VersionedFlowSnapshot;
-import org.apache.nifi.registry.flow.VersionedFlowSnapshotMetadata;
-
-import javax.ws.rs.client.Entity;
-import javax.ws.rs.client.WebTarget;
-import javax.ws.rs.core.MediaType;
-import java.io.IOException;
-import java.util.Arrays;
-import java.util.Collections;
-import java.util.List;
-import java.util.Map;
-
-/**
- * Jersey implementation of FlowSnapshotClient.
- */
-public class JerseyFlowSnapshotClient extends AbstractJerseyClient implements FlowSnapshotClient {
-
-    final WebTarget bucketFlowSnapshotTarget;
-    final WebTarget flowsFlowSnapshotTarget;
-
-    public JerseyFlowSnapshotClient(final WebTarget baseTarget) {
-        this(baseTarget, Collections.emptyMap());
-    }
-
-    public JerseyFlowSnapshotClient(final WebTarget baseTarget, final Map<String,String> headers) {
-        super(headers);
-        this.bucketFlowSnapshotTarget = baseTarget.path("/buckets/{bucketId}/flows/{flowId}/versions");
-        this.flowsFlowSnapshotTarget = baseTarget.path("/flows/{flowId}/versions");
-    }
-
-    @Override
-    public VersionedFlowSnapshot create(final VersionedFlowSnapshot snapshot)
-            throws NiFiRegistryException, IOException {
-        if (snapshot.getSnapshotMetadata() == null) {
-            throw new IllegalArgumentException("Snapshot Metadata cannot be null");
-        }
-
-        final String bucketId = snapshot.getSnapshotMetadata().getBucketIdentifier();
-        if (StringUtils.isBlank(bucketId)) {
-            throw new IllegalArgumentException("Bucket Identifier cannot be blank");
-        }
-
-        final String flowId = snapshot.getSnapshotMetadata().getFlowIdentifier();
-        if (StringUtils.isBlank(flowId)) {
-            throw new IllegalArgumentException("Flow Identifier cannot be blank");
-        }
-
-        return executeAction("Error creating snapshot", () -> {
-            final WebTarget target = bucketFlowSnapshotTarget
-                    .resolveTemplate("bucketId", bucketId)
-                    .resolveTemplate("flowId", flowId);
-
-            return  getRequestBuilder(target)
-                    .post(
-                            Entity.entity(snapshot, MediaType.APPLICATION_JSON),
-                            VersionedFlowSnapshot.class
-                    );
-        });
-    }
-
-    @Override
-    public VersionedFlowSnapshot get(final String bucketId, final String flowId, final int version)
-            throws NiFiRegistryException, IOException {
-        if (StringUtils.isBlank(bucketId)) {
-            throw new IllegalArgumentException("Bucket Identifier cannot be blank");
-        }
-
-        if (StringUtils.isBlank(flowId)) {
-            throw new IllegalArgumentException("Flow Identifier cannot be blank");
-        }
-
-        if (version < 1) {
-            throw new IllegalArgumentException("Version must be greater than 1");
-        }
-
-        return executeAction("Error retrieving flow snapshot", () -> {
-            final WebTarget target = bucketFlowSnapshotTarget
-                    .path("/{version}")
-                    .resolveTemplate("bucketId", bucketId)
-                    .resolveTemplate("flowId", flowId)
-                    .resolveTemplate("version", version);
-
-            return getRequestBuilder(target).get(VersionedFlowSnapshot.class);
-        });
-    }
-
-    @Override
-    public VersionedFlowSnapshot get(final String flowId, final int version)
-            throws NiFiRegistryException, IOException {
-
-        if (StringUtils.isBlank(flowId)) {
-            throw new IllegalArgumentException("Flow Identifier cannot be blank");
-        }
-
-        if (version < 1) {
-            throw new IllegalArgumentException("Version must be greater than 1");
-        }
-
-        return executeAction("Error retrieving flow snapshot", () -> {
-            final WebTarget target = flowsFlowSnapshotTarget
-                    .path("/{version}")
-                    .resolveTemplate("flowId", flowId)
-                    .resolveTemplate("version", version);
-
-            return getRequestBuilder(target).get(VersionedFlowSnapshot.class);
-        });
-    }
-
-    @Override
-    public VersionedFlowSnapshot getLatest(final String bucketId, final String flowId)
-            throws NiFiRegistryException, IOException {
-        if (StringUtils.isBlank(bucketId)) {
-            throw new IllegalArgumentException("Bucket Identifier cannot be blank");
-        }
-
-        if (StringUtils.isBlank(flowId)) {
-            throw new IllegalArgumentException("Flow Identifier cannot be blank");
-        }
-
-        return executeAction("Error retrieving latest snapshot", () -> {
-            final WebTarget target = bucketFlowSnapshotTarget
-                    .path("/latest")
-                    .resolveTemplate("bucketId", bucketId)
-                    .resolveTemplate("flowId", flowId);
-
-            return getRequestBuilder(target).get(VersionedFlowSnapshot.class);
-        });
-    }
-
-    @Override
-    public VersionedFlowSnapshot getLatest(final String flowId)
-            throws NiFiRegistryException, IOException {
-        if (StringUtils.isBlank(flowId)) {
-            throw new IllegalArgumentException("Flow Identifier cannot be blank");
-        }
-
-        return executeAction("Error retrieving latest snapshot", () -> {
-            final WebTarget target = flowsFlowSnapshotTarget
-                    .path("/latest")
-                    .resolveTemplate("flowId", flowId);
-
-            return getRequestBuilder(target).get(VersionedFlowSnapshot.class);
-        });
-    }
-
-    @Override
-    public VersionedFlowSnapshotMetadata getLatestMetadata(final String bucketId, final String flowId) throws NiFiRegistryException, IOException {
-        if (StringUtils.isBlank(bucketId)) {
-            throw new IllegalArgumentException("Bucket Identifier cannot be blank");
-        }
-
-        if (StringUtils.isBlank(flowId)) {
-            throw new IllegalArgumentException("Flow Identifier cannot be blank");
-        }
-
-        return executeAction("Error retrieving latest snapshot metadata", () -> {
-            final WebTarget target = bucketFlowSnapshotTarget
-                    .path("/latest/metadata")
-                    .resolveTemplate("bucketId", bucketId)
-                    .resolveTemplate("flowId", flowId);
-
-            return getRequestBuilder(target).get(VersionedFlowSnapshotMetadata.class);
-        });
-    }
-
-    @Override
-    public VersionedFlowSnapshotMetadata getLatestMetadata(final String flowId) throws NiFiRegistryException, IOException {
-        if (StringUtils.isBlank(flowId)) {
-            throw new IllegalArgumentException("Flow Identifier cannot be blank");
-        }
-
-        return executeAction("Error retrieving latest snapshot metadata", () -> {
-            final WebTarget target = flowsFlowSnapshotTarget
-                    .path("/latest/metadata")
-                    .resolveTemplate("flowId", flowId);
-
-            return getRequestBuilder(target).get(VersionedFlowSnapshotMetadata.class);
-        });
-    }
-
-    @Override
-    @SuppressWarnings("unchecked")
-    public List<VersionedFlowSnapshotMetadata> getSnapshotMetadata(final String bucketId, final String flowId)
-            throws NiFiRegistryException, IOException {
-        if (StringUtils.isBlank(bucketId)) {
-            throw new IllegalArgumentException("Bucket Identifier cannot be blank");
-        }
-
-        if (StringUtils.isBlank(flowId)) {
-            throw new IllegalArgumentException("Flow Identifier cannot be blank");
-        }
-
-        return executeAction("Error retrieving snapshot metadata", () -> {
-            final WebTarget target = bucketFlowSnapshotTarget
-                    .resolveTemplate("bucketId", bucketId)
-                    .resolveTemplate("flowId", flowId);
-
-            final VersionedFlowSnapshotMetadata[] snapshots = getRequestBuilder(target)
-                    .get(VersionedFlowSnapshotMetadata[].class);
-
-            return snapshots == null ? Collections.emptyList() : Arrays.asList(snapshots);
-        });
-    }
-
-    @Override
-    @SuppressWarnings("unchecked")
-    public List<VersionedFlowSnapshotMetadata> getSnapshotMetadata(final String flowId)
-            throws NiFiRegistryException, IOException {
-
-        if (StringUtils.isBlank(flowId)) {
-            throw new IllegalArgumentException("Flow Identifier cannot be blank");
-        }
-
-        return executeAction("Error retrieving snapshot metadata", () -> {
-            final WebTarget target = flowsFlowSnapshotTarget
-                    .resolveTemplate("flowId", flowId);
-
-            final VersionedFlowSnapshotMetadata[] snapshots = getRequestBuilder(target)
-                    .get(VersionedFlowSnapshotMetadata[].class);
-
-            return snapshots == null ? Collections.emptyList() : Arrays.asList(snapshots);
-        });
-    }
-
-}

http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/6f26290d/nifi-registry-client/src/main/java/org/apache/nifi/registry/client/impl/JerseyItemsClient.java
----------------------------------------------------------------------
diff --git a/nifi-registry-client/src/main/java/org/apache/nifi/registry/client/impl/JerseyItemsClient.java b/nifi-registry-client/src/main/java/org/apache/nifi/registry/client/impl/JerseyItemsClient.java
deleted file mode 100644
index 6b01fc4..0000000
--- a/nifi-registry-client/src/main/java/org/apache/nifi/registry/client/impl/JerseyItemsClient.java
+++ /dev/null
@@ -1,85 +0,0 @@
-/*
- * 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.client.impl;
-
-import org.apache.commons.lang3.StringUtils;
-import org.apache.nifi.registry.bucket.BucketItem;
-import org.apache.nifi.registry.client.ItemsClient;
-import org.apache.nifi.registry.client.NiFiRegistryException;
-import org.apache.nifi.registry.field.Fields;
-
-import javax.ws.rs.client.WebTarget;
-import java.io.IOException;
-import java.util.Arrays;
-import java.util.Collections;
-import java.util.List;
-import java.util.Map;
-
-/**
- * Jersey implementation of ItemsClient.
- */
-public class JerseyItemsClient extends AbstractJerseyClient implements ItemsClient {
-
-    private final WebTarget itemsTarget;
-
-    public JerseyItemsClient(final WebTarget baseTarget) {
-        this(baseTarget, Collections.emptyMap());
-    }
-
-    public JerseyItemsClient(final WebTarget baseTarget, final Map<String,String> headers) {
-        super(headers);
-        this.itemsTarget = baseTarget.path("/items");
-    }
-
-
-
-    @Override
-    public List<BucketItem> getAll() throws NiFiRegistryException, IOException {
-        return executeAction("", () -> {
-            WebTarget target = itemsTarget;
-            final BucketItem[] bucketItems = getRequestBuilder(target).get(BucketItem[].class);
-            return bucketItems == null ? Collections.emptyList() : Arrays.asList(bucketItems);
-        });
-    }
-
-    @Override
-    public List<BucketItem> getByBucket(final String bucketId)
-            throws NiFiRegistryException, IOException {
-        if (StringUtils.isBlank(bucketId)) {
-            throw new IllegalArgumentException("Bucket Identifier cannot be blank");
-        }
-
-        return executeAction("", () -> {
-            WebTarget target = itemsTarget
-                    .path("/{bucketId}")
-                    .resolveTemplate("bucketId", bucketId);
-
-            final BucketItem[] bucketItems = getRequestBuilder(target).get(BucketItem[].class);
-            return bucketItems == null ? Collections.emptyList() : Arrays.asList(bucketItems);
-        });
-    }
-
-    @Override
-    public Fields getFields() throws NiFiRegistryException, IOException {
-        return executeAction("", () -> {
-            final WebTarget target = itemsTarget.path("/fields");
-            return getRequestBuilder(target).get(Fields.class);
-
-        });
-    }
-
-}

http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/6f26290d/nifi-registry-client/src/main/java/org/apache/nifi/registry/client/impl/JerseyNiFiRegistryClient.java
----------------------------------------------------------------------
diff --git a/nifi-registry-client/src/main/java/org/apache/nifi/registry/client/impl/JerseyNiFiRegistryClient.java b/nifi-registry-client/src/main/java/org/apache/nifi/registry/client/impl/JerseyNiFiRegistryClient.java
deleted file mode 100644
index 329a47a..0000000
--- a/nifi-registry-client/src/main/java/org/apache/nifi/registry/client/impl/JerseyNiFiRegistryClient.java
+++ /dev/null
@@ -1,247 +0,0 @@
-/*
- * 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.client.impl;
-
-import com.fasterxml.jackson.annotation.JsonInclude;
-import com.fasterxml.jackson.databind.DeserializationFeature;
-import com.fasterxml.jackson.databind.ObjectMapper;
-import com.fasterxml.jackson.databind.module.SimpleModule;
-import com.fasterxml.jackson.module.jaxb.JaxbAnnotationIntrospector;
-import org.apache.commons.lang3.StringUtils;
-import org.apache.nifi.registry.bucket.BucketItem;
-import org.apache.nifi.registry.client.BucketClient;
-import org.apache.nifi.registry.client.FlowClient;
-import org.apache.nifi.registry.client.FlowSnapshotClient;
-import org.apache.nifi.registry.client.ItemsClient;
-import org.apache.nifi.registry.client.NiFiRegistryClient;
-import org.apache.nifi.registry.client.NiFiRegistryClientConfig;
-import org.apache.nifi.registry.client.UserClient;
-import org.apache.nifi.registry.security.util.ProxiedEntitiesUtils;
-import org.glassfish.jersey.client.ClientConfig;
-import org.glassfish.jersey.client.ClientProperties;
-import org.glassfish.jersey.jackson.internal.jackson.jaxrs.json.JacksonJaxbJsonProvider;
-
-import javax.net.ssl.HostnameVerifier;
-import javax.net.ssl.SSLContext;
-import javax.ws.rs.client.Client;
-import javax.ws.rs.client.ClientBuilder;
-import javax.ws.rs.client.WebTarget;
-import java.io.IOException;
-import java.net.URI;
-import java.util.Arrays;
-import java.util.HashMap;
-import java.util.List;
-import java.util.Map;
-import java.util.stream.Collectors;
-
-/**
- * A NiFiRegistryClient that uses Jersey Client.
- */
-public class JerseyNiFiRegistryClient implements NiFiRegistryClient {
-
-    static final String NIFI_REGISTRY_CONTEXT = "nifi-registry-api";
-    static final int DEFAULT_CONNECT_TIMEOUT = 10000;
-    static final int DEFAULT_READ_TIMEOUT = 10000;
-
-    private final Client client;
-    private final WebTarget baseTarget;
-
-    private final BucketClient bucketClient;
-    private final FlowClient flowClient;
-    private final FlowSnapshotClient flowSnapshotClient;
-    private final ItemsClient itemsClient;
-
-    private JerseyNiFiRegistryClient(final NiFiRegistryClient.Builder builder) {
-        final NiFiRegistryClientConfig registryClientConfig = builder.getConfig();
-        if (registryClientConfig == null) {
-            throw new IllegalArgumentException("NiFiRegistryClientConfig cannot be null");
-        }
-
-        String baseUrl = registryClientConfig.getBaseUrl();
-        if (StringUtils.isBlank(baseUrl)) {
-            throw new IllegalArgumentException("Base URL cannot be blank");
-        }
-
-        if (baseUrl.endsWith("/")) {
-            baseUrl = baseUrl.substring(0, baseUrl.length() - 1);
-        }
-
-        if (!baseUrl.endsWith(NIFI_REGISTRY_CONTEXT)) {
-            baseUrl = baseUrl + "/" + NIFI_REGISTRY_CONTEXT;
-        }
-
-        try {
-            new URI(baseUrl);
-        } catch (final Exception e) {
-            throw new IllegalArgumentException("Invalid base URL: " + e.getMessage(), e);
-        }
-
-        final SSLContext sslContext = registryClientConfig.getSslContext();
-        final HostnameVerifier hostnameVerifier = registryClientConfig.getHostnameVerifier();
-
-        final ClientBuilder clientBuilder = ClientBuilder.newBuilder();
-        if (sslContext != null) {
-            clientBuilder.sslContext(sslContext);
-        }
-        if (hostnameVerifier != null) {
-            clientBuilder.hostnameVerifier(hostnameVerifier);
-        }
-
-        final int connectTimeout = registryClientConfig.getConnectTimeout() == null ? DEFAULT_CONNECT_TIMEOUT : registryClientConfig.getConnectTimeout();
-        final int readTimeout = registryClientConfig.getReadTimeout() == null ? DEFAULT_READ_TIMEOUT : registryClientConfig.getReadTimeout();
-
-        final ClientConfig clientConfig = new ClientConfig();
-        clientConfig.property(ClientProperties.CONNECT_TIMEOUT, connectTimeout);
-        clientConfig.property(ClientProperties.READ_TIMEOUT, readTimeout);
-        clientConfig.register(jacksonJaxbJsonProvider());
-        clientBuilder.withConfig(clientConfig);
-        this.client = clientBuilder.build();
-
-        this.baseTarget = client.target(baseUrl);
-        this.bucketClient = new JerseyBucketClient(baseTarget);
-        this.flowClient = new JerseyFlowClient(baseTarget);
-        this.flowSnapshotClient = new JerseyFlowSnapshotClient(baseTarget);
-        this.itemsClient = new JerseyItemsClient(baseTarget);
-    }
-
-    @Override
-    public BucketClient getBucketClient() {
-        return this.bucketClient;
-    }
-
-    @Override
-    public FlowClient getFlowClient() {
-        return this.flowClient;
-    }
-
-    @Override
-    public FlowSnapshotClient getFlowSnapshotClient() {
-        return this.flowSnapshotClient;
-    }
-
-    @Override
-    public ItemsClient getItemsClient() {
-        return this.itemsClient;
-    }
-
-    @Override
-    public BucketClient getBucketClient(String... proxiedEntity) {
-        final Map<String,String> headers = getHeaders(proxiedEntity);
-        return new JerseyBucketClient(baseTarget, headers);
-    }
-
-    @Override
-    public FlowClient getFlowClient(String... proxiedEntity) {
-        final Map<String,String> headers = getHeaders(proxiedEntity);
-        return new JerseyFlowClient(baseTarget, headers);
-    }
-
-    @Override
-    public FlowSnapshotClient getFlowSnapshotClient(String... proxiedEntity) {
-        final Map<String,String> headers = getHeaders(proxiedEntity);
-        return new JerseyFlowSnapshotClient(baseTarget, headers);
-    }
-
-    @Override
-    public ItemsClient getItemsClient(String... proxiedEntity) {
-        final Map<String,String> headers = getHeaders(proxiedEntity);
-        return new JerseyItemsClient(baseTarget, headers);
-    }
-
-    @Override
-    public UserClient getUserClient() {
-        return new JerseyUserClient(baseTarget);
-    }
-
-    @Override
-    public UserClient getUserClient(String... proxiedEntity) {
-        final Map<String,String> headers = getHeaders(proxiedEntity);
-        return new JerseyUserClient(baseTarget, headers);
-    }
-
-    private Map<String,String> getHeaders(String[] proxiedEntities) {
-        final String proxiedEntitiesValue = getProxiedEntitesValue(proxiedEntities);
-
-        final Map<String,String> headers = new HashMap<>();
-        if (proxiedEntitiesValue != null) {
-            headers.put(ProxiedEntitiesUtils.PROXY_ENTITIES_CHAIN, proxiedEntitiesValue);
-        }
-        return headers;
-    }
-
-    private String getProxiedEntitesValue(String[] proxiedEntities) {
-        if (proxiedEntities == null) {
-            return null;
-        }
-
-        final List<String> proxiedEntityChain = Arrays.stream(proxiedEntities).map(ProxiedEntitiesUtils::formatProxyDn).collect(Collectors.toList());
-        return StringUtils.join(proxiedEntityChain, "");
-    }
-
-    @Override
-    public void close() throws IOException {
-        if (this.client != null) {
-            try {
-                this.client.close();
-            } catch (Exception e) {
-
-            }
-        }
-    }
-
-    /**
-     * Builder for creating a JerseyNiFiRegistryClient.
-     */
-    public static class Builder implements NiFiRegistryClient.Builder {
-
-        private NiFiRegistryClientConfig clientConfig;
-
-        @Override
-        public Builder config(final NiFiRegistryClientConfig clientConfig) {
-            this.clientConfig = clientConfig;
-            return this;
-        }
-
-        @Override
-        public NiFiRegistryClientConfig getConfig() {
-            return clientConfig;
-        }
-
-        @Override
-        public NiFiRegistryClient build() {
-            return new JerseyNiFiRegistryClient(this);
-        }
-
-    }
-
-    private static JacksonJaxbJsonProvider jacksonJaxbJsonProvider() {
-        JacksonJaxbJsonProvider jacksonJaxbJsonProvider = new JacksonJaxbJsonProvider();
-
-        ObjectMapper mapper = new ObjectMapper();
-        mapper.setPropertyInclusion(JsonInclude.Value.construct(JsonInclude.Include.NON_NULL, JsonInclude.Include.NON_NULL));
-        mapper.setAnnotationIntrospector(new JaxbAnnotationIntrospector(mapper.getTypeFactory()));
-        // Ignore unknown properties so that deployed client remain compatible with future versions of NiFi Registry that add new fields
-        mapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
-
-        SimpleModule module = new SimpleModule();
-        module.addDeserializer(BucketItem[].class, new BucketItemDeserializer());
-        mapper.registerModule(module);
-
-        jacksonJaxbJsonProvider.setMapper(mapper);
-        return jacksonJaxbJsonProvider;
-    }
-}

http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/6f26290d/nifi-registry-client/src/main/java/org/apache/nifi/registry/client/impl/JerseyUserClient.java
----------------------------------------------------------------------
diff --git a/nifi-registry-client/src/main/java/org/apache/nifi/registry/client/impl/JerseyUserClient.java b/nifi-registry-client/src/main/java/org/apache/nifi/registry/client/impl/JerseyUserClient.java
deleted file mode 100644
index 7625f35..0000000
--- a/nifi-registry-client/src/main/java/org/apache/nifi/registry/client/impl/JerseyUserClient.java
+++ /dev/null
@@ -1,47 +0,0 @@
-/*
- * 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.client.impl;
-
-import org.apache.nifi.registry.client.NiFiRegistryException;
-import org.apache.nifi.registry.client.UserClient;
-import org.apache.nifi.registry.authorization.CurrentUser;
-
-import javax.ws.rs.client.WebTarget;
-import java.io.IOException;
-import java.util.Collections;
-import java.util.Map;
-
-public class JerseyUserClient extends AbstractJerseyClient implements UserClient {
-
-    private final WebTarget accessTarget;
-
-    public JerseyUserClient(final WebTarget baseTarget) {
-        this(baseTarget, Collections.emptyMap());
-    }
-
-    public JerseyUserClient(final WebTarget baseTarget, final Map<String,String> headers) {
-        super(headers);
-        this.accessTarget = baseTarget.path("/access");
-    }
-
-    @Override
-    public CurrentUser getAccessStatus() throws NiFiRegistryException, IOException {
-        return executeAction("Error retrieving access status for the current user", () -> {
-            return getRequestBuilder(accessTarget).get(CurrentUser.class);
-        });
-    }
-}

http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/6f26290d/nifi-registry-core/nifi-registry-bootstrap/pom.xml
----------------------------------------------------------------------
diff --git a/nifi-registry-core/nifi-registry-bootstrap/pom.xml b/nifi-registry-core/nifi-registry-bootstrap/pom.xml
new file mode 100644
index 0000000..af0ad05
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-bootstrap/pom.xml
@@ -0,0 +1,40 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 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. -->
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+    <modelVersion>4.0.0</modelVersion>
+    <parent>
+        <groupId>org.apache.nifi.registry</groupId>
+        <artifactId>nifi-registry-core</artifactId>
+        <version>0.3.0-SNAPSHOT</version>
+    </parent>
+    
+    <artifactId>nifi-registry-bootstrap</artifactId>
+    <packaging>jar</packaging>
+    
+    <dependencies>
+        <dependency>
+            <groupId>org.apache.nifi.registry</groupId>
+            <artifactId>nifi-registry-utils</artifactId>
+            <version>0.3.0-SNAPSHOT</version>
+        </dependency>
+        <dependency>
+            <groupId>org.apache.commons</groupId>
+            <artifactId>commons-lang3</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>net.java.dev.jna</groupId>
+            <artifactId>jna-platform</artifactId>
+            <version>4.4.0</version>
+        </dependency>
+    </dependencies>
+
+</project>


[44/51] [partial] nifi-registry git commit: NIFIREG-201 Refactoring project structure to better isolate extensions

Posted by kd...@apache.org.
http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/6f26290d/nifi-registry-core/nifi-registry-docker/dockerhub/README.md
----------------------------------------------------------------------
diff --git a/nifi-registry-core/nifi-registry-docker/dockerhub/README.md b/nifi-registry-core/nifi-registry-docker/dockerhub/README.md
new file mode 100644
index 0000000..4374d02
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-docker/dockerhub/README.md
@@ -0,0 +1,148 @@
+<!--
+  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.
+-->
+
+# Docker Image Quickstart
+
+## Capabilities
+This image currently supports running in standalone mode either unsecured or with user authentication provided through:
+   * [Two-Way SSL with Client Certificates](https://nifi.apache.org/docs/nifi-registry-docs/html/administration-guide.html#security-configuration)
+   * [Lightweight Directory Access Protocol (LDAP)](https://nifi.apache.org/docs/nifi-registry-docs/html/administration-guide.html#ldap_identity_provider)
+   
+## Building
+The Docker image can be built using the following command:
+
+    # user @ puter in ~/path/to/apache/nifi-registry/nifi-registry-docker/dockerhub    
+    $ docker build -t apache/nifi-registry:latest .
+
+This will result in an image tagged apache/nifi:latest
+
+    $ docker images
+    > REPOSITORY               TAG           IMAGE ID            CREATED                  SIZE
+    > apache/nifi-registry     latest        751428cbf631        A long, long time ago    342MB
+    
+**Note**: The default version of NiFi Registry specified by the Dockerfile is typically that of one that is unreleased if working from source.
+To build an image for a prior released version, one can override the `NIFI_REGISTRY_VERSION` build-arg with the following command:
+    
+    $ docker build --build-arg NIFI_REGISTRY_VERSION={Desired NiFi Registry Version} -t apache/nifi-registry:latest .
+
+There is, however, no guarantee that older versions will work as properties have changed and evolved with subsequent releases.
+The configuration scripts are suitable for at least 0.1.0+.
+
+## Running a container
+
+### Standalone Instance, Unsecured
+The minimum to run a NiFi Registry instance is as follows:
+
+    docker run --name nifi-registry \
+      -p 18080:18080 \
+      -d \
+      apache/nifi-registry:latest
+      
+This will provide a running instance, exposing the instance UI to the host system on at port 18080,
+viewable at `http://localhost:18080/nifi-registry`.
+
+You can also pass in environment variables to change the NiFi Registry communication ports and hostname using the Docker '-e' switch as follows:
+
+    docker run --name nifi-registry \
+      -p 19090:19090 \
+      -d \
+      -e NIFI_REGISTRY_WEB_HTTP_PORT='19090'
+      apache/nifi-registry:latest
+
+For a list of the environment variables recognised in this build, look into the .sh/secure.sh and .sh/start.sh scripts
+        
+### Standalone Instance, Two-Way SSL
+In this configuration, the user will need to provide certificates and the associated configuration information.
+Of particular note, is the `AUTH` environment variable which is set to `tls`.  Additionally, the user must provide an
+the DN as provided by an accessing client certificate in the `INITIAL_ADMIN_IDENTITY` environment variable.
+This value will be used to seed the instance with an initial user with administrative privileges.
+Finally, this command makes use of a volume to provide certificates on the host system to the container instance.
+
+    docker run --name nifi-registry \
+      -v /path/to/tls/certs/localhost:/opt/certs \
+      -p 18443:18443 \
+      -e AUTH=tls \
+      -e KEYSTORE_PATH=/opt/certs/keystore.jks \
+      -e KEYSTORE_TYPE=JKS \
+      -e KEYSTORE_PASSWORD=QKZv1hSWAFQYZ+WU1jjF5ank+l4igeOfQRp+OSbkkrs \
+      -e TRUSTSTORE_PATH=/opt/certs/truststore.jks \
+      -e TRUSTSTORE_PASSWORD=rHkWR1gDNW3R9hgbeRsT3OM3Ue0zwGtQqcFKJD2EXWE \
+      -e TRUSTSTORE_TYPE=JKS \
+      -e INITIAL_ADMIN_IDENTITY='CN=AdminUser, OU=nifi' \
+      -d \
+      apache/nifi-registry:latest
+
+### Standalone Instance, LDAP
+In this configuration, the user will need to provide certificates and the associated configuration information.  Optionally,
+if the LDAP provider of interest is operating in LDAPS or START_TLS modes, certificates will additionally be needed.
+Of particular note, is the `AUTH` environment variable which is set to `ldap`.  Additionally, the user must provide a
+DN as provided by the configured LDAP server in the `INITIAL_ADMIN_IDENTITY` environment variable. This value will be 
+used to seed the instance with an initial user with administrative privileges.  Finally, this command makes use of a 
+volume to provide certificates on the host system to the container instance.
+
+#### For a minimal, connection to an LDAP server using SIMPLE authentication:
+
+    docker run --name nifi-registry \
+      -v /path/to/tls/certs/localhost:/opt/certs \
+      -p 18443:18443 \
+      -e AUTH=ldap \
+      -e KEYSTORE_PATH=/opt/certs/keystore.jks \
+      -e KEYSTORE_TYPE=JKS \
+      -e KEYSTORE_PASSWORD=QKZv1hSWAFQYZ+WU1jjF5ank+l4igeOfQRp+OSbkkrs \
+      -e TRUSTSTORE_PATH=/opt/certs/truststore.jks \
+      -e TRUSTSTORE_PASSWORD=rHkWR1gDNW3R9hgbeRsT3OM3Ue0zwGtQqcFKJD2EXWE \
+      -e TRUSTSTORE_TYPE=JKS \
+      -e INITIAL_ADMIN_IDENTITY='cn=nifi-admin,dc=example,dc=org' \
+      -e LDAP_AUTHENTICATION_STRATEGY='SIMPLE' \
+      -e LDAP_MANAGER_DN='cn=ldap-admin,dc=example,dc=org' \
+      -e LDAP_MANAGER_PASSWORD='password' \
+      -e LDAP_USER_SEARCH_BASE='dc=example,dc=org' \
+      -e LDAP_USER_SEARCH_FILTER='cn={0}' \
+      -e LDAP_IDENTITY_STRATEGY='USE_DN' \
+      -e LDAP_URL='ldap://ldap:389' \
+      -d \
+      apache/nifi-registry:latest
+
+#### The following, optional environment variables may be added to the above command when connecting to a secure  LDAP server configured with START_TLS or LDAPS
+
+    -e LDAP_TLS_KEYSTORE: ''
+    -e LDAP_TLS_KEYSTORE_PASSWORD: ''
+    -e LDAP_TLS_KEYSTORE_TYPE: ''
+    -e LDAP_TLS_TRUSTSTORE: ''
+    -e LDAP_TLS_TRUSTSTORE_PASSWORD: ''
+    -e LDAP_TLS_TRUSTSTORE_TYPE: ''
+
+### The following, optional environment variables can be used to configure the database
+
+| nifi-registry.properties entry         | Variable                   |
+|----------------------------------------|----------------------------|
+| nifi.registry.db.url                   | NIFI_REGISTRY_DB_URL       |
+| nifi.registry.db.driver.class          | NIFI_REGISTRY_DB_CLASS     |
+| nifi.registry.db.driver.directory      | NIFI_REGISTRY_DB_DIR       |
+| nifi.registry.db.driver.username       | NIFI_REGISTRY_DB_USER      |
+| nifi.registry.db.driver.password       | NIFI_REGISTRY_DB_PASS      |
+| nifi.registry.db.driver.maxConnections | NIFI_REGISTRY_DB_MAX_CONNS |
+| nifi.registry.db.sql.debug             | NIFI_REGISTRY_DB_DEBUG_SQL |
+
+#### The following, optional environment variables may be added to configure flow persistence provider.
+
+| Environment Variable           | Configuration Property               |
+|--------------------------------|--------------------------------------|
+| NIFI_REGISTRY_FLOW_STORAGE_DIR | Flow Storage Directory               |
+| NIFI_REGISTRY_FLOW_PROVIDER    | (Class tag); valid values: git, file |
+| NIFI_REGISTRY_GIT_REMOTE       | Remote to Push                       |
+| NIFI_REGISTRY_GIT_USER         | Remote Access User                   |
+| NIFI_REGISTRY_GIT_PASSWORD     | Remote Access Password               |
+

http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/6f26290d/nifi-registry-core/nifi-registry-docker/dockerhub/sh/common.sh
----------------------------------------------------------------------
diff --git a/nifi-registry-core/nifi-registry-docker/dockerhub/sh/common.sh b/nifi-registry-core/nifi-registry-docker/dockerhub/sh/common.sh
new file mode 100755
index 0000000..0f594d9
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-docker/dockerhub/sh/common.sh
@@ -0,0 +1,28 @@
+#!/bin/sh -e
+#    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.
+
+# 1 - value to search for
+# 2 - value to replace
+# 3 - file to perform replacement inline
+prop_replace () {
+  target_file=${3:-${nifi_registry_props_file}}
+  echo 'replacing target file ' ${target_file}
+  sed -i -e "s|^$1=.*$|$1=$2|"  ${target_file}
+}
+
+# NIFI_REGISTRY_HOME is defined by an ENV command in the backing Dockerfile
+export nifi_registry_props_file=${NIFI_REGISTRY_HOME}/conf/nifi-registry.properties
+export hostname=$(hostname)

http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/6f26290d/nifi-registry-core/nifi-registry-docker/dockerhub/sh/secure.sh
----------------------------------------------------------------------
diff --git a/nifi-registry-core/nifi-registry-docker/dockerhub/sh/secure.sh b/nifi-registry-core/nifi-registry-docker/dockerhub/sh/secure.sh
new file mode 100644
index 0000000..352dfad
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-docker/dockerhub/sh/secure.sh
@@ -0,0 +1,56 @@
+#!/bin/sh -e
+
+#    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.
+
+scripts_dir='/opt/nifi-registry/scripts'
+
+[ -f "${scripts_dir}/common.sh" ] && . "${scripts_dir}/common.sh"
+
+# Perform idempotent changes of configuration to support secure environments
+echo 'Configuring environment with SSL settings'
+
+: ${KEYSTORE_PATH:?"Must specify an absolute path to the keystore being used."}
+if [ ! -f "${KEYSTORE_PATH}" ]; then
+    echo "Keystore file specified (${KEYSTORE_PATH}) does not exist."
+    exit 1
+fi
+: ${KEYSTORE_TYPE:?"Must specify the type of keystore (JKS, PKCS12, PEM) of the keystore being used."}
+: ${KEYSTORE_PASSWORD:?"Must specify the password of the keystore being used."}
+
+: ${TRUSTSTORE_PATH:?"Must specify an absolute path to the truststore being used."}
+if [ ! -f "${TRUSTSTORE_PATH}" ]; then
+    echo "Keystore file specified (${TRUSTSTORE_PATH}) does not exist."
+    exit 1
+fi
+: ${TRUSTSTORE_TYPE:?"Must specify the type of truststore (JKS, PKCS12, PEM) of the truststore being used."}
+: ${TRUSTSTORE_PASSWORD:?"Must specify the password of the truststore being used."}
+
+prop_replace 'nifi.registry.security.keystore'           "${KEYSTORE_PATH}"
+prop_replace 'nifi.registry.security.keystoreType'       "${KEYSTORE_TYPE}"
+prop_replace 'nifi.registry.security.keystorePasswd'     "${KEYSTORE_PASSWORD}"
+prop_replace 'nifi.registry.security.truststore'         "${TRUSTSTORE_PATH}"
+prop_replace 'nifi.registry.security.truststoreType'     "${TRUSTSTORE_TYPE}"
+prop_replace 'nifi.registry.security.truststorePasswd'   "${TRUSTSTORE_PASSWORD}"
+
+# Disable HTTP and enable HTTPS
+prop_replace 'nifi.registry.web.http.port'   ''
+prop_replace 'nifi.registry.web.http.host'   ''
+prop_replace 'nifi.registry.web.https.port'  "${NIFI_REGISTRY_WEB_HTTPS_PORT:-18443}"
+prop_replace 'nifi.registry.web.https.host'  "${NIFI_REGISTRY_WEB_HTTPS_HOST:-$HOSTNAME}"
+
+# Establish initial user and an associated admin identity
+sed -i -e 's|<property name="Initial User Identity 1">.*</property>|<property name="Initial User Identity 1">'"${INITIAL_ADMIN_IDENTITY}"'</property>|'  ${NIFI_REGISTRY_HOME}/conf/authorizers.xml
+sed -i -e 's|<property name="Initial Admin Identity">.*</property>|<property name="Initial Admin Identity">'"${INITIAL_ADMIN_IDENTITY}"'</property>|'  ${NIFI_REGISTRY_HOME}/conf/authorizers.xml

http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/6f26290d/nifi-registry-core/nifi-registry-docker/dockerhub/sh/start.sh
----------------------------------------------------------------------
diff --git a/nifi-registry-core/nifi-registry-docker/dockerhub/sh/start.sh b/nifi-registry-core/nifi-registry-docker/dockerhub/sh/start.sh
new file mode 100755
index 0000000..d281490
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-docker/dockerhub/sh/start.sh
@@ -0,0 +1,55 @@
+#!/bin/sh -e
+
+#    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.
+
+scripts_dir='/opt/nifi-registry/scripts'
+
+[ -f "${scripts_dir}/common.sh" ] && . "${scripts_dir}/common.sh"
+
+# Establish baseline properties
+prop_replace 'nifi.registry.web.http.port'      "${NIFI_REGISTRY_WEB_HTTP_PORT:-18080}"
+prop_replace 'nifi.registry.web.http.host'      "${NIFI_REGISTRY_WEB_HTTP_HOST:-$HOSTNAME}"
+
+. ${scripts_dir}/update_database.sh
+
+# Check if we are secured or unsecured
+case ${AUTH} in
+    tls)
+        echo 'Enabling Two-Way SSL user authentication'
+        . "${scripts_dir}/secure.sh"
+        ;;
+    ldap)
+        echo 'Enabling LDAP user authentication'
+        # Reference ldap-provider in properties
+        prop_replace 'nifi.registry.security.identity.provider' 'ldap-identity-provider'
+        prop_replace 'nifi.registry.security.needClientAuth' 'false'
+
+        . "${scripts_dir}/secure.sh"
+        . "${scripts_dir}/update_login_providers.sh"
+        ;;
+esac
+
+. "${scripts_dir}/update_flow_provider.sh"
+
+# Continuously provide logs so that 'docker logs' can produce them
+tail -F "${NIFI_REGISTRY_HOME}/logs/nifi-registry-app.log" &
+"${NIFI_REGISTRY_HOME}/bin/nifi-registry.sh" run &
+nifi_registry_pid="$!"
+
+trap "echo Received trapped signal, beginning shutdown...;" KILL TERM HUP INT EXIT;
+
+echo NiFi-Registry running with PID ${nifi_registry_pid}.
+wait ${nifi_registry_pid}
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/6f26290d/nifi-registry-core/nifi-registry-docker/dockerhub/sh/update_database.sh
----------------------------------------------------------------------
diff --git a/nifi-registry-core/nifi-registry-docker/dockerhub/sh/update_database.sh b/nifi-registry-core/nifi-registry-docker/dockerhub/sh/update_database.sh
new file mode 100644
index 0000000..c1c3c6f
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-docker/dockerhub/sh/update_database.sh
@@ -0,0 +1,24 @@
+#!/bin/sh -e
+
+#    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.
+
+prop_replace 'nifi.registry.db.url'                         "${NIFI_REGISTRY_DB_URL:-jdbc:h2:./database/nifi-registry-primary;AUTOCOMMIT=OFF;DB_CLOSE_ON_EXIT=FALSE;LOCK_MODE=3;LOCK_TIMEOUT=25000;WRITE_DELAY=0;AUTO_SERVER=FALSE}"
+prop_replace 'nifi.registry.db.driver.class'                "${NIFI_REGISTRY_DB_CLASS:-org.h2.Driver}"
+prop_replace 'nifi.registry.db.driver.directory'            "${NIFI_REGISTRY_DB_DIR:-}"
+prop_replace 'nifi.registry.db.driver.username'             "${NIFI_REGISTRY_DB_USER:-nifireg}"
+prop_replace 'nifi.registry.db.driver.password'             "${NIFI_REGISTRY_DB_PASS:-nifireg}"
+prop_replace 'nifi.registry.db.driver.maxConnections'       "${NIFI_REGISTRY_DB_MAX_CONNS:-5}"
+prop_replace 'nifi.registry.db.sql.debug'                   "${NIFI_REGISTRY_DB_DEBUG_SQL:-false}"
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/6f26290d/nifi-registry-core/nifi-registry-docker/dockerhub/sh/update_flow_provider.sh
----------------------------------------------------------------------
diff --git a/nifi-registry-core/nifi-registry-docker/dockerhub/sh/update_flow_provider.sh b/nifi-registry-core/nifi-registry-docker/dockerhub/sh/update_flow_provider.sh
new file mode 100644
index 0000000..79afc91
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-docker/dockerhub/sh/update_flow_provider.sh
@@ -0,0 +1,42 @@
+#!/bin/sh -e
+
+#    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.
+
+providers_file=${NIFI_REGISTRY_HOME}/conf/providers.xml
+property_xpath='/providers/flowPersistenceProvider'
+
+add_property() {
+  property_name=$1
+  property_value=$2
+
+  if [ -n "${property_value}" ]; then
+    xmlstarlet ed --subnode "/providers/flowPersistenceProvider" --type elem -n property -v "${property_value}" providers.xml | xmlstarlet ed --subnode "/providers/flowPersistenceProvider/property[not(name)]" --type attr -n name -v "${property_name}"
+  fi
+}
+
+xmlstarlet ed --inplace -u "${property_xpath}/property[@name='Flow Storage Directory']" -v "${NIFI_REGISTRY_FLOW_STORAGE_DIR:-./flow_storage}" "${providers_file}"
+
+case ${NIFI_REGISTRY_FLOW_PROVIDER} in
+    file)
+        xmlstarlet ed --inplace -u "${property_xpath}/class" -v "org.apache.nifi.registry.provider.flow.FileSystemFlowPersistenceProvider" "${providers_file}"
+        ;;
+    git)
+        xmlstarlet ed --inplace -u "${property_xpath}/class" -v "org.apache.nifi.registry.provider.flow.git.GitFlowPersistenceProvider" "${providers_file}"
+        add_property "Remote To Push"  "${NIFI_REGISTRY_GIT_REMOTE:-}"
+        add_property "Remote Access User"  "${NIFI_REGISTRY_GIT_USER:-}"
+        add_property "Remote Access Password"    "${NIFI_REGISTRY_GIT_PASSWORD:-}"
+        ;;
+esac
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/6f26290d/nifi-registry-core/nifi-registry-docker/dockerhub/sh/update_login_providers.sh
----------------------------------------------------------------------
diff --git a/nifi-registry-core/nifi-registry-docker/dockerhub/sh/update_login_providers.sh b/nifi-registry-core/nifi-registry-docker/dockerhub/sh/update_login_providers.sh
new file mode 100755
index 0000000..e3280b5
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-docker/dockerhub/sh/update_login_providers.sh
@@ -0,0 +1,47 @@
+#!/bin/sh -e
+
+#    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.
+
+login_providers_file=${NIFI_REGISTRY_HOME}/conf/identity-providers.xml
+property_xpath='//identityProviders/provider/property'
+
+# Update a given property in the login-identity-providers file if a value is specified
+edit_property() {
+  property_name=$1
+  property_value=$2
+
+  if [ -n "${property_value}" ]; then
+    xmlstarlet ed --inplace -u "${property_xpath}[@name='${property_name}']" -v "${property_value}" "${login_providers_file}"
+  fi
+}
+
+# Remove comments to enable the ldap-provider
+sed -i '/To enable the ldap-identity-provider remove/d' "${login_providers_file}"
+
+edit_property 'Authentication Strategy'     "${LDAP_AUTHENTICATION_STRATEGY}"
+edit_property 'Manager DN'                  "${LDAP_MANAGER_DN}"
+edit_property 'Manager Password'            "${LDAP_MANAGER_PASSWORD}"
+edit_property 'TLS - Keystore'              "${LDAP_TLS_KEYSTORE}"
+edit_property 'TLS - Keystore Password'     "${LDAP_TLS_KEYSTORE_PASSWORD}"
+edit_property 'TLS - Keystore Type'         "${LDAP_TLS_KEYSTORE_TYPE}"
+edit_property 'TLS - Truststore'            "${LDAP_TLS_TRUSTSTORE}"
+edit_property 'TLS - Truststore Password'   "${LDAP_TLS_TRUSTSTORE_PASSWORD}"
+edit_property 'TLS - Truststore Type'       "${LDAP_TLS_TRUSTSTORE_TYPE}"
+edit_property 'TLS - Protocol'              "${LDAP_TLS_PROTOCOL}"
+edit_property 'Url'                         "${LDAP_URL}"
+edit_property 'User Search Base'            "${LDAP_USER_SEARCH_BASE}"
+edit_property 'User Search Filter'          "${LDAP_USER_SEARCH_FILTER}"
+edit_property 'Identity Strategy'           "${LDAP_IDENTITY_STRATEGY}"

http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/6f26290d/nifi-registry-core/nifi-registry-docker/pom.xml
----------------------------------------------------------------------
diff --git a/nifi-registry-core/nifi-registry-docker/pom.xml b/nifi-registry-core/nifi-registry-docker/pom.xml
new file mode 100644
index 0000000..13628d8
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-docker/pom.xml
@@ -0,0 +1,27 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+  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.
+-->
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+    <parent>
+        <artifactId>nifi-registry-core</artifactId>
+        <groupId>org.apache.nifi.registry</groupId>
+        <version>0.3.0-SNAPSHOT</version>
+    </parent>
+    <modelVersion>4.0.0</modelVersion>
+
+    <artifactId>nifi-registry-docker</artifactId>
+
+
+</project>

http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/6f26290d/nifi-registry-core/nifi-registry-docs/LICENSE
----------------------------------------------------------------------
diff --git a/nifi-registry-core/nifi-registry-docs/LICENSE b/nifi-registry-core/nifi-registry-docs/LICENSE
new file mode 100644
index 0000000..f6dd49c
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-docs/LICENSE
@@ -0,0 +1,235 @@
+
+                                 Apache License
+                           Version 2.0, January 2004
+                        http://www.apache.org/licenses/
+
+   TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+   1. Definitions.
+
+      "License" shall mean the terms and conditions for use, reproduction,
+      and distribution as defined by Sections 1 through 9 of this document.
+
+      "Licensor" shall mean the copyright owner or entity authorized by
+      the copyright owner that is granting the License.
+
+      "Legal Entity" shall mean the union of the acting entity and all
+      other entities that control, are controlled by, or are under common
+      control with that entity. For the purposes of this definition,
+      "control" means (i) the power, direct or indirect, to cause the
+      direction or management of such entity, whether by contract or
+      otherwise, or (ii) ownership of fifty percent (50%) or more of the
+      outstanding shares, or (iii) beneficial ownership of such entity.
+
+      "You" (or "Your") shall mean an individual or Legal Entity
+      exercising permissions granted by this License.
+
+      "Source" form shall mean the preferred form for making modifications,
+      including but not limited to software source code, documentation
+      source, and configuration files.
+
+      "Object" form shall mean any form resulting from mechanical
+      transformation or translation of a Source form, including but
+      not limited to compiled object code, generated documentation,
+      and conversions to other media types.
+
+      "Work" shall mean the work of authorship, whether in Source or
+      Object form, made available under the License, as indicated by a
+      copyright notice that is included in or attached to the work
+      (an example is provided in the Appendix below).
+
+      "Derivative Works" shall mean any work, whether in Source or Object
+      form, that is based on (or derived from) the Work and for which the
+      editorial revisions, annotations, elaborations, or other modifications
+      represent, as a whole, an original work of authorship. For the purposes
+      of this License, Derivative Works shall not include works that remain
+      separable from, or merely link (or bind by name) to the interfaces of,
+      the Work and Derivative Works thereof.
+
+      "Contribution" shall mean any work of authorship, including
+      the original version of the Work and any modifications or additions
+      to that Work or Derivative Works thereof, that is intentionally
+      submitted to Licensor for inclusion in the Work by the copyright owner
+      or by an individual or Legal Entity authorized to submit on behalf of
+      the copyright owner. For the purposes of this definition, "submitted"
+      means any form of electronic, verbal, or written communication sent
+      to the Licensor or its representatives, including but not limited to
+      communication on electronic mailing lists, source code control systems,
+      and issue tracking systems that are managed by, or on behalf of, the
+      Licensor for the purpose of discussing and improving the Work, but
+      excluding communication that is conspicuously marked or otherwise
+      designated in writing by the copyright owner as "Not a Contribution."
+
+      "Contributor" shall mean Licensor and any individual or Legal Entity
+      on behalf of whom a Contribution has been received by Licensor and
+      subsequently incorporated within the Work.
+
+   2. Grant of Copyright License. Subject to the terms and conditions of
+      this License, each Contributor hereby grants to You a perpetual,
+      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+      copyright license to reproduce, prepare Derivative Works of,
+      publicly display, publicly perform, sublicense, and distribute the
+      Work and such Derivative Works in Source or Object form.
+
+   3. Grant of Patent License. Subject to the terms and conditions of
+      this License, each Contributor hereby grants to You a perpetual,
+      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+      (except as stated in this section) patent license to make, have made,
+      use, offer to sell, sell, import, and otherwise transfer the Work,
+      where such license applies only to those patent claims licensable
+      by such Contributor that are necessarily infringed by their
+      Contribution(s) alone or by combination of their Contribution(s)
+      with the Work to which such Contribution(s) was submitted. If You
+      institute patent litigation against any entity (including a
+      cross-claim or counterclaim in a lawsuit) alleging that the Work
+      or a Contribution incorporated within the Work constitutes direct
+      or contributory patent infringement, then any patent licenses
+      granted to You under this License for that Work shall terminate
+      as of the date such litigation is filed.
+
+   4. Redistribution. You may reproduce and distribute copies of the
+      Work or Derivative Works thereof in any medium, with or without
+      modifications, and in Source or Object form, provided that You
+      meet the following conditions:
+
+      (a) You must give any other recipients of the Work or
+          Derivative Works a copy of this License; and
+
+      (b) You must cause any modified files to carry prominent notices
+          stating that You changed the files; and
+
+      (c) You must retain, in the Source form of any Derivative Works
+          that You distribute, all copyright, patent, trademark, and
+          attribution notices from the Source form of the Work,
+          excluding those notices that do not pertain to any part of
+          the Derivative Works; and
+
+      (d) If the Work includes a "NOTICE" text file as part of its
+          distribution, then any Derivative Works that You distribute must
+          include a readable copy of the attribution notices contained
+          within such NOTICE file, excluding those notices that do not
+          pertain to any part of the Derivative Works, in at least one
+          of the following places: within a NOTICE text file distributed
+          as part of the Derivative Works; within the Source form or
+          documentation, if provided along with the Derivative Works; or,
+          within a display generated by the Derivative Works, if and
+          wherever such third-party notices normally appear. The contents
+          of the NOTICE file are for informational purposes only and
+          do not modify the License. You may add Your own attribution
+          notices within Derivative Works that You distribute, alongside
+          or as an addendum to the NOTICE text from the Work, provided
+          that such additional attribution notices cannot be construed
+          as modifying the License.
+
+      You may add Your own copyright statement to Your modifications and
+      may provide additional or different license terms and conditions
+      for use, reproduction, or distribution of Your modifications, or
+      for any such Derivative Works as a whole, provided Your use,
+      reproduction, and distribution of the Work otherwise complies with
+      the conditions stated in this License.
+
+   5. Submission of Contributions. Unless You explicitly state otherwise,
+      any Contribution intentionally submitted for inclusion in the Work
+      by You to the Licensor shall be under the terms and conditions of
+      this License, without any additional terms or conditions.
+      Notwithstanding the above, nothing herein shall supersede or modify
+      the terms of any separate license agreement you may have executed
+      with Licensor regarding such Contributions.
+
+   6. Trademarks. This License does not grant permission to use the trade
+      names, trademarks, service marks, or product names of the Licensor,
+      except as required for reasonable and customary use in describing the
+      origin of the Work and reproducing the content of the NOTICE file.
+
+   7. Disclaimer of Warranty. Unless required by applicable law or
+      agreed to in writing, Licensor provides the Work (and each
+      Contributor provides its Contributions) on an "AS IS" BASIS,
+      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+      implied, including, without limitation, any warranties or conditions
+      of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+      PARTICULAR PURPOSE. You are solely responsible for determining the
+      appropriateness of using or redistributing the Work and assume any
+      risks associated with Your exercise of permissions under this License.
+
+   8. Limitation of Liability. In no event and under no legal theory,
+      whether in tort (including negligence), contract, or otherwise,
+      unless required by applicable law (such as deliberate and grossly
+      negligent acts) or agreed to in writing, shall any Contributor be
+      liable to You for damages, including any direct, indirect, special,
+      incidental, or consequential damages of any character arising as a
+      result of this License or out of the use or inability to use the
+      Work (including but not limited to damages for loss of goodwill,
+      work stoppage, computer failure or malfunction, or any and all
+      other commercial damages or losses), even if such Contributor
+      has been advised of the possibility of such damages.
+
+   9. Accepting Warranty or Additional Liability. While redistributing
+      the Work or Derivative Works thereof, You may choose to offer,
+      and charge a fee for, acceptance of support, warranty, indemnity,
+      or other liability obligations and/or rights consistent with this
+      License. However, in accepting such obligations, You may act only
+      on Your own behalf and on Your sole responsibility, not on behalf
+      of any other Contributor, and only if You agree to indemnify,
+      defend, and hold each Contributor harmless for any liability
+      incurred by, or claims asserted against, such Contributor by reason
+      of your accepting any such warranty or additional liability.
+
+   END OF TERMS AND CONDITIONS
+
+   APPENDIX: How to apply the Apache License to your work.
+
+      To apply the Apache License to your work, attach the following
+      boilerplate notice, with the fields enclosed by brackets "[]"
+      replaced with your own identifying information. (Don't include
+      the brackets!)  The text should be enclosed in the appropriate
+      comment syntax for the file format. We also recommend that a
+      file or class name and description of purpose be included on the
+      same "printed page" as the copyright notice for easier
+      identification within third-party archives.
+
+   Copyright [yyyy] [name of copyright owner]
+
+   Licensed under the Apache License, Version 2.0 (the "License");
+   you may not use this file except in compliance with the License.
+   You may obtain a copy of the License at
+
+       http://www.apache.org/licenses/LICENSE-2.0
+
+   Unless required by applicable law or agreed to in writing, software
+   distributed under the License is distributed on an "AS IS" BASIS,
+   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+   See the License for the specific language governing permissions and
+   limitations under the License.
+
+APACHE NIFI SUBCOMPONENTS:
+
+The Apache NiFi project contains subcomponents with separate copyright
+notices and license terms. Your use of the source code for the these
+subcomponents is subject to the terms and conditions of the following
+licenses. 
+
+This product bundles source from 'Asciidoctor'. Specifically the 'asciidoc-mod.css'.
+The source is available under an MIT LICENSE.
+
+    Copyright (C) 2012-2015 Dan Allen, Ryan Waldron and the Asciidoctor Project
+
+    Permission is hereby granted, free of charge, to any person obtaining a copy
+    of this software and associated documentation files (the "Software"), to deal
+    in the Software without restriction, including without limitation the rights
+    to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+    copies of the Software, and to permit persons to whom the Software is
+    furnished to do so, subject to the following conditions:
+
+    The above copyright notice and this permission notice shall be included in
+    all copies or substantial portions of the Software.
+
+    THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+    IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+    FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+    AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+    LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+    OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+    THE SOFTWARE.
+
+
+

http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/6f26290d/nifi-registry-core/nifi-registry-docs/NOTICE
----------------------------------------------------------------------
diff --git a/nifi-registry-core/nifi-registry-docs/NOTICE b/nifi-registry-core/nifi-registry-docs/NOTICE
new file mode 100644
index 0000000..42dd6ec
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-docs/NOTICE
@@ -0,0 +1,5 @@
+nifi-registry-docs
+Copyright 2014-2017 The Apache Software Foundation
+
+This product includes software developed at
+The Apache Software Foundation (http://www.apache.org/).

http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/6f26290d/nifi-registry-core/nifi-registry-docs/pom.xml
----------------------------------------------------------------------
diff --git a/nifi-registry-core/nifi-registry-docs/pom.xml b/nifi-registry-core/nifi-registry-docs/pom.xml
new file mode 100644
index 0000000..a06a633
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-docs/pom.xml
@@ -0,0 +1,152 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+  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.
+-->
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+    <modelVersion>4.0.0</modelVersion>
+    <parent>
+        <groupId>org.apache.nifi.registry</groupId>
+        <artifactId>nifi-registry-core</artifactId>
+        <version>0.3.0-SNAPSHOT</version>
+    </parent>
+    <packaging>pom</packaging>
+    <artifactId>nifi-registry-docs</artifactId>
+    <build>
+        <plugins>
+            <plugin>
+                <groupId>org.apache.maven.plugins</groupId>
+                <artifactId>maven-resources-plugin</artifactId>
+                <executions>
+                    <execution>
+                        <id>copy-asciidoc</id>
+                        <phase>generate-resources</phase>
+                        <goals>
+                            <goal>copy-resources</goal>
+                        </goals>
+                        <configuration>
+                            <resources>
+                                <resource>
+                                    <directory>src/main/asciidoc</directory>
+                                </resource>
+                            </resources>
+                            <outputDirectory>${project.build.directory}/asciidoc</outputDirectory>
+                        </configuration>
+                    </execution>
+                </executions>
+            </plugin>            
+            <plugin>
+                <groupId>org.asciidoctor</groupId>
+                <artifactId>asciidoctor-maven-plugin</artifactId>
+                <version>1.5.2</version>
+                <executions>
+                    <execution>
+                        <id>output-html</id>
+                        <phase>prepare-package</phase>
+                        <goals>
+                            <goal>process-asciidoc</goal>
+                        </goals>
+                    </execution>
+                </executions>
+                <configuration>
+                    <sourceDirectory>${project.build.directory}/asciidoc</sourceDirectory>
+                    <backend>html5</backend>
+                    <attributes>
+                        <imagesdir>./images</imagesdir>
+                        <icons>font</icons>
+                        <toc>true</toc>
+                        <docVersion>${project.version}</docVersion>
+                        <sectanchors>true</sectanchors>
+                        <idprefix />
+                        <idseparator>-</idseparator>
+                        <docinfo1>true</docinfo1>
+                        <stylesheet>asciidoc-mod.css</stylesheet>
+                    </attributes>
+                </configuration>
+            </plugin>
+            <!-- This plugin is used to insert the Apache License into the output HMTL because
+            AsciiDoc doesn't appear to provide a mechanism for doing this. -->
+            <plugin>
+                <groupId>com.google.code.maven-replacer-plugin</groupId>
+                <artifactId>replacer</artifactId>
+                <version>1.5.3</version>
+                <executions>
+                    <execution>
+                        <phase>package</phase>
+                        <goals>
+                            <goal>replace</goal>
+                        </goals>
+                    </execution>
+                </executions>
+                <configuration>
+                    <filesToInclude>${project.build.directory}/generated-docs/**.html</filesToInclude>
+                    <regex>true</regex>
+                    <regexFlags>
+                        <regexFlag>DOTALL</regexFlag>
+                        <regexFlag>MULTILINE</regexFlag>
+                    </regexFlags>
+                    <token>^(.*)$</token>
+                    <value>
+&lt;!--
+                        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.
+                        --&gt;
+                        $1
+                    </value>
+                </configuration>
+            </plugin>
+            <plugin>
+                <groupId>org.apache.rat</groupId>
+                <artifactId>apache-rat-plugin</artifactId>
+                <configuration>
+                    <excludes combine.children="append">
+                        <!-- MIT license confirmed.  Excluding due to parse error-->
+                        <exclude>src/main/asciidoc/asciidoc-mod.css</exclude>
+                    </excludes>
+                </configuration>
+            </plugin>
+            <plugin>
+                <artifactId>maven-assembly-plugin</artifactId>
+                <configuration>
+                    <attach>true</attach>
+                </configuration>
+                <executions>
+                    <execution>
+                        <id>make shared resource</id>
+                        <goals>
+                            <goal>single</goal>
+                        </goals>
+                        <phase>package</phase>
+                        <configuration>
+                            <descriptors>
+                                <descriptor>src/main/assembly/dependencies.xml</descriptor>
+                            </descriptors>
+                        </configuration>
+                    </execution>
+                </executions>
+            </plugin>            
+        </plugins>
+    </build>
+</project>


[50/51] [partial] nifi-registry git commit: NIFIREG-201 Refactoring project structure to better isolate extensions

Posted by kd...@apache.org.
http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/6f26290d/nifi-registry-bootstrap/src/main/java/org/apache/nifi/registry/bootstrap/BootstrapCodec.java
----------------------------------------------------------------------
diff --git a/nifi-registry-bootstrap/src/main/java/org/apache/nifi/registry/bootstrap/BootstrapCodec.java b/nifi-registry-bootstrap/src/main/java/org/apache/nifi/registry/bootstrap/BootstrapCodec.java
deleted file mode 100644
index a273e07..0000000
--- a/nifi-registry-bootstrap/src/main/java/org/apache/nifi/registry/bootstrap/BootstrapCodec.java
+++ /dev/null
@@ -1,108 +0,0 @@
-/*
- * 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.bootstrap;
-
-import java.io.BufferedReader;
-import java.io.BufferedWriter;
-import java.io.IOException;
-import java.io.InputStream;
-import java.io.InputStreamReader;
-import java.io.OutputStream;
-import java.io.OutputStreamWriter;
-import java.util.Arrays;
-
-import org.apache.nifi.registry.bootstrap.exception.InvalidCommandException;
-
-public class BootstrapCodec {
-
-    private final RunNiFiRegistry runner;
-    private final BufferedReader reader;
-    private final BufferedWriter writer;
-
-    public BootstrapCodec(final RunNiFiRegistry runner, final InputStream in, final OutputStream out) {
-        this.runner = runner;
-        this.reader = new BufferedReader(new InputStreamReader(in));
-        this.writer = new BufferedWriter(new OutputStreamWriter(out));
-    }
-
-    public void communicate() throws IOException {
-        final String line = reader.readLine();
-        final String[] splits = line.split(" ");
-        if (splits.length < 0) {
-            throw new IOException("Received invalid command from NiFi Registry: " + line);
-        }
-
-        final String cmd = splits[0];
-        final String[] args;
-        if (splits.length == 1) {
-            args = new String[0];
-        } else {
-            args = Arrays.copyOfRange(splits, 1, splits.length);
-        }
-
-        try {
-            processRequest(cmd, args);
-        } catch (final InvalidCommandException ice) {
-            throw new IOException("Received invalid command from NiFi Registry: " + line + (ice.getMessage() == null ? "" : " - Details: " + ice.toString()));
-        }
-    }
-
-    private void processRequest(final String cmd, final String[] args) throws InvalidCommandException, IOException {
-        switch (cmd) {
-            case "PORT": {
-                if (args.length != 2) {
-                    throw new InvalidCommandException();
-                }
-
-                final int port;
-                try {
-                    port = Integer.parseInt(args[0]);
-                } catch (final NumberFormatException nfe) {
-                    throw new InvalidCommandException("Invalid Port number; should be integer between 1 and 65535");
-                }
-
-                if (port < 1 || port > 65535) {
-                    throw new InvalidCommandException("Invalid Port number; should be integer between 1 and 65535");
-                }
-
-                final String secretKey = args[1];
-
-                runner.setNiFiRegistryCommandControlPort(port, secretKey);
-                writer.write("OK");
-                writer.newLine();
-                writer.flush();
-            }
-            break;
-            case "STARTED": {
-                if (args.length != 1) {
-                    throw new InvalidCommandException("STARTED command must contain a status argument");
-                }
-
-                if (!"true".equals(args[0]) && !"false".equals(args[0])) {
-                    throw new InvalidCommandException("Invalid status for STARTED command; should be true or false, but was '" + args[0] + "'");
-                }
-
-                final boolean started = Boolean.parseBoolean(args[0]);
-                runner.setNiFiRegistryStarted(started);
-                writer.write("OK");
-                writer.newLine();
-                writer.flush();
-            }
-            break;
-        }
-    }
-}

http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/6f26290d/nifi-registry-bootstrap/src/main/java/org/apache/nifi/registry/bootstrap/NiFiRegistryListener.java
----------------------------------------------------------------------
diff --git a/nifi-registry-bootstrap/src/main/java/org/apache/nifi/registry/bootstrap/NiFiRegistryListener.java b/nifi-registry-bootstrap/src/main/java/org/apache/nifi/registry/bootstrap/NiFiRegistryListener.java
deleted file mode 100644
index f2ead2e..0000000
--- a/nifi-registry-bootstrap/src/main/java/org/apache/nifi/registry/bootstrap/NiFiRegistryListener.java
+++ /dev/null
@@ -1,141 +0,0 @@
-/*
- * 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.bootstrap;
-
-import org.apache.nifi.registry.bootstrap.util.LimitingInputStream;
-
-import java.io.IOException;
-import java.io.InputStream;
-import java.net.InetSocketAddress;
-import java.net.ServerSocket;
-import java.net.Socket;
-import java.util.concurrent.ExecutorService;
-import java.util.concurrent.Executors;
-import java.util.concurrent.ThreadFactory;
-import java.util.concurrent.TimeUnit;
-
-public class NiFiRegistryListener {
-
-    private ServerSocket serverSocket;
-    private volatile Listener listener;
-
-    int start(final RunNiFiRegistry runner) throws IOException {
-        serverSocket = new ServerSocket();
-        serverSocket.bind(new InetSocketAddress("localhost", 0));
-
-        final int localPort = serverSocket.getLocalPort();
-        listener = new Listener(serverSocket, runner);
-        final Thread listenThread = new Thread(listener);
-        listenThread.setName("Listen to NiFi Registry");
-        listenThread.setDaemon(true);
-        listenThread.start();
-        return localPort;
-    }
-
-    public void stop() throws IOException {
-        final Listener listener = this.listener;
-        if (listener == null) {
-            return;
-        }
-
-        listener.stop();
-    }
-
-    private class Listener implements Runnable {
-
-        private final ServerSocket serverSocket;
-        private final ExecutorService executor;
-        private final RunNiFiRegistry runner;
-        private volatile boolean stopped = false;
-
-        public Listener(final ServerSocket serverSocket, final RunNiFiRegistry runner) {
-            this.serverSocket = serverSocket;
-            this.executor = Executors.newFixedThreadPool(2, new ThreadFactory() {
-                @Override
-                public Thread newThread(final Runnable runnable) {
-                    final Thread t = Executors.defaultThreadFactory().newThread(runnable);
-                    t.setDaemon(true);
-                    t.setName("NiFi Registry Bootstrap Command Listener");
-                    return t;
-                }
-            });
-
-            this.runner = runner;
-        }
-
-        public void stop() throws IOException {
-            stopped = true;
-
-            executor.shutdown();
-            try {
-                executor.awaitTermination(3, TimeUnit.SECONDS);
-            } catch (final InterruptedException ie) {
-            }
-
-            serverSocket.close();
-        }
-
-        @Override
-        public void run() {
-            while (!serverSocket.isClosed()) {
-                try {
-                    if (stopped) {
-                        return;
-                    }
-
-                    final Socket socket;
-                    try {
-                        socket = serverSocket.accept();
-                    } catch (final IOException ioe) {
-                        if (stopped) {
-                            return;
-                        }
-
-                        throw ioe;
-                    }
-
-                    executor.submit(new Runnable() {
-                        @Override
-                        public void run() {
-                            try {
-                                // we want to ensure that we don't try to read data from an InputStream directly
-                                // by a BufferedReader because any user on the system could open a socket and send
-                                // a multi-gigabyte file without any new lines in order to crash the Bootstrap,
-                                // which in turn may cause the Shutdown Hook to shutdown NiFi.
-                                // So we will limit the amount of data to read to 4 KB
-                                final InputStream limitingIn = new LimitingInputStream(socket.getInputStream(), 4096);
-                                final BootstrapCodec codec = new BootstrapCodec(runner, limitingIn, socket.getOutputStream());
-                                codec.communicate();
-                            } catch (final Throwable t) {
-                                System.out.println("Failed to communicate with NiFi Registry due to " + t);
-                                t.printStackTrace();
-                            } finally {
-                                try {
-                                    socket.close();
-                                } catch (final IOException ioe) {
-                                }
-                            }
-                        }
-                    });
-                } catch (final Throwable t) {
-                    System.err.println("Failed to receive information from NiFi Registry due to " + t);
-                    t.printStackTrace();
-                }
-            }
-        }
-    }
-}

http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/6f26290d/nifi-registry-bootstrap/src/main/java/org/apache/nifi/registry/bootstrap/RunNiFiRegistry.java
----------------------------------------------------------------------
diff --git a/nifi-registry-bootstrap/src/main/java/org/apache/nifi/registry/bootstrap/RunNiFiRegistry.java b/nifi-registry-bootstrap/src/main/java/org/apache/nifi/registry/bootstrap/RunNiFiRegistry.java
deleted file mode 100644
index 769d1c4..0000000
--- a/nifi-registry-bootstrap/src/main/java/org/apache/nifi/registry/bootstrap/RunNiFiRegistry.java
+++ /dev/null
@@ -1,1246 +0,0 @@
-/*
- * 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.bootstrap;
-
-import org.apache.commons.lang3.StringUtils;
-import org.apache.nifi.registry.bootstrap.util.OSUtils;
-import org.apache.nifi.registry.util.FileUtils;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-
-import java.io.BufferedReader;
-import java.io.File;
-import java.io.FileInputStream;
-import java.io.FileNotFoundException;
-import java.io.FileOutputStream;
-import java.io.FilenameFilter;
-import java.io.IOException;
-import java.io.InputStream;
-import java.io.InputStreamReader;
-import java.io.OutputStream;
-import java.io.Reader;
-import java.lang.reflect.Method;
-import java.net.InetAddress;
-import java.net.InetSocketAddress;
-import java.net.Socket;
-import java.nio.charset.StandardCharsets;
-import java.nio.file.Files;
-import java.nio.file.attribute.PosixFilePermission;
-import java.text.SimpleDateFormat;
-import java.util.ArrayList;
-import java.util.Arrays;
-import java.util.HashMap;
-import java.util.HashSet;
-import java.util.List;
-import java.util.Map;
-import java.util.Map.Entry;
-import java.util.Properties;
-import java.util.Set;
-import java.util.concurrent.ExecutorService;
-import java.util.concurrent.Executors;
-import java.util.concurrent.Future;
-import java.util.concurrent.ThreadFactory;
-import java.util.concurrent.TimeUnit;
-import java.util.concurrent.locks.Condition;
-import java.util.concurrent.locks.Lock;
-import java.util.concurrent.locks.ReentrantLock;
-
-/**
- * <p>
- * The class which bootstraps Apache NiFi Registry. This class looks for the
- * bootstrap.conf file by looking in the following places (in order):</p>
- * <ol>
- * <li>Java System Property named
- * {@code org.apache.nifi.registry.bootstrap.config.file}</li>
- * <li>${NIFI_HOME}/./conf/bootstrap.conf, where ${NIFI_REGISTRY_HOME} references an
- * environment variable {@code NIFI_REGISTRY_HOME}</li>
- * <li>./conf/bootstrap.conf, where {@code ./} represents the working
- * directory.</li>
- * </ol>
- * <p>
- * If the {@code bootstrap.conf} file cannot be found, throws a {@code FileNotFoundException}.
- */
-public class RunNiFiRegistry {
-
-    public static final String DEFAULT_CONFIG_FILE = "./conf/bootstrap.conf";
-    public static final String DEFAULT_JAVA_CMD = "java";
-    public static final String DEFAULT_PID_DIR = "bin";
-    public static final String DEFAULT_LOG_DIR = "./logs";
-
-    public static final String GRACEFUL_SHUTDOWN_PROP = "graceful.shutdown.seconds";
-    public static final String DEFAULT_GRACEFUL_SHUTDOWN_VALUE = "20";
-
-    public static final String NIFI_REGISTRY_PID_DIR_PROP = "org.apache.nifi.registry.bootstrap.config.pid.dir";
-    public static final String NIFI_REGISTRY_PID_FILE_NAME = "nifi-registry.pid";
-    public static final String NIFI_REGISTRY_STATUS_FILE_NAME = "nifi-registry.status";
-    public static final String NIFI_REGISTRY_LOCK_FILE_NAME = "nifi-registry.lock";
-    public static final String NIFI_REGISTRY_BOOTSTRAP_SENSITIVE_KEY = "nifi.registry.bootstrap.sensitive.key";
-
-    public static final String PID_KEY = "pid";
-
-    public static final int STARTUP_WAIT_SECONDS = 60;
-
-    public static final String SHUTDOWN_CMD = "SHUTDOWN";
-    public static final String PING_CMD = "PING";
-    public static final String DUMP_CMD = "DUMP";
-
-    private volatile boolean autoRestartNiFiRegistry = true;
-    private volatile int ccPort = -1;
-    private volatile long nifiRegistryPid = -1L;
-    private volatile String secretKey;
-    private volatile ShutdownHook shutdownHook;
-    private volatile boolean nifiRegistryStarted;
-
-    private final Lock startedLock = new ReentrantLock();
-    private final Lock lock = new ReentrantLock();
-    private final Condition startupCondition = lock.newCondition();
-
-    private final File bootstrapConfigFile;
-
-    // used for logging initial info; these will be logged to console by default when the app is started
-    private final Logger cmdLogger = LoggerFactory.getLogger("org.apache.nifi.registry.bootstrap.Command");
-    // used for logging all info. These by default will be written to the log file
-    private final Logger defaultLogger = LoggerFactory.getLogger(RunNiFiRegistry.class);
-
-
-    private final ExecutorService loggingExecutor;
-    private volatile Set<Future<?>> loggingFutures = new HashSet<>(2);
-
-    public RunNiFiRegistry(final File bootstrapConfigFile, final boolean verbose) throws IOException {
-        this.bootstrapConfigFile = bootstrapConfigFile;
-
-        loggingExecutor = Executors.newFixedThreadPool(2, new ThreadFactory() {
-            @Override
-            public Thread newThread(final Runnable runnable) {
-                final Thread t = Executors.defaultThreadFactory().newThread(runnable);
-                t.setDaemon(true);
-                t.setName("NiFi logging handler");
-                return t;
-            }
-        });
-    }
-
-    private static void printUsage() {
-        System.out.println("Usage:");
-        System.out.println();
-        System.out.println("java org.apache.nifi.bootstrap.RunNiFiRegistry [<-verbose>] <command> [options]");
-        System.out.println();
-        System.out.println("Valid commands include:");
-        System.out.println("");
-        System.out.println("Start : Start a new instance of Apache NiFi Registry");
-        System.out.println("Stop : Stop a running instance of Apache NiFi Registry");
-        System.out.println("Restart : Stop Apache NiFi Registry, if it is running, and then start a new instance");
-        System.out.println("Status : Determine if there is a running instance of Apache NiFi Registry");
-        System.out.println("Dump : Write a Thread Dump to the file specified by [options], or to the log if no file is given");
-        System.out.println("Run : Start a new instance of Apache NiFi Registry and monitor the Process, restarting if the instance dies");
-        System.out.println();
-    }
-
-    private static String[] shift(final String[] orig) {
-        return Arrays.copyOfRange(orig, 1, orig.length);
-    }
-
-    public static void main(String[] args) throws IOException, InterruptedException {
-        if (args.length < 1 || args.length > 3) {
-            printUsage();
-            return;
-        }
-
-        File dumpFile = null;
-        boolean verbose = false;
-        if (args[0].equals("-verbose")) {
-            verbose = true;
-            args = shift(args);
-        }
-
-        final String cmd = args[0];
-        if (cmd.equals("dump")) {
-            if (args.length > 1) {
-                dumpFile = new File(args[1]);
-            } else {
-                dumpFile = null;
-            }
-        }
-
-        switch (cmd.toLowerCase()) {
-            case "start":
-            case "run":
-            case "stop":
-            case "status":
-            case "dump":
-            case "restart":
-            case "env":
-                break;
-            default:
-                printUsage();
-                return;
-        }
-
-        final File configFile = getDefaultBootstrapConfFile();
-        final RunNiFiRegistry runNiFiRegistry = new RunNiFiRegistry(configFile, verbose);
-
-        Integer exitStatus = null;
-        switch (cmd.toLowerCase()) {
-            case "start":
-                runNiFiRegistry.start();
-                break;
-            case "run":
-                runNiFiRegistry.start();
-                break;
-            case "stop":
-                runNiFiRegistry.stop();
-                break;
-            case "status":
-                exitStatus = runNiFiRegistry.status();
-                break;
-            case "restart":
-                runNiFiRegistry.stop();
-                runNiFiRegistry.start();
-                break;
-            case "dump":
-                runNiFiRegistry.dump(dumpFile);
-                break;
-            case "env":
-                runNiFiRegistry.env();
-                break;
-        }
-        if (exitStatus != null) {
-            System.exit(exitStatus);
-        }
-    }
-
-    private static File getDefaultBootstrapConfFile() {
-        String configFilename = System.getProperty("org.apache.nifi.registry.bootstrap.config.file");
-
-        if (configFilename == null) {
-            final String nifiRegistryHome = System.getenv("NIFI_REGISTRY_HOME");
-            if (nifiRegistryHome != null) {
-                final File nifiRegistryHomeFile = new File(nifiRegistryHome.trim());
-                final File configFile = new File(nifiRegistryHomeFile, DEFAULT_CONFIG_FILE);
-                configFilename = configFile.getAbsolutePath();
-            }
-        }
-
-        if (configFilename == null) {
-            configFilename = DEFAULT_CONFIG_FILE;
-        }
-
-        final File configFile = new File(configFilename);
-        return configFile;
-    }
-
-    protected File getBootstrapFile(final Logger logger, String directory, String defaultDirectory, String fileName) throws IOException {
-
-        final File confDir = bootstrapConfigFile.getParentFile();
-        final File nifiHome = confDir.getParentFile();
-
-        String confFileDir = System.getProperty(directory);
-
-        final File fileDir;
-
-        if (confFileDir != null) {
-            fileDir = new File(confFileDir.trim());
-        } else {
-            fileDir = new File(nifiHome, defaultDirectory);
-        }
-
-        FileUtils.ensureDirectoryExistAndCanAccess(fileDir);
-        final File statusFile = new File(fileDir, fileName);
-        logger.debug("Status File: {}", statusFile);
-        return statusFile;
-    }
-
-    protected File getPidFile(final Logger logger) throws IOException {
-        return getBootstrapFile(logger, NIFI_REGISTRY_PID_DIR_PROP, DEFAULT_PID_DIR, NIFI_REGISTRY_PID_FILE_NAME);
-    }
-
-    protected File getStatusFile(final Logger logger) throws IOException {
-        return getBootstrapFile(logger, NIFI_REGISTRY_PID_DIR_PROP, DEFAULT_PID_DIR, NIFI_REGISTRY_STATUS_FILE_NAME);
-    }
-
-    protected File getLockFile(final Logger logger) throws IOException {
-        return getBootstrapFile(logger, NIFI_REGISTRY_PID_DIR_PROP, DEFAULT_PID_DIR, NIFI_REGISTRY_LOCK_FILE_NAME);
-    }
-
-    protected File getStatusFile() throws IOException {
-        return getStatusFile(defaultLogger);
-    }
-
-    private Properties loadProperties(final Logger logger) throws IOException {
-        final Properties props = new Properties();
-        final File statusFile = getStatusFile(logger);
-        if (statusFile == null || !statusFile.exists()) {
-            logger.debug("No status file to load properties from");
-            return props;
-        }
-
-        try (final FileInputStream fis = new FileInputStream(getStatusFile(logger))) {
-            props.load(fis);
-        }
-
-        final Map<Object, Object> modified = new HashMap<>(props);
-        modified.remove("secret.key");
-        logger.debug("Properties: {}", modified);
-
-        return props;
-    }
-
-    private synchronized void savePidProperties(final Properties pidProperties, final Logger logger) throws IOException {
-        final String pid = pidProperties.getProperty(PID_KEY);
-        if (!StringUtils.isBlank(pid)) {
-            writePidFile(pid, logger);
-        }
-
-        final File statusFile = getStatusFile(logger);
-        if (statusFile.exists() && !statusFile.delete()) {
-            logger.warn("Failed to delete {}", statusFile);
-        }
-
-        if (!statusFile.createNewFile()) {
-            throw new IOException("Failed to create file " + statusFile);
-        }
-
-        try {
-            final Set<PosixFilePermission> perms = new HashSet<>();
-            perms.add(PosixFilePermission.OWNER_READ);
-            perms.add(PosixFilePermission.OWNER_WRITE);
-            Files.setPosixFilePermissions(statusFile.toPath(), perms);
-        } catch (final Exception e) {
-            logger.warn("Failed to set permissions so that only the owner can read status file {}; "
-                    + "this may allows others to have access to the key needed to communicate with NiFi Registry. "
-                    + "Permissions should be changed so that only the owner can read this file", statusFile);
-        }
-
-        try (final FileOutputStream fos = new FileOutputStream(statusFile)) {
-            pidProperties.store(fos, null);
-            fos.getFD().sync();
-        }
-
-        logger.debug("Saved Properties {} to {}", new Object[]{pidProperties, statusFile});
-    }
-
-    private synchronized void writePidFile(final String pid, final Logger logger) throws IOException {
-        final File pidFile = getPidFile(logger);
-        if (pidFile.exists() && !pidFile.delete()) {
-            logger.warn("Failed to delete {}", pidFile);
-        }
-
-        if (!pidFile.createNewFile()) {
-            throw new IOException("Failed to create file " + pidFile);
-        }
-
-        try {
-            final Set<PosixFilePermission> perms = new HashSet<>();
-            perms.add(PosixFilePermission.OWNER_WRITE);
-            perms.add(PosixFilePermission.OWNER_READ);
-            perms.add(PosixFilePermission.GROUP_READ);
-            perms.add(PosixFilePermission.OTHERS_READ);
-            Files.setPosixFilePermissions(pidFile.toPath(), perms);
-        } catch (final Exception e) {
-            logger.warn("Failed to set permissions so that only the owner can read pid file {}; "
-                    + "this may allows others to have access to the key needed to communicate with NiFi Registry. "
-                    + "Permissions should be changed so that only the owner can read this file", pidFile);
-        }
-
-        try (final FileOutputStream fos = new FileOutputStream(pidFile)) {
-            fos.write(pid.getBytes(StandardCharsets.UTF_8));
-            fos.getFD().sync();
-        }
-
-        logger.debug("Saved Pid {} to {}", new Object[]{pid, pidFile});
-    }
-
-    private boolean isPingSuccessful(final int port, final String secretKey, final Logger logger) {
-        logger.debug("Pinging {}", port);
-
-        try (final Socket socket = new Socket("localhost", port)) {
-            final OutputStream out = socket.getOutputStream();
-            out.write((PING_CMD + " " + secretKey + "\n").getBytes(StandardCharsets.UTF_8));
-            out.flush();
-
-            logger.debug("Sent PING command");
-            socket.setSoTimeout(5000);
-            final InputStream in = socket.getInputStream();
-            final BufferedReader reader = new BufferedReader(new InputStreamReader(in));
-            final String response = reader.readLine();
-            logger.debug("PING response: {}", response);
-            out.close();
-            reader.close();
-
-            return PING_CMD.equals(response);
-        } catch (final IOException ioe) {
-            return false;
-        }
-    }
-
-    private Integer getCurrentPort(final Logger logger) throws IOException {
-        final Properties props = loadProperties(logger);
-        final String portVal = props.getProperty("port");
-        if (portVal == null) {
-            logger.debug("No Port found in status file");
-            return null;
-        } else {
-            logger.debug("Port defined in status file: {}", portVal);
-        }
-
-        final int port = Integer.parseInt(portVal);
-        final boolean success = isPingSuccessful(port, props.getProperty("secret.key"), logger);
-        if (success) {
-            logger.debug("Successful PING on port {}", port);
-            return port;
-        }
-
-        final String pid = props.getProperty(PID_KEY);
-        logger.debug("PID in status file is {}", pid);
-        if (pid != null) {
-            final boolean procRunning = isProcessRunning(pid, logger);
-            if (procRunning) {
-                return port;
-            } else {
-                return null;
-            }
-        }
-
-        return null;
-    }
-
-    private boolean isProcessRunning(final String pid, final Logger logger) {
-        try {
-            // We use the "ps" command to check if the process is still running.
-            final ProcessBuilder builder = new ProcessBuilder();
-
-            builder.command("ps", "-p", pid);
-            final Process proc = builder.start();
-
-            // Look for the pid in the output of the 'ps' command.
-            boolean running = false;
-            String line;
-            try (final InputStream in = proc.getInputStream();
-                 final Reader streamReader = new InputStreamReader(in);
-                 final BufferedReader reader = new BufferedReader(streamReader)) {
-
-                while ((line = reader.readLine()) != null) {
-                    if (line.trim().startsWith(pid)) {
-                        running = true;
-                    }
-                }
-            }
-
-            // If output of the ps command had our PID, the process is running.
-            if (running) {
-                logger.debug("Process with PID {} is running", pid);
-            } else {
-                logger.debug("Process with PID {} is not running", pid);
-            }
-
-            return running;
-        } catch (final IOException ioe) {
-            System.err.println("Failed to determine if Process " + pid + " is running; assuming that it is not");
-            return false;
-        }
-    }
-
-    private Status getStatus(final Logger logger) {
-        final Properties props;
-        try {
-            props = loadProperties(logger);
-        } catch (final IOException ioe) {
-            return new Status(null, null, false, false);
-        }
-
-        if (props == null) {
-            return new Status(null, null, false, false);
-        }
-
-        final String portValue = props.getProperty("port");
-        final String pid = props.getProperty(PID_KEY);
-        final String secretKey = props.getProperty("secret.key");
-
-        if (portValue == null && pid == null) {
-            return new Status(null, null, false, false);
-        }
-
-        Integer port = null;
-        boolean pingSuccess = false;
-        if (portValue != null) {
-            try {
-                port = Integer.parseInt(portValue);
-                pingSuccess = isPingSuccessful(port, secretKey, logger);
-            } catch (final NumberFormatException nfe) {
-                return new Status(null, null, false, false);
-            }
-        }
-
-        if (pingSuccess) {
-            return new Status(port, pid, true, true);
-        }
-
-        final boolean alive = pid != null && isProcessRunning(pid, logger);
-        return new Status(port, pid, pingSuccess, alive);
-    }
-
-    public int status() throws IOException {
-        final Logger logger = cmdLogger;
-        final Status status = getStatus(logger);
-        if (status.isRespondingToPing()) {
-            logger.info("Apache NiFi Registry is currently running, listening to Bootstrap on port {}, PID={}",
-                    new Object[]{status.getPort(), status.getPid() == null ? "unknown" : status.getPid()});
-            return 0;
-        }
-
-        if (status.isProcessRunning()) {
-            logger.info("Apache NiFi Registry is running at PID {} but is not responding to ping requests", status.getPid());
-            return 4;
-        }
-
-        if (status.getPort() == null) {
-            logger.info("Apache NiFi Registry is not running");
-            return 3;
-        }
-
-        if (status.getPid() == null) {
-            logger.info("Apache NiFi Registry is not responding to Ping requests. The process may have died or may be hung");
-        } else {
-            logger.info("Apache NiFi Registry is not running");
-        }
-        return 3;
-    }
-
-    public void env() {
-        final Logger logger = cmdLogger;
-        final Status status = getStatus(logger);
-        if (status.getPid() == null) {
-            logger.info("Apache NiFi Registry is not running");
-            return;
-        }
-        final Class<?> virtualMachineClass;
-        try {
-            virtualMachineClass = Class.forName("com.sun.tools.attach.VirtualMachine");
-        } catch (final ClassNotFoundException cnfe) {
-            logger.error("Seems tools.jar (Linux / Windows JDK) or classes.jar (Mac OS) is not available in classpath");
-            return;
-        }
-        final Method attachMethod;
-        final Method detachMethod;
-
-        try {
-            attachMethod = virtualMachineClass.getMethod("attach", String.class);
-            detachMethod = virtualMachineClass.getDeclaredMethod("detach");
-        } catch (final Exception e) {
-            logger.error("Methods required for getting environment not available", e);
-            return;
-        }
-
-        final Object virtualMachine;
-        try {
-            virtualMachine = attachMethod.invoke(null, status.getPid());
-        } catch (final Throwable t) {
-            logger.error("Problem attaching to NiFi", t);
-            return;
-        }
-
-        try {
-            final Method getSystemPropertiesMethod = virtualMachine.getClass().getMethod("getSystemProperties");
-
-            final Properties sysProps = (Properties) getSystemPropertiesMethod.invoke(virtualMachine);
-            for (Entry<Object, Object> syspropEntry : sysProps.entrySet()) {
-                logger.info(syspropEntry.getKey().toString() + " = " + syspropEntry.getValue().toString());
-            }
-        } catch (Throwable t) {
-            throw new RuntimeException(t);
-        } finally {
-            try {
-                detachMethod.invoke(virtualMachine);
-            } catch (final Exception e) {
-                logger.warn("Caught exception detaching from process", e);
-            }
-        }
-    }
-
-    /**
-     * Writes a NiFi thread dump to the given file; if file is null, logs at
-     * INFO level instead.
-     *
-     * @param dumpFile the file to write the dump content to
-     * @throws IOException if any issues occur while writing the dump file
-     */
-    public void dump(final File dumpFile) throws IOException {
-        final Logger logger = defaultLogger;    // dump to bootstrap log file by default
-        final Integer port = getCurrentPort(logger);
-        if (port == null) {
-            logger.info("Apache NiFi Registry is not currently running");
-            return;
-        }
-
-        final Properties nifiRegistryProps = loadProperties(logger);
-        final String secretKey = nifiRegistryProps.getProperty("secret.key");
-
-        final StringBuilder sb = new StringBuilder();
-        try (final Socket socket = new Socket()) {
-            logger.debug("Connecting to NiFi Registry instance");
-            socket.setSoTimeout(60000);
-            socket.connect(new InetSocketAddress("localhost", port));
-            logger.debug("Established connection to NiFi Registry instance.");
-            socket.setSoTimeout(60000);
-
-            logger.debug("Sending DUMP Command to port {}", port);
-            final OutputStream out = socket.getOutputStream();
-            out.write((DUMP_CMD + " " + secretKey + "\n").getBytes(StandardCharsets.UTF_8));
-            out.flush();
-
-            final InputStream in = socket.getInputStream();
-            try (final BufferedReader reader = new BufferedReader(new InputStreamReader(in))) {
-                String line;
-                while ((line = reader.readLine()) != null) {
-                    sb.append(line).append("\n");
-                }
-            }
-        }
-
-        final String dump = sb.toString();
-        if (dumpFile == null) {
-            logger.info(dump);
-        } else {
-            try (final FileOutputStream fos = new FileOutputStream(dumpFile)) {
-                fos.write(dump.getBytes(StandardCharsets.UTF_8));
-            }
-            // we want to log to the console (by default) that we wrote the thread dump to the specified file
-            cmdLogger.info("Successfully wrote thread dump to {}", dumpFile.getAbsolutePath());
-        }
-    }
-
-    public void notifyStop() {
-        final String hostname = getHostname();
-        final SimpleDateFormat sdf = new SimpleDateFormat("yyyy/MM/dd HH:mm:ss.SSS");
-        final String now = sdf.format(System.currentTimeMillis());
-        String user = System.getProperty("user.name");
-        if (user == null || user.trim().isEmpty()) {
-            user = "Unknown User";
-        }
-    }
-
-    public void stop() throws IOException {
-        final Logger logger = cmdLogger;
-        final Integer port = getCurrentPort(logger);
-        if (port == null) {
-            logger.info("Apache NiFi Registry is not currently running");
-            return;
-        }
-
-        // indicate that a stop command is in progress
-        final File lockFile = getLockFile(logger);
-        if (!lockFile.exists()) {
-            lockFile.createNewFile();
-        }
-
-        final Properties nifiRegistryProps = loadProperties(logger);
-        final String secretKey = nifiRegistryProps.getProperty("secret.key");
-        final String pid = nifiRegistryProps.getProperty(PID_KEY);
-        final File statusFile = getStatusFile(logger);
-        final File pidFile = getPidFile(logger);
-
-        try (final Socket socket = new Socket()) {
-            logger.debug("Connecting to NiFi Registry instance");
-            socket.setSoTimeout(10000);
-            socket.connect(new InetSocketAddress("localhost", port));
-            logger.debug("Established connection to NiFi Registry instance.");
-            socket.setSoTimeout(10000);
-
-            logger.debug("Sending SHUTDOWN Command to port {}", port);
-            final OutputStream out = socket.getOutputStream();
-            out.write((SHUTDOWN_CMD + " " + secretKey + "\n").getBytes(StandardCharsets.UTF_8));
-            out.flush();
-            socket.shutdownOutput();
-
-            final InputStream in = socket.getInputStream();
-            int lastChar;
-            final StringBuilder sb = new StringBuilder();
-            while ((lastChar = in.read()) > -1) {
-                sb.append((char) lastChar);
-            }
-            final String response = sb.toString().trim();
-
-            logger.debug("Received response to SHUTDOWN command: {}", response);
-
-            if (SHUTDOWN_CMD.equals(response)) {
-                logger.info("Apache NiFi Registry has accepted the Shutdown Command and is shutting down now");
-
-                if (pid != null) {
-                    final Properties bootstrapProperties = new Properties();
-                    try (final FileInputStream fis = new FileInputStream(bootstrapConfigFile)) {
-                        bootstrapProperties.load(fis);
-                    }
-
-                    String gracefulShutdown = bootstrapProperties.getProperty(GRACEFUL_SHUTDOWN_PROP, DEFAULT_GRACEFUL_SHUTDOWN_VALUE);
-                    int gracefulShutdownSeconds;
-                    try {
-                        gracefulShutdownSeconds = Integer.parseInt(gracefulShutdown);
-                    } catch (final NumberFormatException nfe) {
-                        gracefulShutdownSeconds = Integer.parseInt(DEFAULT_GRACEFUL_SHUTDOWN_VALUE);
-                    }
-
-                    notifyStop();
-                    final long startWait = System.nanoTime();
-                    while (isProcessRunning(pid, logger)) {
-                        logger.info("Waiting for Apache NiFi Registry to finish shutting down...");
-                        final long waitNanos = System.nanoTime() - startWait;
-                        final long waitSeconds = TimeUnit.NANOSECONDS.toSeconds(waitNanos);
-                        if (waitSeconds >= gracefulShutdownSeconds && gracefulShutdownSeconds > 0) {
-                            if (isProcessRunning(pid, logger)) {
-                                logger.warn("NiFi Registry has not finished shutting down after {} seconds. Killing process.", gracefulShutdownSeconds);
-                                try {
-                                    killProcessTree(pid, logger);
-                                } catch (final IOException ioe) {
-                                    logger.error("Failed to kill Process with PID {}", pid);
-                                }
-                            }
-                            break;
-                        } else {
-                            try {
-                                Thread.sleep(2000L);
-                            } catch (final InterruptedException ie) {
-                            }
-                        }
-                    }
-
-                    if (statusFile.exists() && !statusFile.delete()) {
-                        logger.error("Failed to delete status file {}; this file should be cleaned up manually", statusFile);
-                    }
-
-                    if (pidFile.exists() && !pidFile.delete()) {
-                        logger.error("Failed to delete pid file {}; this file should be cleaned up manually", pidFile);
-                    }
-
-                    logger.info("NiFi Registry has finished shutting down.");
-                }
-            } else {
-                logger.error("When sending SHUTDOWN command to NiFi Registry , got unexpected response {}", response);
-            }
-        } catch (final IOException ioe) {
-            if (pid == null) {
-                logger.error("Failed to send shutdown command to port {} due to {}. No PID found for the NiFi Registry process, so unable to kill process; "
-                        + "the process should be killed manually.", new Object[]{port, ioe.toString()});
-            } else {
-                logger.error("Failed to send shutdown command to port {} due to {}. Will kill the NiFi Registry Process with PID {}.", port, ioe.toString(), pid);
-                notifyStop();
-                killProcessTree(pid, logger);
-                if (statusFile.exists() && !statusFile.delete()) {
-                    logger.error("Failed to delete status file {}; this file should be cleaned up manually", statusFile);
-                }
-            }
-        } finally {
-            if (lockFile.exists() && !lockFile.delete()) {
-                logger.error("Failed to delete lock file {}; this file should be cleaned up manually", lockFile);
-            }
-        }
-    }
-
-    private static List<String> getChildProcesses(final String ppid) throws IOException {
-        final Process proc = Runtime.getRuntime().exec(new String[]{"ps", "-o", "pid", "--no-headers", "--ppid", ppid});
-        final List<String> childPids = new ArrayList<>();
-        try (final InputStream in = proc.getInputStream();
-             final BufferedReader reader = new BufferedReader(new InputStreamReader(in))) {
-
-            String line;
-            while ((line = reader.readLine()) != null) {
-                childPids.add(line.trim());
-            }
-        }
-
-        return childPids;
-    }
-
-    private void killProcessTree(final String pid, final Logger logger) throws IOException {
-        logger.debug("Killing Process Tree for PID {}", pid);
-
-        final List<String> children = getChildProcesses(pid);
-        logger.debug("Children of PID {}: {}", new Object[]{pid, children});
-
-        for (final String childPid : children) {
-            killProcessTree(childPid, logger);
-        }
-
-        Runtime.getRuntime().exec(new String[]{"kill", "-9", pid});
-    }
-
-    public static boolean isAlive(final Process process) {
-        try {
-            process.exitValue();
-            return false;
-        } catch (final IllegalStateException | IllegalThreadStateException itse) {
-            return true;
-        }
-    }
-
-    private String getHostname() {
-        String hostname = "Unknown Host";
-        String ip = "Unknown IP Address";
-        try {
-            final InetAddress localhost = InetAddress.getLocalHost();
-            hostname = localhost.getHostName();
-            ip = localhost.getHostAddress();
-        } catch (final Exception e) {
-            defaultLogger.warn("Failed to obtain hostname for notification due to:", e);
-        }
-
-        return hostname + " (" + ip + ")";
-    }
-
-    @SuppressWarnings({"rawtypes", "unchecked"})
-    public void start() throws IOException, InterruptedException {
-        final Integer port = getCurrentPort(cmdLogger);
-        if (port != null) {
-            cmdLogger.info("Apache NiFi Registry is already running, listening to Bootstrap on port " + port);
-            return;
-        }
-
-        final File prevLockFile = getLockFile(cmdLogger);
-        if (prevLockFile.exists() && !prevLockFile.delete()) {
-            cmdLogger.warn("Failed to delete previous lock file {}; this file should be cleaned up manually", prevLockFile);
-        }
-
-        final ProcessBuilder builder = new ProcessBuilder();
-
-        if (!bootstrapConfigFile.exists()) {
-            throw new FileNotFoundException(bootstrapConfigFile.getAbsolutePath());
-        }
-
-        final Properties properties = new Properties();
-        try (final FileInputStream fis = new FileInputStream(bootstrapConfigFile)) {
-            properties.load(fis);
-        }
-
-        final Map<String, String> props = new HashMap<>();
-        props.putAll((Map) properties);
-
-        final String specifiedWorkingDir = props.get("working.dir");
-        if (specifiedWorkingDir != null) {
-            builder.directory(new File(specifiedWorkingDir));
-        }
-
-        final File bootstrapConfigAbsoluteFile = bootstrapConfigFile.getAbsoluteFile();
-        final File binDir = bootstrapConfigAbsoluteFile.getParentFile();
-        final File workingDir = binDir.getParentFile();
-
-        if (specifiedWorkingDir == null) {
-            builder.directory(workingDir);
-        }
-
-        final String nifiRegistryLogDir = replaceNull(System.getProperty("org.apache.nifi.registry.bootstrap.config.log.dir"), DEFAULT_LOG_DIR).trim();
-
-        final String libFilename = replaceNull(props.get("lib.dir"), "./lib").trim();
-        File libDir = getFile(libFilename, workingDir);
-        File libSharedDir = getFile(libFilename + "/shared", workingDir);
-
-        final String confFilename = replaceNull(props.get("conf.dir"), "./conf").trim();
-        File confDir = getFile(confFilename, workingDir);
-
-        String nifiRegistryPropsFilename = props.get("props.file");
-        if (nifiRegistryPropsFilename == null) {
-            if (confDir.exists()) {
-                nifiRegistryPropsFilename = new File(confDir, "nifi-registry.properties").getAbsolutePath();
-            } else {
-                nifiRegistryPropsFilename = DEFAULT_CONFIG_FILE;
-            }
-        }
-
-        nifiRegistryPropsFilename = nifiRegistryPropsFilename.trim();
-
-        final List<String> javaAdditionalArgs = new ArrayList<>();
-        for (final Map.Entry<String, String> entry : props.entrySet()) {
-            final String key = entry.getKey();
-            final String value = entry.getValue();
-
-            if (key.startsWith("java.arg")) {
-                javaAdditionalArgs.add(value);
-            }
-        }
-
-        final File[] libSharedFiles = libSharedDir.listFiles(new FilenameFilter() {
-            @Override
-            public boolean accept(final File dir, final String filename) {
-                return filename.toLowerCase().endsWith(".jar");
-            }
-        });
-
-        if (libSharedFiles == null || libSharedFiles.length == 0) {
-            throw new RuntimeException("Could not find lib shared directory at " + libSharedDir.getAbsolutePath());
-        }
-
-        final File[] libFiles = libDir.listFiles(new FilenameFilter() {
-            @Override
-            public boolean accept(final File dir, final String filename) {
-                return filename.toLowerCase().endsWith(".jar");
-            }
-        });
-
-        if (libFiles == null || libFiles.length == 0) {
-            throw new RuntimeException("Could not find lib directory at " + libDir.getAbsolutePath());
-        }
-
-        final File[] confFiles = confDir.listFiles();
-        if (confFiles == null || confFiles.length == 0) {
-            throw new RuntimeException("Could not find conf directory at " + confDir.getAbsolutePath());
-        }
-
-        final List<String> cpFiles = new ArrayList<>(confFiles.length + libFiles.length + libSharedFiles.length);
-        cpFiles.add(confDir.getAbsolutePath());
-        for (final File file : libSharedFiles) {
-            cpFiles.add(file.getAbsolutePath());
-        }
-        for (final File file : libFiles) {
-            cpFiles.add(file.getAbsolutePath());
-        }
-
-        final StringBuilder classPathBuilder = new StringBuilder();
-        for (int i = 0; i < cpFiles.size(); i++) {
-            final String filename = cpFiles.get(i);
-            classPathBuilder.append(filename);
-            if (i < cpFiles.size() - 1) {
-                classPathBuilder.append(File.pathSeparatorChar);
-            }
-        }
-
-        final String classPath = classPathBuilder.toString();
-        String javaCmd = props.get("java");
-        if (javaCmd == null) {
-            javaCmd = DEFAULT_JAVA_CMD;
-        }
-        if (javaCmd.equals(DEFAULT_JAVA_CMD)) {
-            String javaHome = System.getenv("JAVA_HOME");
-            if (javaHome != null) {
-                String fileExtension = isWindows() ? ".exe" : "";
-                File javaFile = new File(javaHome + File.separatorChar + "bin"
-                        + File.separatorChar + "java" + fileExtension);
-                if (javaFile.exists() && javaFile.canExecute()) {
-                    javaCmd = javaFile.getAbsolutePath();
-                }
-            }
-        }
-
-        final NiFiRegistryListener listener = new NiFiRegistryListener();
-        final int listenPort = listener.start(this);
-
-        final List<String> cmd = new ArrayList<>();
-
-        cmd.add(javaCmd);
-        cmd.add("-classpath");
-        cmd.add(classPath);
-        cmd.addAll(javaAdditionalArgs);
-        cmd.add("-Dnifi.registry.properties.file.path=" + nifiRegistryPropsFilename);
-        cmd.add("-Dnifi.registry.bootstrap.config.file.path=" + bootstrapConfigFile.getAbsolutePath());
-        cmd.add("-Dnifi.registry.bootstrap.listen.port=" + listenPort);
-        cmd.add("-Dapp=NiFiRegistry");
-        cmd.add("-Dorg.apache.nifi.registry.bootstrap.config.log.dir=" + nifiRegistryLogDir);
-        cmd.add("org.apache.nifi.registry.NiFiRegistry");
-
-        builder.command(cmd);
-
-        final StringBuilder cmdBuilder = new StringBuilder();
-        for (final String s : cmd) {
-            cmdBuilder.append(s).append(" ");
-        }
-
-        cmdLogger.info("Starting Apache NiFi Registry...");
-        cmdLogger.info("Working Directory: {}", workingDir.getAbsolutePath());
-        cmdLogger.info("Command: {}", cmdBuilder.toString());
-
-        String gracefulShutdown = props.get(GRACEFUL_SHUTDOWN_PROP);
-        if (gracefulShutdown == null) {
-            gracefulShutdown = DEFAULT_GRACEFUL_SHUTDOWN_VALUE;
-        }
-
-        final int gracefulShutdownSeconds;
-        try {
-            gracefulShutdownSeconds = Integer.parseInt(gracefulShutdown);
-        } catch (final NumberFormatException nfe) {
-            throw new NumberFormatException("The '" + GRACEFUL_SHUTDOWN_PROP + "' property in Bootstrap Config File "
-                    + bootstrapConfigAbsoluteFile.getAbsolutePath() + " has an invalid value. Must be a non-negative integer");
-        }
-
-        if (gracefulShutdownSeconds < 0) {
-            throw new NumberFormatException("The '" + GRACEFUL_SHUTDOWN_PROP + "' property in Bootstrap Config File "
-                    + bootstrapConfigAbsoluteFile.getAbsolutePath() + " has an invalid value. Must be a non-negative integer");
-        }
-
-        Process process = builder.start();
-        handleLogging(process);
-        Long pid = OSUtils.getProcessId(process, cmdLogger);
-        if (pid == null) {
-            cmdLogger.warn("Launched Apache NiFi Registry but could not determined the Process ID");
-        } else {
-            nifiRegistryPid = pid;
-            final Properties pidProperties = new Properties();
-            pidProperties.setProperty(PID_KEY, String.valueOf(nifiRegistryPid));
-            savePidProperties(pidProperties, cmdLogger);
-            cmdLogger.info("Launched Apache NiFi Registry with Process ID " + pid);
-        }
-
-        shutdownHook = new ShutdownHook(process, this, secretKey, gracefulShutdownSeconds, loggingExecutor);
-        final Runtime runtime = Runtime.getRuntime();
-        runtime.addShutdownHook(shutdownHook);
-
-        final String hostname = getHostname();
-        final SimpleDateFormat sdf = new SimpleDateFormat("yyyy/MM/dd HH:mm:ss.SSS");
-        String now = sdf.format(System.currentTimeMillis());
-        String user = System.getProperty("user.name");
-        if (user == null || user.trim().isEmpty()) {
-            user = "Unknown User";
-        }
-
-        while (true) {
-            final boolean alive = isAlive(process);
-
-            if (alive) {
-                try {
-                    Thread.sleep(1000L);
-                } catch (final InterruptedException ie) {
-                }
-            } else {
-                try {
-                    runtime.removeShutdownHook(shutdownHook);
-                } catch (final IllegalStateException ise) {
-                    // happens when already shutting down
-                }
-
-                now = sdf.format(System.currentTimeMillis());
-                if (autoRestartNiFiRegistry) {
-                    final File statusFile = getStatusFile(defaultLogger);
-                    if (!statusFile.exists()) {
-                        defaultLogger.info("Status File no longer exists. Will not restart NiFi Registry ");
-                        return;
-                    }
-
-                    final File lockFile = getLockFile(defaultLogger);
-                    if (lockFile.exists()) {
-                        defaultLogger.info("A shutdown was initiated. Will not restart NiFi Registry ");
-                        return;
-                    }
-
-                    final boolean previouslyStarted = getNifiRegistryStarted();
-                    if (!previouslyStarted) {
-                        defaultLogger.info("NiFi Registry never started. Will not restart NiFi Registry ");
-                        return;
-                    } else {
-                        setNiFiRegistryStarted(false);
-                    }
-
-                    defaultLogger.warn("Apache NiFi Registry appears to have died. Restarting...");
-                    process = builder.start();
-                    handleLogging(process);
-
-                    pid = OSUtils.getProcessId(process, defaultLogger);
-                    if (pid == null) {
-                        cmdLogger.warn("Launched Apache NiFi Registry but could not obtain the Process ID");
-                    } else {
-                        nifiRegistryPid = pid;
-                        final Properties pidProperties = new Properties();
-                        pidProperties.setProperty(PID_KEY, String.valueOf(nifiRegistryPid));
-                        savePidProperties(pidProperties, defaultLogger);
-                        cmdLogger.info("Launched Apache NiFi Registry with Process ID " + pid);
-                    }
-
-                    shutdownHook = new ShutdownHook(process, this, secretKey, gracefulShutdownSeconds, loggingExecutor);
-                    runtime.addShutdownHook(shutdownHook);
-
-                    final boolean started = waitForStart();
-
-                    if (started) {
-                        defaultLogger.info("Successfully started Apache NiFi Registry {}", (pid == null ? "" : " with PID " + pid));
-                    } else {
-                        defaultLogger.error("Apache NiFi Registry does not appear to have started");
-                    }
-                } else {
-                    return;
-                }
-            }
-        }
-    }
-
-    private void handleLogging(final Process process) {
-        final Set<Future<?>> existingFutures = loggingFutures;
-        if (existingFutures != null) {
-            for (final Future<?> future : existingFutures) {
-                future.cancel(false);
-            }
-        }
-
-        final Future<?> stdOutFuture = loggingExecutor.submit(new Runnable() {
-            @Override
-            public void run() {
-                final Logger stdOutLogger = LoggerFactory.getLogger("org.apache.nifi.registry.StdOut");
-                final InputStream in = process.getInputStream();
-                try (final BufferedReader reader = new BufferedReader(new InputStreamReader(in))) {
-                    String line;
-                    while ((line = reader.readLine()) != null) {
-                        stdOutLogger.info(line);
-                    }
-                } catch (IOException e) {
-                    defaultLogger.error("Failed to read from NiFi Registry's Standard Out stream", e);
-                }
-            }
-        });
-
-        final Future<?> stdErrFuture = loggingExecutor.submit(new Runnable() {
-            @Override
-            public void run() {
-                final Logger stdErrLogger = LoggerFactory.getLogger("org.apache.nifi.registry.StdErr");
-                final InputStream in = process.getErrorStream();
-                try (final BufferedReader reader = new BufferedReader(new InputStreamReader(in))) {
-                    String line;
-                    while ((line = reader.readLine()) != null) {
-                        stdErrLogger.error(line);
-                    }
-                } catch (IOException e) {
-                    defaultLogger.error("Failed to read from NiFi Registry's Standard Error stream", e);
-                }
-            }
-        });
-
-        final Set<Future<?>> futures = new HashSet<>();
-        futures.add(stdOutFuture);
-        futures.add(stdErrFuture);
-        this.loggingFutures = futures;
-    }
-
-
-    private boolean isWindows() {
-        final String osName = System.getProperty("os.name");
-        return osName != null && osName.toLowerCase().contains("win");
-    }
-
-    private boolean waitForStart() {
-        lock.lock();
-        try {
-            final long startTime = System.nanoTime();
-
-            while (ccPort < 1) {
-                try {
-                    startupCondition.await(1, TimeUnit.SECONDS);
-                } catch (final InterruptedException ie) {
-                    return false;
-                }
-
-                final long waitNanos = System.nanoTime() - startTime;
-                final long waitSeconds = TimeUnit.NANOSECONDS.toSeconds(waitNanos);
-                if (waitSeconds > STARTUP_WAIT_SECONDS) {
-                    return false;
-                }
-            }
-        } finally {
-            lock.unlock();
-        }
-        return true;
-    }
-
-    private File getFile(final String filename, final File workingDir) {
-        File file = new File(filename);
-        if (!file.isAbsolute()) {
-            file = new File(workingDir, filename);
-        }
-
-        return file;
-    }
-
-    private String replaceNull(final String value, final String replacement) {
-        return (value == null) ? replacement : value;
-    }
-
-    void setAutoRestartNiFiRegistry(final boolean restart) {
-        this.autoRestartNiFiRegistry = restart;
-    }
-
-    void setNiFiRegistryCommandControlPort(final int port, final String secretKey) throws IOException {
-        this.ccPort = port;
-        this.secretKey = secretKey;
-
-        if (shutdownHook != null) {
-            shutdownHook.setSecretKey(secretKey);
-        }
-
-        final File statusFile = getStatusFile(defaultLogger);
-
-        final Properties nifiProps = new Properties();
-        if (nifiRegistryPid != -1) {
-            nifiProps.setProperty(PID_KEY, String.valueOf(nifiRegistryPid));
-        }
-        nifiProps.setProperty("port", String.valueOf(ccPort));
-        nifiProps.setProperty("secret.key", secretKey);
-
-        try {
-            savePidProperties(nifiProps, defaultLogger);
-        } catch (final IOException ioe) {
-            defaultLogger.warn("Apache NiFi Registry has started but failed to persist NiFi Registry Port information to {} due to {}", new Object[]{statusFile.getAbsolutePath(), ioe});
-        }
-
-        defaultLogger.info("Apache NiFi Registry now running and listening for Bootstrap requests on port {}", port);
-    }
-
-    int getNiFiRegistryCommandControlPort() {
-        return this.ccPort;
-    }
-
-    void setNiFiRegistryStarted(final boolean nifiStarted) {
-        startedLock.lock();
-        try {
-            this.nifiRegistryStarted = nifiStarted;
-        } finally {
-            startedLock.unlock();
-        }
-    }
-
-    boolean getNifiRegistryStarted() {
-        startedLock.lock();
-        try {
-            return nifiRegistryStarted;
-        } finally {
-            startedLock.unlock();
-        }
-    }
-
-    private static class Status {
-
-        private final Integer port;
-        private final String pid;
-
-        private final Boolean respondingToPing;
-        private final Boolean processRunning;
-
-        public Status(final Integer port, final String pid, final Boolean respondingToPing, final Boolean processRunning) {
-            this.port = port;
-            this.pid = pid;
-            this.respondingToPing = respondingToPing;
-            this.processRunning = processRunning;
-        }
-
-        public String getPid() {
-            return pid;
-        }
-
-        public Integer getPort() {
-            return port;
-        }
-
-        public boolean isRespondingToPing() {
-            return Boolean.TRUE.equals(respondingToPing);
-        }
-
-        public boolean isProcessRunning() {
-            return Boolean.TRUE.equals(processRunning);
-        }
-    }
-}

http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/6f26290d/nifi-registry-bootstrap/src/main/java/org/apache/nifi/registry/bootstrap/ShutdownHook.java
----------------------------------------------------------------------
diff --git a/nifi-registry-bootstrap/src/main/java/org/apache/nifi/registry/bootstrap/ShutdownHook.java b/nifi-registry-bootstrap/src/main/java/org/apache/nifi/registry/bootstrap/ShutdownHook.java
deleted file mode 100644
index ba370e6..0000000
--- a/nifi-registry-bootstrap/src/main/java/org/apache/nifi/registry/bootstrap/ShutdownHook.java
+++ /dev/null
@@ -1,97 +0,0 @@
-/*
- * 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.bootstrap;
-
-import java.io.File;
-import java.io.IOException;
-import java.io.OutputStream;
-import java.net.Socket;
-import java.nio.charset.StandardCharsets;
-import java.util.concurrent.ExecutorService;
-import java.util.concurrent.TimeUnit;
-
-public class ShutdownHook extends Thread {
-
-    private final Process nifiRegistryProcess;
-    private final RunNiFiRegistry runner;
-    private final int gracefulShutdownSeconds;
-    private final ExecutorService executor;
-
-    private volatile String secretKey;
-
-    public ShutdownHook(final Process nifiRegistryProcess, final RunNiFiRegistry runner, final String secretKey, final int gracefulShutdownSeconds, final ExecutorService executor) {
-        this.nifiRegistryProcess = nifiRegistryProcess;
-        this.runner = runner;
-        this.secretKey = secretKey;
-        this.gracefulShutdownSeconds = gracefulShutdownSeconds;
-        this.executor = executor;
-    }
-
-    void setSecretKey(final String secretKey) {
-        this.secretKey = secretKey;
-    }
-
-    @Override
-    public void run() {
-        executor.shutdown();
-        runner.setAutoRestartNiFiRegistry(false);
-        final int ccPort = runner.getNiFiRegistryCommandControlPort();
-        if (ccPort > 0) {
-            System.out.println("Initiating Shutdown of NiFi Registry...");
-
-            try {
-                final Socket socket = new Socket("localhost", ccPort);
-                final OutputStream out = socket.getOutputStream();
-                out.write(("SHUTDOWN " + secretKey + "\n").getBytes(StandardCharsets.UTF_8));
-                out.flush();
-
-                socket.close();
-            } catch (final IOException ioe) {
-                System.out.println("Failed to Shutdown NiFi Registry due to " + ioe);
-            }
-        }
-
-        runner.notifyStop();
-        System.out.println("Waiting for Apache NiFi Registry to finish shutting down...");
-        final long startWait = System.nanoTime();
-        while (RunNiFiRegistry.isAlive(nifiRegistryProcess)) {
-            final long waitNanos = System.nanoTime() - startWait;
-            final long waitSeconds = TimeUnit.NANOSECONDS.toSeconds(waitNanos);
-            if (waitSeconds >= gracefulShutdownSeconds && gracefulShutdownSeconds > 0) {
-                if (RunNiFiRegistry.isAlive(nifiRegistryProcess)) {
-                    System.out.println("NiFi Registry has not finished shutting down after " + gracefulShutdownSeconds + " seconds. Killing process.");
-                    nifiRegistryProcess.destroy();
-                }
-                break;
-            } else {
-                try {
-                    Thread.sleep(1000L);
-                } catch (final InterruptedException ie) {
-                }
-            }
-        }
-
-        try {
-            final File statusFile = runner.getStatusFile();
-            if (!statusFile.delete()) {
-                System.err.println("Failed to delete status file " + statusFile.getAbsolutePath() + "; this file should be cleaned up manually");
-            }
-        }catch (IOException ex){
-            System.err.println("Failed to retrieve status file " + ex);
-        }
-    }
-}

http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/6f26290d/nifi-registry-bootstrap/src/main/java/org/apache/nifi/registry/bootstrap/exception/InvalidCommandException.java
----------------------------------------------------------------------
diff --git a/nifi-registry-bootstrap/src/main/java/org/apache/nifi/registry/bootstrap/exception/InvalidCommandException.java b/nifi-registry-bootstrap/src/main/java/org/apache/nifi/registry/bootstrap/exception/InvalidCommandException.java
deleted file mode 100644
index 6c51c08..0000000
--- a/nifi-registry-bootstrap/src/main/java/org/apache/nifi/registry/bootstrap/exception/InvalidCommandException.java
+++ /dev/null
@@ -1,38 +0,0 @@
-/*
- * 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.bootstrap.exception;
-
-public class InvalidCommandException extends Exception {
-
-    private static final long serialVersionUID = 1L;
-
-    public InvalidCommandException() {
-        super();
-    }
-
-    public InvalidCommandException(final String message) {
-        super(message);
-    }
-
-    public InvalidCommandException(final Throwable t) {
-        super(t);
-    }
-
-    public InvalidCommandException(final String message, final Throwable t) {
-        super(message, t);
-    }
-}

http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/6f26290d/nifi-registry-bootstrap/src/main/java/org/apache/nifi/registry/bootstrap/util/LimitingInputStream.java
----------------------------------------------------------------------
diff --git a/nifi-registry-bootstrap/src/main/java/org/apache/nifi/registry/bootstrap/util/LimitingInputStream.java b/nifi-registry-bootstrap/src/main/java/org/apache/nifi/registry/bootstrap/util/LimitingInputStream.java
deleted file mode 100644
index 79af09e..0000000
--- a/nifi-registry-bootstrap/src/main/java/org/apache/nifi/registry/bootstrap/util/LimitingInputStream.java
+++ /dev/null
@@ -1,107 +0,0 @@
-/*
- * 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.bootstrap.util;
-
-import java.io.IOException;
-import java.io.InputStream;
-
-public class LimitingInputStream extends InputStream {
-
-    private final InputStream in;
-    private final long limit;
-    private long bytesRead = 0;
-
-    public LimitingInputStream(final InputStream in, final long limit) {
-        this.in = in;
-        this.limit = limit;
-    }
-
-    @Override
-    public int read() throws IOException {
-        if (bytesRead >= limit) {
-            return -1;
-        }
-
-        final int val = in.read();
-        if (val > -1) {
-            bytesRead++;
-        }
-        return val;
-    }
-
-    @Override
-    public int read(final byte[] b) throws IOException {
-        if (bytesRead >= limit) {
-            return -1;
-        }
-
-        final int maxToRead = (int) Math.min(b.length, limit - bytesRead);
-
-        final int val = in.read(b, 0, maxToRead);
-        if (val > 0) {
-            bytesRead += val;
-        }
-        return val;
-    }
-
-    @Override
-    public int read(byte[] b, int off, int len) throws IOException {
-        if (bytesRead >= limit) {
-            return -1;
-        }
-
-        final int maxToRead = (int) Math.min(len, limit - bytesRead);
-
-        final int val = in.read(b, off, maxToRead);
-        if (val > 0) {
-            bytesRead += val;
-        }
-        return val;
-    }
-
-    @Override
-    public long skip(final long n) throws IOException {
-        final long skipped = in.skip(Math.min(n, limit - bytesRead));
-        bytesRead += skipped;
-        return skipped;
-    }
-
-    @Override
-    public int available() throws IOException {
-        return in.available();
-    }
-
-    @Override
-    public void close() throws IOException {
-        in.close();
-    }
-
-    @Override
-    public void mark(int readlimit) {
-        in.mark(readlimit);
-    }
-
-    @Override
-    public boolean markSupported() {
-        return in.markSupported();
-    }
-
-    @Override
-    public void reset() throws IOException {
-        in.reset();
-    }
-}

http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/6f26290d/nifi-registry-bootstrap/src/main/java/org/apache/nifi/registry/bootstrap/util/OSUtils.java
----------------------------------------------------------------------
diff --git a/nifi-registry-bootstrap/src/main/java/org/apache/nifi/registry/bootstrap/util/OSUtils.java b/nifi-registry-bootstrap/src/main/java/org/apache/nifi/registry/bootstrap/util/OSUtils.java
deleted file mode 100644
index 17c43df..0000000
--- a/nifi-registry-bootstrap/src/main/java/org/apache/nifi/registry/bootstrap/util/OSUtils.java
+++ /dev/null
@@ -1,107 +0,0 @@
-/*
- * 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.bootstrap.util;
-
-import java.lang.reflect.Field;
-
-import org.slf4j.Logger;
-import com.sun.jna.Pointer;
-import com.sun.jna.platform.win32.Kernel32;
-import com.sun.jna.platform.win32.WinNT;
-
-/**
- * OS specific utilities with generic method interfaces
- */
-public final class OSUtils {
-    /**
-     * @param process NiFi Process Reference
-     * @param logger  Logger Reference for Debug
-     * @return        Returns pid or null in-case pid could not be determined
-     * This method takes {@link Process} and {@link Logger} and returns
-     * the platform specific ProcessId for Unix like systems, a.k.a <b>pid</b>
-     * In-case it fails to determine the pid, it will return Null.
-     * Purpose for the Logger is to log any interaction for debugging.
-     */
-    private static Long getUnicesPid(final Process process, final Logger logger) {
-        try {
-            final Class<?> procClass = process.getClass();
-            final Field pidField = procClass.getDeclaredField("pid");
-            pidField.setAccessible(true);
-            final Object pidObject = pidField.get(process);
-
-            logger.debug("PID Object = {}", pidObject);
-
-            if (pidObject instanceof Number) {
-                return ((Number) pidObject).longValue();
-            }
-            return null;
-        } catch (final IllegalAccessException | NoSuchFieldException nsfe) {
-            logger.debug("Could not find PID for child process due to {}", nsfe);
-            return null;
-        }
-    }
-
-    /**
-     * @param process NiFi Registry Process Reference
-     * @param logger  Logger Reference for Debug
-     * @return        Returns pid or null in-case pid could not be determined
-     * This method takes {@link Process} and {@link Logger} and returns
-     * the platform specific Handle for Win32 Systems, a.k.a <b>pid</b>
-     * In-case it fails to determine the pid, it will return Null.
-     * Purpose for the Logger is to log any interaction for debugging.
-     */
-    private static Long getWindowsProcessId(final Process process, final Logger logger) {
-        /* determine the pid on windows plattforms */
-        try {
-            Field f = process.getClass().getDeclaredField("handle");
-            f.setAccessible(true);
-            long handl = f.getLong(process);
-
-            Kernel32 kernel = Kernel32.INSTANCE;
-            WinNT.HANDLE handle = new WinNT.HANDLE();
-            handle.setPointer(Pointer.createConstant(handl));
-            int ret = kernel.GetProcessId(handle);
-            logger.debug("Detected pid: {}", ret);
-            return Long.valueOf(ret);
-        } catch (final IllegalAccessException | NoSuchFieldException nsfe) {
-            logger.debug("Could not find PID for child process due to {}", nsfe);
-        }
-        return null;
-    }
-
-    /**
-     * @param process NiFi Process Reference
-     * @param logger  Logger Reference for Debug
-     * @return        Returns pid or null in-case pid could not be determined
-     * This method takes {@link Process} and {@link Logger} and returns
-     * the platform specific ProcessId for Unix like systems or Handle for Win32 Systems, a.k.a <b>pid</b>
-     * In-case it fails to determine the pid, it will return Null.
-     * Purpose for the Logger is to log any interaction for debugging.
-     */
-    public static Long getProcessId(final Process process, final Logger logger) {
-        if (process.getClass().getName().equals("java.lang.UNIXProcess")) {
-            return getUnicesPid(process, logger);
-        } else if (process.getClass().getName().equals("java.lang.Win32Process")
-                || process.getClass().getName().equals("java.lang.ProcessImpl")) {
-            return getWindowsProcessId(process, logger);
-        }
-
-        return null;
-    }
-
-}

http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/6f26290d/nifi-registry-client/pom.xml
----------------------------------------------------------------------
diff --git a/nifi-registry-client/pom.xml b/nifi-registry-client/pom.xml
deleted file mode 100644
index a766ff0..0000000
--- a/nifi-registry-client/pom.xml
+++ /dev/null
@@ -1,61 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<!-- 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. -->
-<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
-    <modelVersion>4.0.0</modelVersion>
-    <parent>
-        <groupId>org.apache.nifi.registry</groupId>
-        <artifactId>nifi-registry</artifactId>
-        <version>0.3.0-SNAPSHOT</version>
-    </parent>
-    
-    <artifactId>nifi-registry-client</artifactId>
-    <packaging>jar</packaging>
-
-    <dependencies>
-        <dependency>
-            <groupId>org.apache.nifi.registry</groupId>
-            <artifactId>nifi-registry-data-model</artifactId>
-            <version>0.3.0-SNAPSHOT</version>
-        </dependency>
-        <dependency>
-            <groupId>org.apache.nifi.registry</groupId>
-            <artifactId>nifi-registry-security-utils</artifactId>
-            <version>0.3.0-SNAPSHOT</version>
-        </dependency>
-        <dependency>
-            <groupId>org.glassfish.jersey.core</groupId>
-            <artifactId>jersey-client</artifactId>
-            <version>${jersey.version}</version>
-        </dependency>
-        <dependency>
-            <groupId>org.glassfish.jersey.media</groupId>
-            <artifactId>jersey-media-json-jackson</artifactId>
-            <version>${jersey.version}</version>
-        </dependency>
-        <dependency>
-            <groupId>org.glassfish.jersey.inject</groupId>
-            <artifactId>jersey-hk2</artifactId>
-            <version>${jersey.version}</version>
-        </dependency>
-        <dependency>
-            <groupId>org.glassfish.jersey.core</groupId>
-            <artifactId>jersey-common</artifactId>
-            <version>${jersey.version}</version>
-        </dependency>
-        <dependency>
-            <groupId>org.slf4j</groupId>
-            <artifactId>slf4j-simple</artifactId>
-            <version>${org.slf4j.version}</version>
-            <scope>test</scope>
-        </dependency>
-    </dependencies>
-</project>

http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/6f26290d/nifi-registry-client/src/main/java/org/apache/nifi/registry/client/BucketClient.java
----------------------------------------------------------------------
diff --git a/nifi-registry-client/src/main/java/org/apache/nifi/registry/client/BucketClient.java b/nifi-registry-client/src/main/java/org/apache/nifi/registry/client/BucketClient.java
deleted file mode 100644
index 80f72bb..0000000
--- a/nifi-registry-client/src/main/java/org/apache/nifi/registry/client/BucketClient.java
+++ /dev/null
@@ -1,76 +0,0 @@
-/*
- * 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.client;
-
-import org.apache.nifi.registry.bucket.Bucket;
-import org.apache.nifi.registry.field.Fields;
-
-import java.io.IOException;
-import java.util.List;
-
-/**
- * Client for interacting with buckets.
- */
-public interface BucketClient {
-
-    /**
-     * Creates the given bucket.
-     *
-     * @param bucket the bucket to create
-     * @return the created bucket with containing identifier that was generated
-     */
-    Bucket create(Bucket bucket) throws NiFiRegistryException, IOException;
-
-    /**
-     * Gets the bucket with the given id.
-     *
-     * @param bucketId the id of the bucket to retrieve
-     * @return the bucket with the given id
-     */
-    Bucket get(String bucketId) throws NiFiRegistryException, IOException;
-
-    /**
-     * Updates the given bucket. Only the name and description can be updated.
-     *
-     * @param bucket the bucket with updates, must contain the id
-     * @return the updated bucket
-     */
-    Bucket update(Bucket bucket) throws NiFiRegistryException, IOException;
-
-    /**
-     * Deletes the bucket with the given id.
-     *
-     * @param bucketId the id of the bucket to delete
-     * @return the deleted bucket
-     */
-    Bucket delete(String bucketId) throws NiFiRegistryException, IOException;
-
-    /**
-     * Gets the fields that can be used to sort/search buckets.
-     *
-     * @return the bucket fields
-     */
-    Fields getFields() throws NiFiRegistryException, IOException;
-
-    /**
-     * Gets all buckets.
-     *
-     * @return the list of all buckets
-     */
-    List<Bucket> getAll() throws NiFiRegistryException, IOException;
-
-}


[21/51] [partial] nifi-registry git commit: NIFIREG-201 Refactoring project structure to better isolate extensions

Posted by kd...@apache.org.
http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/6f26290d/nifi-registry-core/nifi-registry-security-api/src/main/java/org/apache/nifi/registry/security/authentication/BasicAuthIdentityProvider.java
----------------------------------------------------------------------
diff --git a/nifi-registry-core/nifi-registry-security-api/src/main/java/org/apache/nifi/registry/security/authentication/BasicAuthIdentityProvider.java b/nifi-registry-core/nifi-registry-security-api/src/main/java/org/apache/nifi/registry/security/authentication/BasicAuthIdentityProvider.java
new file mode 100644
index 0000000..64c8b8e
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-security-api/src/main/java/org/apache/nifi/registry/security/authentication/BasicAuthIdentityProvider.java
@@ -0,0 +1,100 @@
+/*
+ * 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.security.authentication;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import javax.servlet.http.HttpServletRequest;
+import java.nio.charset.Charset;
+import java.util.Base64;
+
+public abstract class BasicAuthIdentityProvider implements IdentityProvider {
+
+    public static final String AUTHORIZATION = "Authorization";
+    public static final String BASIC = "Basic ";
+
+    private static final Logger logger = LoggerFactory.getLogger(BasicAuthIdentityProvider.class);
+
+    private static final IdentityProviderUsage usage = new IdentityProviderUsage() {
+        @Override
+        public String getText() {
+            return "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>'.";
+        }
+
+        @Override
+        public AuthType getAuthType() {
+            return AuthType.BASIC;
+        }
+    };
+
+    @Override
+    public IdentityProviderUsage getUsageInstructions() {
+        return usage;
+    }
+
+    @Override
+    public AuthenticationRequest extractCredentials(HttpServletRequest servletRequest) {
+
+        if (servletRequest == null) {
+            logger.debug("Cannot extract user credentials from null servletRequest");
+            return null;
+        }
+
+        // only support this type of login when running securely
+        if (!servletRequest.isSecure()) {
+            return null;
+        }
+
+        final String authorization = servletRequest.getHeader(AUTHORIZATION);
+        if (authorization == null || !authorization.startsWith(BASIC)) {
+            logger.debug("HTTP Basic Auth credentials not present. Not attempting to extract credentials for authentication.");
+            return null;
+        }
+
+        AuthenticationRequest authenticationRequest;
+
+        try {
+
+            // Authorization: Basic {base64credentials}
+            String base64Credentials = authorization.substring(BASIC.length()).trim();
+            String credentials = new String(Base64.getDecoder().decode(base64Credentials), Charset.forName("UTF-8"));
+            // credentials = username:password
+            final String[] credentialParts = credentials.split(":", 2);
+            String username = credentialParts[0];
+            String password = credentialParts[1];
+
+            authenticationRequest = new UsernamePasswordAuthenticationRequest(username, password);
+
+        } catch (IllegalArgumentException | IndexOutOfBoundsException e) {
+            logger.info("Failed to extract user identity credentials.");
+            logger.debug("", e);
+            return null;
+        }
+
+        return authenticationRequest;
+
+    }
+
+    @Override
+    public boolean supports(Class<? extends AuthenticationRequest> authenticationRequestClazz) {
+        return UsernamePasswordAuthenticationRequest.class.isAssignableFrom(authenticationRequestClazz);
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/6f26290d/nifi-registry-core/nifi-registry-security-api/src/main/java/org/apache/nifi/registry/security/authentication/BearerAuthIdentityProvider.java
----------------------------------------------------------------------
diff --git a/nifi-registry-core/nifi-registry-security-api/src/main/java/org/apache/nifi/registry/security/authentication/BearerAuthIdentityProvider.java b/nifi-registry-core/nifi-registry-security-api/src/main/java/org/apache/nifi/registry/security/authentication/BearerAuthIdentityProvider.java
new file mode 100644
index 0000000..0647782
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-security-api/src/main/java/org/apache/nifi/registry/security/authentication/BearerAuthIdentityProvider.java
@@ -0,0 +1,77 @@
+/*
+ * 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.security.authentication;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import javax.servlet.http.HttpServletRequest;
+
+public abstract class BearerAuthIdentityProvider implements IdentityProvider {
+
+    public static final String AUTHORIZATION = "Authorization";
+    public static final String BEARER = "Bearer ";
+
+    private static final Logger logger = LoggerFactory.getLogger(BearerAuthIdentityProvider.class);
+
+    private static final IdentityProviderUsage usage = new IdentityProviderUsage() {
+        @Override
+        public String getText() {
+            return "The user credentials must be passed in standard HTTP Bearer Authorization format. " +
+                    "That is: 'Authorization: Bearer <token>', " +
+                    "where <token> is a value that will be validated by this identity provider.";
+        }
+
+        @Override
+        public AuthType getAuthType() {
+            return AuthType.BEARER;
+        }
+    };
+
+    @Override
+    public IdentityProviderUsage getUsageInstructions() {
+        return usage;
+    }
+
+    @Override
+    public AuthenticationRequest extractCredentials(HttpServletRequest request) {
+
+        if (request == null) {
+            logger.debug("Cannot extract user credentials from null servletRequest");
+            return null;
+        }
+
+        // only support this type of login when running securely
+        if (!request.isSecure()) {
+            return null;
+        }
+
+        // get the principal out of the user token
+        final String authorization = request.getHeader(AUTHORIZATION);
+        if (authorization == null || !authorization.startsWith(BEARER)) {
+            logger.debug("HTTP Bearer Auth credentials not present. Not attempting to extract credentials for authentication.");
+            return null;
+        }
+
+        // Extract the encoded token from the Authorization header
+        final String token = authorization.substring(BEARER.length()).trim();
+
+        return new AuthenticationRequest(null, token, null);
+
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/6f26290d/nifi-registry-core/nifi-registry-security-api/src/main/java/org/apache/nifi/registry/security/authentication/IdentityProvider.java
----------------------------------------------------------------------
diff --git a/nifi-registry-core/nifi-registry-security-api/src/main/java/org/apache/nifi/registry/security/authentication/IdentityProvider.java b/nifi-registry-core/nifi-registry-security-api/src/main/java/org/apache/nifi/registry/security/authentication/IdentityProvider.java
new file mode 100644
index 0000000..88488fb
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-security-api/src/main/java/org/apache/nifi/registry/security/authentication/IdentityProvider.java
@@ -0,0 +1,157 @@
+/*
+ * 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.security.authentication;
+
+import org.apache.nifi.registry.security.authentication.exception.IdentityAccessException;
+import org.apache.nifi.registry.security.authentication.exception.InvalidCredentialsException;
+import org.apache.nifi.registry.security.exception.SecurityProviderCreationException;
+import org.apache.nifi.registry.security.exception.SecurityProviderDestructionException;
+
+import javax.servlet.http.HttpServletRequest;
+
+/**
+ * IdentityProvider is an interface for a class that is able to establish a client identity.
+ *
+ * Specifically, this provider can:
+ *  - extract credentials from an HttpServletRequest (eg, parse a header, form parameter, or client certificates)
+ *  - authenticate those credentials and map them to an authenticated identity value
+ *    (eg, determine a username given a valid auth token)
+ */
+public interface IdentityProvider {
+
+    /**
+     * @return an IdentityProviderUsage that describes the expectations of the inputs
+     *         to {@link #authenticate(AuthenticationRequest)}
+     */
+    IdentityProviderUsage getUsageInstructions();
+
+    /**
+     * Extracts credentials from an {@link HttpServletRequest}.
+     *
+     * First, a check to the HttpServletRequest should be made to determine if this IdentityProvider is
+     * well suited to authenticate the request. For example, if the IdentityProvider is designed to read
+     * a particular header field to look for a token or identity claim, the check might be that the proper
+     * header field exists and (if a shared header field, such as "Authorization") that the format of the
+     * value in the header matches the expected format for this identity provider (e.g., must start with
+     * a prefix such as "Bearer"). Note, the expectations of the HttpServletRequest can be described by
+     * the {@link #getUsageInstructions()} method.
+     *
+     * If this check fails, this method should return null. This will indicate to the framework that the
+     * IdentityProvider does not recognize an identity claim present in the HttpServletRequest and that
+     * the framework should try another IdentityProvider.
+     *
+     * If the identity claim format is recognized, it should be extracted and returned in an
+     * {@link AuthenticationRequest}. The types and values set in the {@link AuthenticationRequest} are
+     * left to the discretion of the IdentityProvider, as the intended audience of the request is the
+     * {@link #authenticate(AuthenticationRequest)} method, where the corresponding logic to interpret
+     * an {@link AuthenticationRequest} can be implemented. As a rule of thumb, any values that could be considered
+     * sensitive, such as a password or persistent token susceptible to replay attacks, should be stored
+     * in the credentials field of the {@link AuthenticationRequest} as the framework will make the most effort
+     * to protect that value, including obscuring it in toString() output.
+     *
+     * If the {@link AuthenticationRequest} is insufficient or too generic for this IdentityProvider implementation,
+     * this IdentityProvider may subclass {@link AuthenticationRequest} to create a credentials-bearing request
+     * object that is better suited for this IdentityProvider implementation. In that case, the implementation
+     * might wish to also override the {@link #supports(Class)} method to indicate what types of request
+     * objects it supports in the call to {@link #authenticate(AuthenticationRequest)}.
+     *
+     * If credential location is recognized in the {@link HttpServletRequest} but extraction fails,
+     * in most cases that exceptional case should be caught, logged, and null should be returned, as it
+     * is possible another IdentityProvider will be able to parse the credentials or find a separate
+     * set of credentials in the {@link HttpServletRequest} (e.g., a request containing an Authorization
+     * header and a client certificate.)
+     *
+     * @param servletRequest the {@link HttpServletRequest} request that may contain credentials
+     *                       understood by this IdentityProvider
+     * @return an AuthenticationRequest containing the extracted credentials in a format this
+     *         IdentityProvider understands, or null if no credentials could be found in or extracted
+     *         successfully from the servletRequest
+     */
+    AuthenticationRequest extractCredentials(HttpServletRequest servletRequest);
+
+    /**
+     * Authenticates the credentials passed in the {@link AuthenticationRequest}.
+     *
+     * In typical usage, the AuthenticationRequest argument is expected to originate from this
+     * IdentityProvider's {@link #extractCredentials} method, so the logic for interpreting the
+     * values in the {@link AuthenticationRequest} should correspond to how the {@link AuthenticationRequest}
+     * is formed there.
+     *
+     * The first step of authentication should be to check if the credentials are understandable
+     * by this IdentityProvider. If this check fails, this method should return null. This will
+     * indicate to the framework that the IdentityProvider is not able to make a judgement call
+     * on if the request can be authenticated, and the framework can check with another IdentityProvider
+     * if one is available.
+     *
+     * If this IdentityProvider is able to interpret the AuthenticationRequest, it should perform
+     * and authentication check. If the authentication check fails, an exception should be thrown.
+     * Use an {@link InvalidCredentialsException} if the authentication check completed and the
+     * credentials failed authentication. Use an {@link IdentityAccessException} if a dependency
+     * service or provider fails, such as an failure to read a persistent store of identity or
+     * credential data. Either exception type will indicate to the framework that this IdentityProvider's
+     * opinion is that the client making the request should be blocked from accessing a resource
+     * that requires authentication. (Versus a null return value, which is an indication that this
+     * IdentityProvider is not well suited to make a judgement call one way or the other.)
+     *
+     * @param authenticationRequest the request, containing identity claim credentials for the
+     *                              IdentityProvider to authenticate and determine an identity
+     * @return The authentication response containing a fully populated identity value,
+     *         or null if identity cannot be determined
+     * @throws InvalidCredentialsException The login credentials were interpretable by this
+     *                                     IdentityProvider and failed authentication
+     * @throws IdentityAccessException Unable to assign an identity due to an issue accessing
+     *                                 underlying storage or service
+     */
+    AuthenticationResponse authenticate(AuthenticationRequest authenticationRequest)
+            throws InvalidCredentialsException, IdentityAccessException;
+
+    /**
+     * Allows this IdentityProvider to declare support for specific subclasses of {@link AuthenticationRequest}.
+     *
+     * In normal usage, only an AuthenticationRequest originating from this IdentityProvider's
+     * {@link #extractCredentials(HttpServletRequest)} method will be passed to {@link #authenticate(AuthenticationRequest)}.
+     * However, when IdentityProviders are used with another framework,
+     * another component may formulate the AuthenticationRequest to pass to the
+     * {@link #authenticate(AuthenticationRequest)} method. This allows a caller to
+     * check if the IdentityProvider can support the AuthenticationRequest class.
+     * If the caller knows the IdentityProvider can support the AuthenticationRequest
+     * (e.g., it was generated by calling {@link #extractCredentials(HttpServletRequest)},
+     * this check is optional and does not need to be performed.
+     *
+     * @param authenticationRequestClazz the class the caller wants to check
+     * @return a boolean value indicating if this IdentityProvider supports authenticationRequestClazz
+     */
+    default boolean supports(Class<? extends AuthenticationRequest> authenticationRequestClazz) {
+        return AuthenticationRequest.class.equals(authenticationRequestClazz);
+    }
+
+    /**
+     * Called to configure the AuthorityProvider after instance creation.
+     *
+     * @param configurationContext at the time of configuration
+     * @throws SecurityProviderCreationException for any issues configuring the provider
+     */
+    void onConfigured(IdentityProviderConfigurationContext configurationContext) throws SecurityProviderCreationException;
+
+    /**
+     * Called immediately before instance destruction for implementers to release resources.
+     *
+     * @throws SecurityProviderDestructionException If pre-destruction fails.
+     */
+    void preDestruction() throws SecurityProviderDestructionException;
+
+}

http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/6f26290d/nifi-registry-core/nifi-registry-security-api/src/main/java/org/apache/nifi/registry/security/authentication/IdentityProviderConfigurationContext.java
----------------------------------------------------------------------
diff --git a/nifi-registry-core/nifi-registry-security-api/src/main/java/org/apache/nifi/registry/security/authentication/IdentityProviderConfigurationContext.java b/nifi-registry-core/nifi-registry-security-api/src/main/java/org/apache/nifi/registry/security/authentication/IdentityProviderConfigurationContext.java
new file mode 100644
index 0000000..6be0207
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-security-api/src/main/java/org/apache/nifi/registry/security/authentication/IdentityProviderConfigurationContext.java
@@ -0,0 +1,50 @@
+/*
+ * 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.security.authentication;
+
+import java.util.Map;
+
+public interface IdentityProviderConfigurationContext {
+
+    /**
+     * @return identifier for the authority provider
+     */
+    String getIdentifier();
+
+    /**
+     * @return the IdentityProviderLookup from the factory context
+     */
+    public IdentityProviderLookup getIdentityProviderLookup();
+
+    /**
+     * Retrieves all properties the component currently understands regardless
+     * of whether a value has been set for them or not. If no value is present
+     * then its value is null and thus any registered default for the property
+     * descriptor applies.
+     *
+     * @return Map of all properties
+     */
+    Map<String, String> getProperties();
+
+    /**
+     * @param property to lookup the descriptor and value of
+     * @return the value the component currently understands for the given
+     * PropertyDescriptor. This method does not substitute default
+     * PropertyDescriptor values, so the value returned will be null if not set
+     */
+    String getProperty(String property);
+}

http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/6f26290d/nifi-registry-core/nifi-registry-security-api/src/main/java/org/apache/nifi/registry/security/authentication/IdentityProviderLookup.java
----------------------------------------------------------------------
diff --git a/nifi-registry-core/nifi-registry-security-api/src/main/java/org/apache/nifi/registry/security/authentication/IdentityProviderLookup.java b/nifi-registry-core/nifi-registry-security-api/src/main/java/org/apache/nifi/registry/security/authentication/IdentityProviderLookup.java
new file mode 100644
index 0000000..dbf6d58
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-security-api/src/main/java/org/apache/nifi/registry/security/authentication/IdentityProviderLookup.java
@@ -0,0 +1,23 @@
+/*
+ * 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.security.authentication;
+
+public interface IdentityProviderLookup {
+
+    IdentityProvider getIdentityProvider(String identifier);
+
+}

http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/6f26290d/nifi-registry-core/nifi-registry-security-api/src/main/java/org/apache/nifi/registry/security/authentication/IdentityProviderUsage.java
----------------------------------------------------------------------
diff --git a/nifi-registry-core/nifi-registry-security-api/src/main/java/org/apache/nifi/registry/security/authentication/IdentityProviderUsage.java b/nifi-registry-core/nifi-registry-security-api/src/main/java/org/apache/nifi/registry/security/authentication/IdentityProviderUsage.java
new file mode 100644
index 0000000..aefc97d
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-security-api/src/main/java/org/apache/nifi/registry/security/authentication/IdentityProviderUsage.java
@@ -0,0 +1,135 @@
+/*
+ * 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.security.authentication;
+
+public interface IdentityProviderUsage {
+
+    /**
+     * Provides the usage instructions for an identity provider.
+     *
+     * The instructions should target a human consumer of the
+     * NiFi Registry REST API that needs to know how to handle
+     * Authentication when using / programming an API client.
+     *
+     * @return the usage instructions for an identity provider
+     */
+    String getText();
+
+    /**
+     * If the identity provider follows an HTTP standard auth
+     * scheme, this provides which scheme is being used
+     * (or "Other" if the identity provider follows its own scheme).
+     *
+     * In the case the scheme is well understood, such as HTTP
+     * "Basic" Auth, this may be sufficient. In other cases,
+     * {@link #getText()} should provider detailed human-readable
+     * instructions about how a client should interact with
+     * the {@link IdentityProvider}.
+     *
+     * @return an enum for the auth
+     */
+    AuthType getAuthType();
+
+    /**
+     * Standard auth types as maintained by IANA:
+     * https://www.iana.org/assignments/http-authschemes/http-authschemes.xhtml
+     *
+     * Note, draft and experimental standards are not included, nor are app-specific custom schemes.
+     * To create an enum for such a scheme, use OTHER with a custom httpAuthScheme string, e.g.:
+     *
+     * <code>AuthType myAuthType = AuthType.OTHER.httpAuthScheme("my-auth-scheme");</code>
+     */
+    enum AuthType {
+
+        /**
+         * Indicates the AuthType is unknown. Can be used in places where an AuthType is required but unknown by default.
+         */
+        UNKNOWN(0, "Unknown"),
+
+        /**
+         * HTTP Basic Auth as defined by RFC7617.
+         */
+        BASIC(1, "Basic"),
+
+        /**
+         * HTTP Bearer Auth as defined by RFC6750.
+         */
+        BEARER(2, "Bearer"),
+
+        /**
+         * HTTP Digest Auth as defined by RFC7616.
+         */
+        DIGEST(3, "Digest"),
+
+        /**
+         * HTTP Negotiate (SPNEGO) Auth as defined by RFC4559.
+         */
+        NEGOTIATE(4, "Negotiate"),
+
+        /**
+         * HTTP OAuth as defined by RFC5849
+         */
+        OAUTH(5, "OAuth"),
+
+        /**
+         * A distinct AuthType for which there is not yet a defined enumeration value.
+         * If a HTTP Auth Scheme should be set (e.g., for use in a WWW-Authenticate challenge list)
+         * use the setter, i.e.:
+         * <code>AuthType myAuthType = AuthType.OTHER.httpAuthScheme("my-auth-scheme");</code>
+         */
+        OTHER(99, "Other"),
+        ;
+
+        private final int code;
+        private String httpAuthScheme;
+
+        private AuthType(int statusCode, String httpAuthScheme) {
+            this.code = statusCode;
+            this.httpAuthScheme = httpAuthScheme;
+        }
+
+        public int getStatusCode() {
+            return this.code;
+        }
+
+        public String getHttpAuthScheme() {
+            return this.toString();
+        }
+
+        public AuthType httpAuthScheme(String httpAuthScheme) {
+            if (httpAuthScheme != null) {
+                this.httpAuthScheme = httpAuthScheme;
+            }
+            return this;
+        }
+
+        public String toString() {
+            return this.httpAuthScheme;
+        }
+
+        public static AuthType fromCode(int code) {
+            AuthType[] enumTypes = values();
+            for (AuthType s : enumTypes) {
+                if (s.code == code) {
+                    return s;
+                }
+            }
+            return null;
+        }
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/6f26290d/nifi-registry-core/nifi-registry-security-api/src/main/java/org/apache/nifi/registry/security/authentication/UsernamePasswordAuthenticationRequest.java
----------------------------------------------------------------------
diff --git a/nifi-registry-core/nifi-registry-security-api/src/main/java/org/apache/nifi/registry/security/authentication/UsernamePasswordAuthenticationRequest.java b/nifi-registry-core/nifi-registry-security-api/src/main/java/org/apache/nifi/registry/security/authentication/UsernamePasswordAuthenticationRequest.java
new file mode 100644
index 0000000..3abcf94
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-security-api/src/main/java/org/apache/nifi/registry/security/authentication/UsernamePasswordAuthenticationRequest.java
@@ -0,0 +1,25 @@
+/*
+ * 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.security.authentication;
+
+public class UsernamePasswordAuthenticationRequest extends AuthenticationRequest {
+
+    public UsernamePasswordAuthenticationRequest(String username, String password) {
+        super(username, password, null);
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/6f26290d/nifi-registry-core/nifi-registry-security-api/src/main/java/org/apache/nifi/registry/security/authentication/annotation/IdentityProviderContext.java
----------------------------------------------------------------------
diff --git a/nifi-registry-core/nifi-registry-security-api/src/main/java/org/apache/nifi/registry/security/authentication/annotation/IdentityProviderContext.java b/nifi-registry-core/nifi-registry-security-api/src/main/java/org/apache/nifi/registry/security/authentication/annotation/IdentityProviderContext.java
new file mode 100644
index 0000000..8d0ddf0
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-security-api/src/main/java/org/apache/nifi/registry/security/authentication/annotation/IdentityProviderContext.java
@@ -0,0 +1,31 @@
+/*
+ * 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.security.authentication.annotation;
+
+import java.lang.annotation.Documented;
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Inherited;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+@Documented
+@Target({ElementType.FIELD, ElementType.METHOD})
+@Retention(RetentionPolicy.RUNTIME)
+@Inherited
+public @interface IdentityProviderContext {
+}

http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/6f26290d/nifi-registry-core/nifi-registry-security-api/src/main/java/org/apache/nifi/registry/security/authentication/exception/IdentityAccessException.java
----------------------------------------------------------------------
diff --git a/nifi-registry-core/nifi-registry-security-api/src/main/java/org/apache/nifi/registry/security/authentication/exception/IdentityAccessException.java b/nifi-registry-core/nifi-registry-security-api/src/main/java/org/apache/nifi/registry/security/authentication/exception/IdentityAccessException.java
new file mode 100644
index 0000000..fae567a
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-security-api/src/main/java/org/apache/nifi/registry/security/authentication/exception/IdentityAccessException.java
@@ -0,0 +1,33 @@
+/*
+ * 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.security.authentication.exception;
+
+/**
+ * Represents the case when the identity could not be confirmed because it was unable
+ * to access the backing store.
+ */
+public class IdentityAccessException extends RuntimeException {
+
+    public IdentityAccessException(String message, Throwable cause) {
+        super(message, cause);
+    }
+
+    public IdentityAccessException(String message) {
+        super(message);
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/6f26290d/nifi-registry-core/nifi-registry-security-api/src/main/java/org/apache/nifi/registry/security/authentication/exception/InvalidCredentialsException.java
----------------------------------------------------------------------
diff --git a/nifi-registry-core/nifi-registry-security-api/src/main/java/org/apache/nifi/registry/security/authentication/exception/InvalidCredentialsException.java b/nifi-registry-core/nifi-registry-security-api/src/main/java/org/apache/nifi/registry/security/authentication/exception/InvalidCredentialsException.java
new file mode 100644
index 0000000..e7c7339
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-security-api/src/main/java/org/apache/nifi/registry/security/authentication/exception/InvalidCredentialsException.java
@@ -0,0 +1,33 @@
+/*
+ * 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.security.authentication.exception;
+
+/**
+ * Represents the case when the identity could not be confirmed because the
+ * identity claim credentials were invalid.
+ */
+public class InvalidCredentialsException extends RuntimeException {
+
+    public InvalidCredentialsException(String message, Throwable cause) {
+        super(message, cause);
+    }
+
+    public InvalidCredentialsException(String message) {
+        super(message);
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/6f26290d/nifi-registry-core/nifi-registry-security-api/src/main/java/org/apache/nifi/registry/security/authorization/AccessPolicy.java
----------------------------------------------------------------------
diff --git a/nifi-registry-core/nifi-registry-security-api/src/main/java/org/apache/nifi/registry/security/authorization/AccessPolicy.java b/nifi-registry-core/nifi-registry-security-api/src/main/java/org/apache/nifi/registry/security/authorization/AccessPolicy.java
new file mode 100644
index 0000000..aa8260b
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-security-api/src/main/java/org/apache/nifi/registry/security/authorization/AccessPolicy.java
@@ -0,0 +1,367 @@
+/*
+ * 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.security.authorization;
+
+import java.nio.charset.StandardCharsets;
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.Objects;
+import java.util.Set;
+import java.util.UUID;
+
+/**
+ * Defines a policy for a set of userIdentifiers to perform a set of actions on a given resource.
+ */
+public class AccessPolicy {
+
+    private final String identifier;
+
+    private final String resource;
+
+    private final Set<String> users;
+
+    private final Set<String> groups;
+
+    private final RequestAction action;
+
+    private AccessPolicy(final Builder builder) {
+        this.identifier = builder.identifier;
+        this.resource = builder.resource;
+        this.action = builder.action;
+        this.users = Collections.unmodifiableSet(new HashSet<>(builder.users));
+        this.groups = Collections.unmodifiableSet(new HashSet<>(builder.groups));
+
+        if (this.identifier == null || this.identifier.trim().isEmpty()) {
+            throw new IllegalArgumentException("Identifier can not be null or empty");
+        }
+
+        if (this.resource == null) {
+            throw new IllegalArgumentException("Resource can not be null");
+        }
+
+        if (this.action == null) {
+            throw new IllegalArgumentException("Action can not be null");
+        }
+    }
+
+    /**
+     * @return the identifier for this policy
+     */
+    public String getIdentifier() {
+        return identifier;
+    }
+
+    /**
+     * @return the resource for this policy
+     */
+    public String getResource() {
+        return resource;
+    }
+
+    /**
+     * @return an unmodifiable set of user ids for this policy
+     */
+    public Set<String> getUsers() {
+        return users;
+    }
+
+    /**
+     * @return an unmodifiable set of group ids for this policy
+     */
+    public Set<String> getGroups() {
+        return groups;
+    }
+
+    /**
+     * @return the action for this policy
+     */
+    public RequestAction getAction() {
+        return action;
+    }
+
+    @Override
+    public boolean equals(Object obj) {
+        if (obj == null) {
+            return false;
+        }
+        if (getClass() != obj.getClass()) {
+            return false;
+        }
+
+        final AccessPolicy other = (AccessPolicy) obj;
+        return Objects.equals(this.identifier, other.identifier);
+    }
+
+    @Override
+    public int hashCode() {
+        return Objects.hashCode(this.identifier);
+    }
+
+    @Override
+    public String toString() {
+        return String.format("identifier[%s], resource[%s], users[%s], groups[%s], action[%s]",
+                getIdentifier(), getResource(), getUsers(), getGroups(), getAction());
+    }
+
+    /**
+     * Builder for Access Policies.
+     */
+    public static class Builder {
+
+        private String identifier;
+        private String resource;
+        private RequestAction action;
+        private Set<String> users = new HashSet<>();
+        private Set<String> groups = new HashSet<>();
+        private final boolean fromPolicy;
+
+        /**
+         * Default constructor for building a new AccessPolicy.
+         */
+        public Builder() {
+            this.fromPolicy = false;
+        }
+
+        /**
+         * Initializes the builder with the state of the provided policy. When using this constructor
+         * the identifier field of the builder can not be changed and will result in an IllegalStateException
+         * if attempting to do so.
+         *
+         * @param other the existing access policy to initialize from
+         */
+        public Builder(final AccessPolicy other) {
+            if (other == null) {
+                throw new IllegalArgumentException("Can not initialize builder with a null access policy");
+            }
+
+            this.identifier = other.getIdentifier();
+            this.resource = other.getResource();
+            this.action = other.getAction();
+            this.users.clear();
+            this.users.addAll(other.getUsers());
+            this.groups.clear();
+            this.groups.addAll(other.getGroups());
+            this.fromPolicy = true;
+        }
+
+        /**
+         * Sets the identifier of the builder.
+         *
+         * @param identifier the identifier to set
+         * @return the builder
+         * @throws IllegalStateException if this method is called when this builder was constructed from an existing Policy
+         */
+        public Builder identifier(final String identifier) {
+            if (fromPolicy) {
+                throw new IllegalStateException(
+                        "Identifier can not be changed when initialized from an existing policy");
+            }
+
+            this.identifier = identifier;
+            return this;
+        }
+
+        /**
+         * Sets the identifier of the builder to a random UUID.
+         *
+         * @return the builder
+         * @throws IllegalStateException if this method is called when this builder was constructed from an existing Policy
+         */
+        public Builder identifierGenerateRandom() {
+            if (fromPolicy) {
+                throw new IllegalStateException(
+                        "Identifier can not be changed when initialized from an existing policy");
+            }
+
+            this.identifier = UUID.randomUUID().toString();
+            return this;
+        }
+
+        /**
+         * Sets the identifier of the builder with a UUID generated from the specified seed string.
+         *
+         * @return the builder
+         * @throws IllegalStateException if this method is called when this builder was constructed from an existing Policy
+         */
+        public Builder identifierGenerateFromSeed(final String seed) {
+            if (fromPolicy) {
+                throw new IllegalStateException(
+                        "Identifier can not be changed when initialized from an existing policy");
+            }
+            if (seed == null) {
+                throw new IllegalArgumentException("Cannot seed the policy identifier with a null value.");
+            }
+
+            this.identifier = UUID.nameUUIDFromBytes(seed.getBytes(StandardCharsets.UTF_8)).toString();
+            return this;
+        }
+
+        /**
+         * Sets the resource of the builder.
+         *
+         * @param resource the resource to set
+         * @return the builder
+         */
+        public Builder resource(final String resource) {
+            this.resource = resource;
+            return this;
+        }
+
+        /**
+         * Adds all the users from the provided set to the builder's set of users.
+         *
+         * @param users the users to add
+         * @return the builder
+         */
+        public Builder addUsers(final Set<String> users) {
+            if (users != null) {
+                this.users.addAll(users);
+            }
+            return this;
+        }
+
+        /**
+         * Adds the given user to the builder's set of users.
+         *
+         * @param user the user to add
+         * @return the builder
+         */
+        public Builder addUser(final String user) {
+            if (user != null) {
+                this.users.add(user);
+            }
+            return this;
+        }
+
+        /**
+         * Removes all users in the provided set from the builder's set of users.
+         *
+         * @param users the users to remove
+         * @return the builder
+         */
+        public Builder removeUsers(final Set<String> users) {
+            if (users != null) {
+                this.users.removeAll(users);
+            }
+            return this;
+        }
+
+        /**
+         * Removes the provided user from the builder's set of users.
+         *
+         * @param user the user to remove
+         * @return the builder
+         */
+        public Builder removeUser(final String user) {
+            if (user != null) {
+                this.users.remove(user);
+            }
+            return this;
+        }
+
+        /**
+         * Clears the builder's set of users so that it is non-null and size == 0.
+         *
+         * @return the builder
+         */
+        public Builder clearUsers() {
+            this.users.clear();
+            return this;
+        }
+
+        /**
+         * Adds all the groups from the provided set to the builder's set of groups.
+         *
+         * @param groups the groups to add
+         * @return the builder
+         */
+        public Builder addGroups(final Set<String> groups) {
+            if (groups != null) {
+                this.groups.addAll(groups);
+            }
+            return this;
+        }
+
+        /**
+         * Adds the given group to the builder's set of groups.
+         *
+         * @param group the group to add
+         * @return the builder
+         */
+        public Builder addGroup(final String group) {
+            if (group != null) {
+                this.groups.add(group);
+            }
+            return this;
+        }
+
+        /**
+         * Removes all groups in the provided set from the builder's set of groups.
+         *
+         * @param groups the groups to remove
+         * @return the builder
+         */
+        public Builder removeGroups(final Set<String> groups) {
+            if (groups != null) {
+                this.groups.removeAll(groups);
+            }
+            return this;
+        }
+
+        /**
+         * Removes the provided groups from the builder's set of groups.
+         *
+         * @param group the group to remove
+         * @return the builder
+         */
+        public Builder removeGroup(final String group) {
+            if (group != null) {
+                this.groups.remove(group);
+            }
+            return this;
+        }
+
+        /**
+         * Clears the builder's set of groups so that it is non-null and size == 0.
+         *
+         * @return the builder
+         */
+        public Builder clearGroups() {
+            this.groups.clear();
+            return this;
+        }
+
+        /**
+         * Sets the action for this builder.
+         *
+         * @param action the action to set
+         * @return the builder
+         */
+        public Builder action(final RequestAction action) {
+            this.action = action;
+            return this;
+        }
+
+        /**
+         * @return a new AccessPolicy constructed from the state of the builder
+         */
+        public AccessPolicy build() {
+            return new AccessPolicy(this);
+        }
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/6f26290d/nifi-registry-core/nifi-registry-security-api/src/main/java/org/apache/nifi/registry/security/authorization/AccessPolicyProvider.java
----------------------------------------------------------------------
diff --git a/nifi-registry-core/nifi-registry-security-api/src/main/java/org/apache/nifi/registry/security/authorization/AccessPolicyProvider.java b/nifi-registry-core/nifi-registry-security-api/src/main/java/org/apache/nifi/registry/security/authorization/AccessPolicyProvider.java
new file mode 100644
index 0000000..214787d
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-security-api/src/main/java/org/apache/nifi/registry/security/authorization/AccessPolicyProvider.java
@@ -0,0 +1,90 @@
+/*
+ * 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.security.authorization;
+
+import org.apache.nifi.registry.security.authorization.exception.AuthorizationAccessException;
+import org.apache.nifi.registry.security.exception.SecurityProviderCreationException;
+import org.apache.nifi.registry.security.exception.SecurityProviderDestructionException;
+
+import java.util.Set;
+
+/**
+ * Provides access to AccessPolicies and the configured UserGroupProvider.
+ *
+ * NOTE: Extensions will be called often and frequently. Because of this, if the underlying implementation needs to
+ * make remote calls or expensive calculations those should probably be done asynchronously and/or cache the results.
+ *
+ * Additionally, extensions need to be thread safe.
+ */
+public interface AccessPolicyProvider {
+
+    /**
+     * Retrieves all access policies. Must be non null
+     *
+     * @return a list of policies
+     * @throws AuthorizationAccessException if there was an unexpected error performing the operation
+     */
+    Set<AccessPolicy> getAccessPolicies() throws AuthorizationAccessException;
+
+    /**
+     * Retrieves the policy with the given identifier.
+     *
+     * @param identifier the id of the policy to retrieve
+     * @return the policy with the given id, or null if no matching policy exists
+     * @throws AuthorizationAccessException if there was an unexpected error performing the operation
+     */
+    AccessPolicy getAccessPolicy(String identifier) throws AuthorizationAccessException;
+
+    /**
+     * Gets the access policies for the specified resource identifier and request action.
+     *
+     * @param resourceIdentifier the resource identifier
+     * @param action the request action
+     * @return the policy matching the resouce and action, or null if no matching policy exists
+     * @throws AuthorizationAccessException if there was any unexpected error performing the operation
+     */
+    AccessPolicy getAccessPolicy(String resourceIdentifier, RequestAction action) throws AuthorizationAccessException;
+
+    /**
+     * Returns the UserGroupProvider for this managed Authorizer. Must be non null
+     *
+     * @return the UserGroupProvider
+     */
+    UserGroupProvider getUserGroupProvider();
+
+    /**
+     * Called immediately after instance creation for implementers to perform additional setup
+     *
+     * @param initializationContext in which to initialize
+     */
+    void initialize(AccessPolicyProviderInitializationContext initializationContext) throws SecurityProviderCreationException;
+
+    /**
+     * Called to configure the Authorizer.
+     *
+     * @param configurationContext at the time of configuration
+     * @throws SecurityProviderCreationException for any issues configuring the provider
+     */
+    void onConfigured(AuthorizerConfigurationContext configurationContext) throws SecurityProviderCreationException;
+
+    /**
+     * Called immediately before instance destruction for implementers to release resources.
+     *
+     * @throws SecurityProviderDestructionException If pre-destruction fails.
+     */
+    void preDestruction() throws SecurityProviderDestructionException;
+}

http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/6f26290d/nifi-registry-core/nifi-registry-security-api/src/main/java/org/apache/nifi/registry/security/authorization/AccessPolicyProviderInitializationContext.java
----------------------------------------------------------------------
diff --git a/nifi-registry-core/nifi-registry-security-api/src/main/java/org/apache/nifi/registry/security/authorization/AccessPolicyProviderInitializationContext.java b/nifi-registry-core/nifi-registry-security-api/src/main/java/org/apache/nifi/registry/security/authorization/AccessPolicyProviderInitializationContext.java
new file mode 100644
index 0000000..92792e9
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-security-api/src/main/java/org/apache/nifi/registry/security/authorization/AccessPolicyProviderInitializationContext.java
@@ -0,0 +1,30 @@
+/*
+ * 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.security.authorization;
+
+/**
+ * Initialization content for AccessPolicyProviders.
+ */
+public interface AccessPolicyProviderInitializationContext extends UserGroupProviderInitializationContext {
+
+    /**
+     * The lookup for accessing other configured AccessPolicyProviders.
+     *
+     * @return  The AccessPolicyProvider lookup
+     */
+    AccessPolicyProviderLookup getAccessPolicyProviderLookup();
+}

http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/6f26290d/nifi-registry-core/nifi-registry-security-api/src/main/java/org/apache/nifi/registry/security/authorization/AccessPolicyProviderLookup.java
----------------------------------------------------------------------
diff --git a/nifi-registry-core/nifi-registry-security-api/src/main/java/org/apache/nifi/registry/security/authorization/AccessPolicyProviderLookup.java b/nifi-registry-core/nifi-registry-security-api/src/main/java/org/apache/nifi/registry/security/authorization/AccessPolicyProviderLookup.java
new file mode 100644
index 0000000..679072a
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-security-api/src/main/java/org/apache/nifi/registry/security/authorization/AccessPolicyProviderLookup.java
@@ -0,0 +1,31 @@
+/*
+ * 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.security.authorization;
+
+/**
+ *
+ */
+public interface AccessPolicyProviderLookup {
+
+    /**
+     * Looks up the AccessPolicyProvider with the specified identifier
+     *
+     * @param identifier        The identifier of the AccessPolicyProvider
+     * @return                  The AccessPolicyProvider
+     */
+    AccessPolicyProvider getAccessPolicyProvider(String identifier);
+}

http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/6f26290d/nifi-registry-core/nifi-registry-security-api/src/main/java/org/apache/nifi/registry/security/authorization/AuthorizationAuditor.java
----------------------------------------------------------------------
diff --git a/nifi-registry-core/nifi-registry-security-api/src/main/java/org/apache/nifi/registry/security/authorization/AuthorizationAuditor.java b/nifi-registry-core/nifi-registry-security-api/src/main/java/org/apache/nifi/registry/security/authorization/AuthorizationAuditor.java
new file mode 100644
index 0000000..ae01ece
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-security-api/src/main/java/org/apache/nifi/registry/security/authorization/AuthorizationAuditor.java
@@ -0,0 +1,30 @@
+/*
+ * 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.security.authorization;
+
+public interface AuthorizationAuditor {
+
+    /**
+     * Audits an authorization request. Will be invoked for any Approved or Denied results. ResourceNotFound
+     * will either re-attempt authorization using a parent resource or will generate a failure result and
+     * audit that.
+     *
+     * @param request the request for authorization
+     * @param result the authorization result
+     */
+    void auditAccessAttempt(final AuthorizationRequest request, final AuthorizationResult result);
+}

http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/6f26290d/nifi-registry-core/nifi-registry-security-api/src/main/java/org/apache/nifi/registry/security/authorization/AuthorizationRequest.java
----------------------------------------------------------------------
diff --git a/nifi-registry-core/nifi-registry-security-api/src/main/java/org/apache/nifi/registry/security/authorization/AuthorizationRequest.java b/nifi-registry-core/nifi-registry-security-api/src/main/java/org/apache/nifi/registry/security/authorization/AuthorizationRequest.java
new file mode 100644
index 0000000..1eb99f9
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-security-api/src/main/java/org/apache/nifi/registry/security/authorization/AuthorizationRequest.java
@@ -0,0 +1,245 @@
+/*
+ * 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.security.authorization;
+
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Objects;
+import java.util.Set;
+import java.util.function.Supplier;
+
+/**
+ * Represents an authorization request for a given user/entity performing an action against a resource within some userContext.
+ */
+public class AuthorizationRequest {
+
+    public static final String DEFAULT_EXPLANATION = "Unable to perform the desired action.";
+
+    private final Resource resource;
+    private final Resource requestedResource;
+    private final String identity;
+    private final Set<String> groups;
+    private final RequestAction action;
+    private final boolean isAccessAttempt;
+    private final boolean isAnonymous;
+    private final Map<String, String> userContext;
+    private final Map<String, String> resourceContext;
+    private final Supplier<String> explanationSupplier;
+
+    private AuthorizationRequest(final Builder builder) {
+        Objects.requireNonNull(builder.resource, "The resource is required when creating an authorization request");
+        Objects.requireNonNull(builder.action, "The action is required when creating an authorization request");
+        Objects.requireNonNull(builder.isAccessAttempt, "Whether this request is an access attempt is request");
+        Objects.requireNonNull(builder.isAnonymous, "Whether this request is being performed by an anonymous user is required");
+
+        this.resource = builder.resource;
+        this.identity = builder.identity;
+        this.groups = builder.groups == null ? null : Collections.unmodifiableSet(builder.groups);
+        this.action = builder.action;
+        this.isAccessAttempt = builder.isAccessAttempt;
+        this.isAnonymous = builder.isAnonymous;
+        this.userContext = builder.userContext == null ? null : Collections.unmodifiableMap(builder.userContext);
+        this.resourceContext = builder.resourceContext == null ? null : Collections.unmodifiableMap(builder.resourceContext);
+        this.explanationSupplier = () -> {
+            final String explanation = builder.explanationSupplier.get();
+
+            // ensure the specified supplier returns non null
+            if (explanation == null) {
+                return DEFAULT_EXPLANATION;
+            } else {
+                return explanation;
+            }
+        };
+
+        if (builder.requestedResource == null) {
+            this.requestedResource = builder.resource;
+        } else {
+            this.requestedResource = builder.requestedResource;
+        }
+    }
+
+    /**
+     * The Resource being authorized. Not null.
+     *
+     * @return The resource
+     */
+    public Resource getResource() {
+        return resource;
+    }
+
+    /**
+     * The original Resource being requested. In cases with inherited policies, this will be a ancestor resource of
+     * of the current resource. The initial request, and cases without inheritance, the requested resource will be
+     * the same as the current resource.
+     *
+     * @return The requested resource
+     */
+    public Resource getRequestedResource() {
+        return requestedResource;
+    }
+
+    /**
+     * The identity accessing the Resource. May be null if the user could not authenticate.
+     *
+     * @return The identity
+     */
+    public String getIdentity() {
+        return identity;
+    }
+
+    /**
+     * The groups the user making this request belongs to. May be null if this NiFi is not configured to load user
+     * groups or empty if the user has no groups
+     *
+     * @return The groups
+     */
+    public Set<String> getGroups() {
+        return groups;
+    }
+
+    /**
+     * Whether this is a direct access attempt of the Resource if if it's being checked as part of another response.
+     *
+     * @return if this is a direct access attempt
+     */
+    public boolean isAccessAttempt() {
+        return isAccessAttempt;
+    }
+
+    /**
+     * Whether the entity accessing is anonymous.
+     *
+     * @return whether the entity is anonymous
+     */
+    public boolean isAnonymous() {
+        return isAnonymous;
+    }
+
+    /**
+     * The action being taken against the Resource. Not null.
+     *
+     * @return The action
+     */
+    public RequestAction getAction() {
+        return action;
+    }
+
+    /**
+     * The userContext of the user request to make additional access decisions. May be null.
+     *
+     * @return  The userContext of the user request
+     */
+    public Map<String, String> getUserContext() {
+        return userContext;
+    }
+
+    /**
+     * The event attributes to make additional access decisions for provenance events. May be null.
+     *
+     * @return  The event attributes
+     */
+    public Map<String, String> getResourceContext() {
+        return resourceContext;
+    }
+
+    /**
+     * A supplier for the explanation if access is denied. Non null.
+     *
+     * @return The explanation supplier if access is denied
+     */
+    public Supplier<String> getExplanationSupplier() {
+        return explanationSupplier;
+    }
+
+    /**
+     * AuthorizationRequest builder.
+     */
+    public static final class Builder {
+
+        private Resource resource;
+        private Resource requestedResource;
+        private String identity;
+        private Set<String> groups;
+        private Boolean isAnonymous;
+        private Boolean isAccessAttempt;
+        private RequestAction action;
+        private Map<String, String> userContext;
+        private Map<String, String> resourceContext;
+        private Supplier<String> explanationSupplier = () -> DEFAULT_EXPLANATION;
+
+        public Builder resource(final Resource resource) {
+            this.resource = resource;
+            return this;
+        }
+
+        public Builder requestedResource(final Resource requestedResource) {
+            this.requestedResource = requestedResource;
+            return this;
+        }
+
+        public Builder identity(final String identity) {
+            this.identity = identity;
+            return this;
+        }
+
+        public Builder groups(final Set<String> groups) {
+            this.groups = groups;
+            return this;
+        }
+
+        public Builder anonymous(final Boolean isAnonymous) {
+            this.isAnonymous = isAnonymous;
+            return this;
+        }
+
+        public Builder accessAttempt(final Boolean isAccessAttempt) {
+            this.isAccessAttempt = isAccessAttempt;
+            return this;
+        }
+
+        public Builder action(final RequestAction action) {
+            this.action = action;
+            return this;
+        }
+
+        public Builder userContext(final Map<String, String> userContext) {
+            if (userContext != null) {
+                this.userContext = new HashMap<>(userContext);
+            }
+            return this;
+        }
+
+        public Builder resourceContext(final Map<String, String> resourceContext) {
+            if (resourceContext != null) {
+                this.resourceContext = new HashMap<>(resourceContext);
+            }
+            return this;
+        }
+
+        public Builder explanationSupplier(final Supplier<String> explanationSupplier) {
+            if (explanationSupplier != null) {
+                this.explanationSupplier = explanationSupplier;
+            }
+            return this;
+        }
+
+        public AuthorizationRequest build() {
+            return new AuthorizationRequest(this);
+        }
+    }
+}

http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/6f26290d/nifi-registry-core/nifi-registry-security-api/src/main/java/org/apache/nifi/registry/security/authorization/AuthorizationResult.java
----------------------------------------------------------------------
diff --git a/nifi-registry-core/nifi-registry-security-api/src/main/java/org/apache/nifi/registry/security/authorization/AuthorizationResult.java b/nifi-registry-core/nifi-registry-security-api/src/main/java/org/apache/nifi/registry/security/authorization/AuthorizationResult.java
new file mode 100644
index 0000000..5f9b55e
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-security-api/src/main/java/org/apache/nifi/registry/security/authorization/AuthorizationResult.java
@@ -0,0 +1,103 @@
+/*
+ * 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.security.authorization;
+
+/**
+ * Represents a decision whether authorization is granted.
+ */
+public class AuthorizationResult {
+
+    public enum Result {
+        Approved,
+        Denied,
+        ResourceNotFound
+    }
+
+    private static final AuthorizationResult APPROVED = new AuthorizationResult(Result.Approved, null);
+    private static final AuthorizationResult RESOURCE_NOT_FOUND = new AuthorizationResult(Result.ResourceNotFound, "Not authorized for the requested resource.");
+
+    private final Result result;
+    private final String explanation;
+
+    /**
+     * Creates a new AuthorizationResult with the specified result and explanation.
+     *
+     * @param result of the authorization
+     * @param explanation for the authorization attempt
+     */
+    private AuthorizationResult(Result result, String explanation) {
+        if (Result.Denied.equals(result) && explanation == null) {
+            throw new IllegalArgumentException("An explanation is required when the authorization request is denied.");
+        }
+
+        if (Result.ResourceNotFound.equals(result) && explanation == null) {
+            throw new IllegalArgumentException("An explanation is required when the authorization request is resource not found.");
+        }
+
+        this.result = result;
+        this.explanation = explanation;
+    }
+
+    /**
+     * @return Whether or not the request is approved
+     */
+    public Result getResult() {
+        return result;
+    }
+
+    /**
+     * @return If the request is denied, the reason why. Null otherwise
+     */
+    public String getExplanation() {
+        return explanation;
+    }
+
+    /**
+     * @return a new approved AuthorizationResult
+     */
+    public static AuthorizationResult approved() {
+        return APPROVED;
+    }
+
+    /**
+     * Resource not found will indicate that there are no specific authorization rules for this resource.
+     * @return a new resource not found AuthorizationResult
+     */
+    public static AuthorizationResult resourceNotFound() {
+        return RESOURCE_NOT_FOUND;
+    }
+
+    /**
+     * Creates a new denied AuthorizationResult with a message indicating 'Access is denied'.
+     *
+     * @return a new denied AuthorizationResult
+     */
+    public static AuthorizationResult denied() {
+        return denied(AuthorizationRequest.DEFAULT_EXPLANATION);
+    }
+
+    /**
+     * Creates a new denied AuthorizationResult with the specified explanation.
+     *
+     * @param explanation for why it was denied
+     * @return a new denied AuthorizationResult with the specified explanation
+     * @throws IllegalArgumentException if explanation is null
+     */
+    public static AuthorizationResult denied(String explanation) {
+        return new AuthorizationResult(Result.Denied, explanation);
+    }
+}

http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/6f26290d/nifi-registry-core/nifi-registry-security-api/src/main/java/org/apache/nifi/registry/security/authorization/Authorizer.java
----------------------------------------------------------------------
diff --git a/nifi-registry-core/nifi-registry-security-api/src/main/java/org/apache/nifi/registry/security/authorization/Authorizer.java b/nifi-registry-core/nifi-registry-security-api/src/main/java/org/apache/nifi/registry/security/authorization/Authorizer.java
new file mode 100644
index 0000000..c8fc0f6
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-security-api/src/main/java/org/apache/nifi/registry/security/authorization/Authorizer.java
@@ -0,0 +1,63 @@
+/*
+ * 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.security.authorization;
+
+import org.apache.nifi.registry.security.authorization.exception.AuthorizationAccessException;
+import org.apache.nifi.registry.security.exception.SecurityProviderCreationException;
+import org.apache.nifi.registry.security.exception.SecurityProviderDestructionException;
+
+/**
+ * Authorizes user requests.
+ */
+public interface Authorizer {
+
+    /**
+     * Determines if the specified user/entity is authorized to access the specified resource within the given context.
+     * These details are all contained in the AuthorizationRequest.
+     *
+     * NOTE: This method will be called often and frequently. Because of this, if the underlying implementation needs to
+     * make remote calls or expensive calculations those should probably be done asynchronously and/or cache the results.
+     *
+     * @param   request The authorization request
+     * @return  the authorization result
+     * @throws  AuthorizationAccessException if unable to access the policies
+     */
+    AuthorizationResult authorize(AuthorizationRequest request) throws AuthorizationAccessException;
+
+    /**
+     * Called immediately after instance creation for implementers to perform additional setup
+     *
+     * @param initializationContext in which to initialize
+     */
+    void initialize(AuthorizerInitializationContext initializationContext) throws SecurityProviderCreationException;
+
+    /**
+     * Called to configure the Authorizer.
+     *
+     * @param configurationContext at the time of configuration
+     * @throws SecurityProviderCreationException for any issues configuring the provider
+     */
+    void onConfigured(AuthorizerConfigurationContext configurationContext) throws SecurityProviderCreationException;
+
+    /**
+     * Called immediately before instance destruction for implementers to release resources.
+     *
+     * @throws SecurityProviderDestructionException If pre-destruction fails.
+     */
+    void preDestruction() throws SecurityProviderDestructionException;
+
+}

http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/6f26290d/nifi-registry-core/nifi-registry-security-api/src/main/java/org/apache/nifi/registry/security/authorization/AuthorizerConfigurationContext.java
----------------------------------------------------------------------
diff --git a/nifi-registry-core/nifi-registry-security-api/src/main/java/org/apache/nifi/registry/security/authorization/AuthorizerConfigurationContext.java b/nifi-registry-core/nifi-registry-security-api/src/main/java/org/apache/nifi/registry/security/authorization/AuthorizerConfigurationContext.java
new file mode 100644
index 0000000..5e33f0c
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-security-api/src/main/java/org/apache/nifi/registry/security/authorization/AuthorizerConfigurationContext.java
@@ -0,0 +1,48 @@
+/*
+ * 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.security.authorization;
+
+import org.apache.nifi.registry.util.PropertyValue;
+
+import java.util.Map;
+
+/**
+ *
+ */
+public interface AuthorizerConfigurationContext {
+
+    /**
+     * @return identifier for the authorizer
+     */
+    String getIdentifier();
+
+    /**
+     * Retrieves all properties the component currently understands regardless
+     * of whether a value has been set for them or not. If no value is present
+     * then its value is null and thus any registered default for the property
+     * descriptor applies.
+     *
+     * @return Map of all properties
+     */
+    Map<String, String> getProperties();
+
+    /**
+     * @param property to lookup the descriptor and value of
+     * @return the value the component currently understands for the given PropertyDescriptor
+     */
+    PropertyValue getProperty(String property);
+}

http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/6f26290d/nifi-registry-core/nifi-registry-security-api/src/main/java/org/apache/nifi/registry/security/authorization/AuthorizerInitializationContext.java
----------------------------------------------------------------------
diff --git a/nifi-registry-core/nifi-registry-security-api/src/main/java/org/apache/nifi/registry/security/authorization/AuthorizerInitializationContext.java b/nifi-registry-core/nifi-registry-security-api/src/main/java/org/apache/nifi/registry/security/authorization/AuthorizerInitializationContext.java
new file mode 100644
index 0000000..55854b3
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-security-api/src/main/java/org/apache/nifi/registry/security/authorization/AuthorizerInitializationContext.java
@@ -0,0 +1,30 @@
+/*
+ * 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.security.authorization;
+
+/**
+ * Initialization content for Authorizers.
+ */
+public interface AuthorizerInitializationContext extends AccessPolicyProviderInitializationContext {
+
+    /**
+     * The lookup for accessing other configured Authorizers.
+     *
+     * @return  The Authorizer lookup
+     */
+    AuthorizerLookup getAuthorizerLookup();
+}

http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/6f26290d/nifi-registry-core/nifi-registry-security-api/src/main/java/org/apache/nifi/registry/security/authorization/AuthorizerLookup.java
----------------------------------------------------------------------
diff --git a/nifi-registry-core/nifi-registry-security-api/src/main/java/org/apache/nifi/registry/security/authorization/AuthorizerLookup.java b/nifi-registry-core/nifi-registry-security-api/src/main/java/org/apache/nifi/registry/security/authorization/AuthorizerLookup.java
new file mode 100644
index 0000000..2c429d9
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-security-api/src/main/java/org/apache/nifi/registry/security/authorization/AuthorizerLookup.java
@@ -0,0 +1,31 @@
+/*
+ * 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.security.authorization;
+
+/**
+ *
+ */
+public interface AuthorizerLookup {
+
+    /**
+     * Looks up the Authorizer with the specified identifier
+     *
+     * @param identifier        The identifier of the Authorizer
+     * @return                  The Authorizer
+     */
+    Authorizer getAuthorizer(String identifier);
+}


[43/51] [partial] nifi-registry git commit: NIFIREG-201 Refactoring project structure to better isolate extensions

Posted by kd...@apache.org.
http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/6f26290d/nifi-registry-core/nifi-registry-docs/src/main/asciidoc/administration-guide.adoc
----------------------------------------------------------------------
diff --git a/nifi-registry-core/nifi-registry-docs/src/main/asciidoc/administration-guide.adoc b/nifi-registry-core/nifi-registry-docs/src/main/asciidoc/administration-guide.adoc
new file mode 100644
index 0000000..53e32cf
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-docs/src/main/asciidoc/administration-guide.adoc
@@ -0,0 +1,1212 @@
+//
+// 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.
+//
+= Apache NiFi Registry System Administrator's Guide
+Apache NiFi Team <de...@nifi.apache.org>
+:homepage: http://nifi.apache.org
+:linkattrs:
+
+== System Requirements
+
+NiFi Registry has the following minimum system requirements:
+
+* Requires Java Development Kit (JDK) 8, newer than 1.8.0_45
+
+WARNING: When running Registry with only a JRE you may encounter the following error as Flyway (database migration tool) attempts to utilize a resource from the JDK: +
+ +
+ `java.lang.RuntimeException: org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'flywayInitializer' defined in class path resource [org/springframework/boot/autoconfigure/flyway/FlywayAutoConfiguration$FlywayConfiguration.class]: Invocation of init method failed; nested exception is org.flywaydb.core.api.FlywayException: Validate failed: Detected failed migration to version 1.3 (DropBucketItemNameUniqueness)`
+
+* Supported Operating Systems:
+** Linux
+** Unix
+** Mac OS X
+* Supported Web Browsers:
+** Google Chrome:  Current & (Current - 1)
+** Mozilla FireFox: Current & (Current - 1)
+** Safari:  Current & (Current - 1)
+
+
+
+== How to install and start NiFi Registry
+
+* Linux/Unix/OS X
+** Decompress and untar into desired installation directory
+** Make any desired edits in files found under `<installdir>/conf`
+** From the `<installdir>/bin` directory, execute the following commands by typing `./nifi-registry.sh <command>`:
+*** `start`: starts NiFi Registry in the background
+*** `stop`: stops NiFi Registry that is running in the background
+*** `status`: provides the current status of NiFi Registry
+*** `run`: runs NiFi Registry in the foreground and waits for a Ctrl-C to initiate shutdown of NiFi Registry
+*** `install`: installs NiFi Registry as a service that can then be controlled via
+**** `service nifi-registry start`
+**** `service nifi-registry stop`
+**** `service nifi-registry status`
+
+
+When NiFi Registry first starts up, the following directories are created:
+
+* `flow_storage`
+* `database`
+* `work`
+* `logs`
+* `run`
+
+See the <<system_properties>> section of this guide for more information about NiFi Registry configuration files.
+
+[[security_configuration]]
+== Security Configuration
+
+NiFi Registry provides several different configuration options for security purposes. The most important properties are those under the
+"security properties" heading in the _nifi-registry.properties_ file. In order to run securely, the following properties must be set:
+
+[options="header,footer"]
+|==================================================================================================================================================
+| Property Name | Description
+|`nifi.registry.security.keystore` | Filename of the Keystore that contains the server's private key.
+|`nifi.registry.security.keystoreType` | The type of Keystore. Must be either `PKCS12` or `JKS`.  JKS is the preferred type, PKCS12 files will be loaded with BouncyCastle provider.
+|`nifi.registry.security.keystorePasswd` | The password for the Keystore.
+|`nifi.registry.security.keyPasswd` | The password for the certificate in the Keystore. If not set, the value of `nifi.registry.security.keystorePasswd` will be used.
+|`nifi.registry.security.truststore` | Filename of the Truststore that will be used to authorize those connecting to NiFi Registry.  A secured instance with no Truststore will refuse all incoming connections.
+|`nifi.registry.security.truststoreType` | The type of the Truststore. Must be either `PKCS12` or `JKS`.  JKS is the preferred type, PKCS12 files will be loaded with BouncyCastle provider.
+|`nifi.registry.security.truststorePasswd` | The password for the Truststore.
+|`nifi.registry.security.needClientAuth` | This specifies that connecting clients must authenticate with a client cert. Setting this to `false` will specify that connecting clients may optionally authenticate with a client cert, but may also login with a username and password against a configured identity provider. The default value is `true`.
+|==================================================================================================================================================
+
+Once the above properties have been configured, we can enable the User Interface to be accessed over HTTPS instead of HTTP. This is accomplished
+by setting the `nifi.registry.web.https.host` and `nifi.registry.web.https.port` properties. The `nifi.registry.web.https.host` property indicates which hostname the server
+should run on. If it is desired that the HTTPS interface be accessible from all network interfaces, a value of `0.0.0.0` should be used for `nifi.registry.web.https.host`.
+
+NOTE: It is important when enabling HTTPS that the `nifi.registry.web.http.port` property be unset.
+
+[[user_authentication]]
+== User Authentication
+
+A secured instance of NiFi Registry cannot be accessed anonymously, so a method of user authentication must be configured.
+
+NOTE: NiFi Registry does not perform user authentication over HTTP. Using HTTP, all users will have full permissions.
+
+Any secured instance of NiFi Registry supports authentication via client certificates that are trusted by the NiFi Registry's SSL Context Truststore.
+Alternatively, a secured NiFi Registry can be configured to authenticate users via username/password.
+
+Username/password authentication is performed by an 'Identity Provider'. The Identity Provider is a pluggable mechanism for
+authenticating users via their username/password. Which Identity Provider to use is configured in the _nifi-registry.properties_ file.
+Currently NiFi Registry offers Identity Providers for LDAP and Kerberos.
+
+Identity Providers are configured using two properties in the _nifi-registry.properties_ file:
+
+* The `nifi.registry.security.identity.providers.configuration.file` property specifies the configuration file where identity providers are defined.  By default, the _identity-providers.xml_ file located in the root installation `conf` directory is selected.
+* The `nifi.registry.security.identity.provider` property indicates which of the configured identity providers in the _identity-providers.xml_ file to use. By default, this property is not configured meaning that username/password must be explicitly enabled.
+
+NOTE: NiFi Registry can only be configured to use one Identity Provider at a given time.
+
+[[ldap_identity_provider]]
+=== Lightweight Directory Access Protocol (LDAP)
+
+Below is an example and description of configuring a Identity Provider that integrates with a Directory Server to authenticate users.
+
+Set the following in _nifi-registry.properties_ to enable LDAP username/password authentication:
+
+----
+nifi.registry.security.identity.provider=ldap-identity-provider
+----
+
+Modify _identity-providers.xml_ to enable the `ldap-identity-provider`. Here is the sample provided in the file:
+
+----
+<provider>
+    <identifier>ldap-identity-provider</identifier>
+    <class>org.apache.nifi.registry.security.ldap.LdapIdentityProvider</class>
+    <property name="Authentication Strategy">START_TLS</property>
+
+    <property name="Manager DN"></property>
+    <property name="Manager Password"></property>
+
+    <property name="TLS - Keystore"></property>
+    <property name="TLS - Keystore Password"></property>
+    <property name="TLS - Keystore Type"></property>
+    <property name="TLS - Truststore"></property>
+    <property name="TLS - Truststore Password"></property>
+    <property name="TLS - Truststore Type"></property>
+    <property name="TLS - Client Auth"></property>
+    <property name="TLS - Protocol"></property>
+    <property name="TLS - Shutdown Gracefully"></property>
+
+    <property name="Referral Strategy">FOLLOW</property>
+    <property name="Connect Timeout">10 secs</property>
+    <property name="Read Timeout">10 secs</property>
+
+    <property name="Url"></property>
+    <property name="User Search Base"></property>
+    <property name="User Search Filter"></property>
+
+    <property name="Identity Strategy">USE_DN</property>
+    <property name="Authentication Expiration">12 hours</property>
+</provider>
+----
+
+The `ldap-identity-provider` has the following properties:
+
+[options="header,footer"]
+|==================================================================================================================================================
+| Property Name | Description
+|`Authentication Strategy` | How the connection to the LDAP server is authenticated. Possible values are `ANONYMOUS`, `SIMPLE`, `LDAPS`, or `START_TLS`.
+|`Manager DN` | The DN of the manager that is used to bind to the LDAP server to search for users.
+|`Manager Password` | The password of the manager that is used to bind to the LDAP server to search for users.
+|`TLS - Keystore` | Path to the Keystore that is used when connecting to LDAP using LDAPS or START_TLS.
+|`TLS - Keystore Password` | Password for the Keystore that is used when connecting to LDAP using LDAPS or START_TLS.
+|`TLS - Keystore Type` | Type of the Keystore that is used when connecting to LDAP using LDAPS or START_TLS (i.e. `JKS` or `PKCS12`).
+|`TLS - Truststore` | Path to the Truststore that is used when connecting to LDAP using LDAPS or START_TLS.
+|`TLS - Truststore Password` | Password for the Truststore that is used when connecting to LDAP using LDAPS or START_TLS.
+|`TLS - Truststore Type` | Type of the Truststore that is used when connecting to LDAP using LDAPS or START_TLS (i.e. `JKS` or `PKCS12`).
+|`TLS - Client Auth` | Client authentication policy when connecting to LDAP using LDAPS or START_TLS. Possible values are `REQUIRED`, `WANT`, `NONE`.
+|`TLS - Protocol` | Protocol to use when connecting to LDAP using LDAPS or START_TLS. (i.e. `TLS`, `TLSv1.1`, `TLSv1.2`, etc).
+|`TLS - Shutdown Gracefully` | Specifies whether the TLS should be shut down gracefully before the target context is closed. Defaults to `false`.
+|`Referral Strategy` | Strategy for handling referrals. Possible values are `FOLLOW`, `IGNORE`, `THROW`.
+|`Connect Timeout` | Duration of connect timeout. (i.e. `10 secs`).
+|`Read Timeout` | Duration of read timeout. (i.e. `10 secs`).
+|`Url` | Space-separated list of URLs of the LDAP servers (i.e. `ldap://<hostname>:<port>`).
+|`User Search Base` | Base DN for searching for users (i.e. `CN=Users,DC=example,DC=com`).
+|`User Search Filter` | Filter for searching for users against the `User Search Base`. (i.e. `sAMAccountName={0}`). The user specified name is inserted into '{0}'.
+|`Identity Strategy` | Strategy to identify users. Possible values are `USE_DN` and `USE_USERNAME`. The default functionality if this property is missing is `USE_DN` in order to retain backward
+compatibility. `USE_DN` will use the full DN of the user entry if possible. `USE_USERNAME` will use the username the user logged in with.
+|`Authentication Expiration` | The duration of how long the user authentication is valid for. If the user never logs out, they will be required to log back in following this duration.
+|==================================================================================================================================================
+
+[[kerberos_identity_provider]]
+=== Kerberos
+
+Below is an example and description of configuring an Identity Provider that integrates with a Kerberos Key Distribution Center (KDC) to authenticate users.
+
+Set the following in _nifi-registry.properties_ to enable Kerberos username/password authentication:
+
+----
+nifi.registry.security.user.identity.provider=kerberos-identity-provider
+----
+
+Modify _identity-providers.xml_ to enable the `kerberos-identity-provider`. Here is the sample provided in the file:
+
+----
+<provider>
+    <identifier>kerberos-identity-provider</identifier>
+    <class>org.apache.nifi.registry.web.security.authentication.kerberos.KerberosIdentityProvider</class>
+    <property name="Default Realm">NIFI.APACHE.ORG</property>
+    <property name="Authentication Expiration">12 hours</property>
+    <property name="Enable Debug">false</property>
+</provider>
+----
+
+The `kerberos-identity-provider` has the following properties:
+
+[options="header,footer"]
+|==================================================================================================================================================
+| Property Name | Description
+|`Default Realm` | Default realm to provide when user enters incomplete user principal (i.e. `NIFI.APACHE.ORG`).
+|`Authentication Expiration`| The duration for which the user authentication is valid. If the user never logs out, they will be required to log back in following this duration.
+|`Enable Debug`| Enables debug logging output for the SunJaasKerberosClient used internally by the KerberosIdentityProvider.  By default, this is set to `false`.
+|==================================================================================================================================================
+
+See also <<kerberos_service>> to allow single sign-on access via client Kerberos tickets.
+
+[[authorization]]
+== Authorization
+
+After you have configured NiFi Registry to run securely and with an authentication mechanism, you must configure who has access to the system and their level of access.
+This is done by defining policies that give users and groups permissions to perform a particular action. These policies are defined in an 'authorizer'.
+
+[[authorizer-configuration]]
+=== Authorizer Configuration
+
+An 'authorizer' manages known users and their access policies. Authorizers are configured using two properties in the _nifi-registry.properties_ file:
+
+* The `nifi.registry.security.authorizers.configuration.file` property specifies the configuration file where authorizers are defined.  By default, the _authorizers.xml_ file located in the root installation conf directory is selected.
+* The `nifi.registry.security.authorizer` property indicates which of the configured authorizers in the _authorizers.xml_ file to use.
+
+[[authorizers-setup]]
+=== Authorizers.xml Setup
+
+The _authorizers.xml_ file is used to define and configure available authorizers.
+
+==== StandardManagedAuthorizer
+The default Authorizer is the StandardManagedAuthorizer, however, you can develop additional Authorizers as extensions. The StandardManagedAuthorizer has the following properties:
+
+[options="header,footer"]
+|==================================================================================================================================================
+| Property Name | Description
+|`Access Policy Provider` | The identifier for an Access Policy Provider defined above.
+|==================================================================================================================================================
+
+The managed authorizer is comprised of a UserGroupProvider and a AccessPolicyProvider.  The users, group, and access policies will be loaded and optionally configured through these providers.  The managed authorizer will make all access decisions based on these provided users, groups, and access policies.
+
+During startup there is a check to ensure that there are no two users/groups with the same identity/name. This check is executed regardless of the configured implementation. This is necessary because this is how users/groups are identified and authorized during access decisions.
+
+==== UserGroupProvider
+
+===== FileUserGroupProvider
+
+The default UserGroupProvider is the FileUserGroupProvider, however, you can develop additional UserGroupProviders as extensions.  The FileUserGroupProvider has the following properties:
+
+[options="header,footer"]
+|==================================================================================================================================================
+| Property Name | Description
+|`Users File` | The file where the FileUserGroupProvider stores users and groups.
+  By default, _users.xml_ in the `conf` directory is chosen.
+|`Initial User Identity`| The identity of a user or system to seed an empty Users File.
+  Multiple Initial User Identity properties can be specified, but the name of each property must be unique, for example: ``"Initial User Identity A"``, ``"Initial User Identity B"``, ``"Initial User Identity C"`` or ``"Initial User Identity 1"``, ``"Initial User Identity 2"``, ``"Initial User Identity 3"``.
+|==================================================================================================================================================
+
+NOTE: Initial User Identities are only created if the specified Users File is missing or empty during NiFi Registry startup. Changes to the configured Initial Users Identities will not take effect if the Users File is populated.
+
+===== LdapUserGroupProvider
+
+Another option for the UserGroupProvider is the LdapUserGroupProvider. By default, this option is commented out but can be configured in lieu of the FileUserGroupProvider.
+This will sync users and groups from a directory server and will present them in NiFi Registry UI in read only form. The LdapUserGroupProvider has the following properties:
+
+[options="header,footer"]
+|==================================================================================================================================================
+| Property Name | Description
+|`Authentication Strategy` | How the connection to the LDAP server is authenticated. Possible values are `ANONYMOUS`, `SIMPLE`, `LDAPS`, or `START_TLS`.
+|`Manager DN`| The DN of the manager that is used to bind to the LDAP server to search for users.
+|`Manager Password`| The password of the manager that is used to bind to the LDAP server to search for users.
+|`TLS - Keystore` | Path to the Keystore that is used when connecting to LDAP using LDAPS or START_TLS.
+|`TLS - Keystore Password`| Password for the Keystore that is used when connecting to LDAP using LDAPS or START_TLS.
+|`TLS - Keystore Type`| Type of the Keystore that is used when connecting to LDAP using LDAPS or START_TLS (i.e. `JKS` or `PKCS12`).
+|`TLS - Truststore` | Path to the Truststore that is used when connecting to LDAP using LDAPS or START_TLS.
+|`TLS - Truststore Password`| Password for the Truststore that is used when connecting to LDAP using LDAPS or START_TLS.
+|`TLS - Truststore Type`| Type of the Truststore that is used when connecting to LDAP using LDAPS or START_TLS (i.e. `JKS` or `PKCS12`).
+|`TLS - Client Auth`| Client authentication policy when connecting to LDAP using LDAPS or START_TLS. Possible values are `REQUIRED`, `WANT`, `NONE`.
+|`TLS - Protocol`| Protocol to use when connecting to LDAP using LDAPS or START_TLS. (i.e. `TLS`, `TLSv1.1`, `TLSv1.2`, etc).
+|`TLS - Shutdown Gracefully`| Specifies whether the TLS should be shut down gracefully before the target context is closed. Defaults to `false`.
+|`Referral Strategy`| Strategy for handling referrals. Possible values are `FOLLOW`, `IGNORE`, `THROW`.
+|`Connect Timeout`| Duration of connect timeout. (i.e. `10 secs`).
+|`Read Timeout`| Duration of read timeout. (i.e. `10 secs`).
+|`Url`| Space-separated list of URLs of the LDAP servers (i.e. `ldap://<hostname>:<port>`).
+|`Page Size`| Sets the page size when retrieving users and groups. If not specified, no paging is performed.
+|`Sync Interval`| Duration of time between syncing users and groups. (i.e. `30 mins`).
+|`User Search Base`| Base DN for searching for users (i.e. `ou=users,o=nifi`). Required to search users.
+|`User Object Class`| Object class for identifying users (i.e. `person`). Required if searching users.
+|`User Search Scope`| Search scope for searching users (`ONE_LEVEL`, `OBJECT`, or `SUBTREE`). Required if searching users.
+|`User Search Filter`| Filter for searching for users against the `User Search Base` (i.e. `(memberof=cn=team1,ou=groups,o=nifi)`). Optional.
+|`User Identity Attribute`| Attribute to use to extract user identity (i.e. `cn`). Optional. If not set, the entire DN is used.
+|`User Group Name Attribute`| Attribute to use to define group membership (i.e. `memberof`). Optional. If not set group membership will not be calculated through the users. Will rely on group membership being defined through `Group Member Attribute` if set. The value of this property is the name of the attribute in the user LDAP entry that associates them with a group. The value of that user attribute could be a dn or group name for instance. What value is expected is configured in the `User Group Name Attribute - Referenced Group Attribute`.
+|`User Group Name Attribute - Referenced Group Attribute`|  If blank, the value of the attribute defined in `User Group Name Attribute` is expected to be the full dn of the group. If not blank, this property will define the attribute of the group LDAP entry that the value of the attribute defined in `User Group Name Attribute` is referencing (i.e. `name`). Use of this property requires that `Group Search Base` is also configured.
+|`Group Search Base`| Base DN for searching for groups (i.e. `ou=groups,o=nifi`). Required to search groups.
+|`Group Object Class`| Object class for identifying groups (i.e. `groupOfNames`). Required if searching groups.
+|`Group Search Scope`| Search scope for searching groups (`ONE_LEVEL`, `OBJECT`, or `SUBTREE`). Required if searching groups.
+|`Group Search Filter`| Filter for searching for groups against the `Group Search Base`. Optional.
+|`Group Name Attribute`| Attribute to use to extract group name (i.e. `cn`). Optional. If not set, the entire DN is used.
+|`Group Member Attribute`| Attribute to use to define group membership (i.e. `member`). Optional. If not set group membership will not be calculated through the groups. Will rely on group membership being defined through `User Group Name Attribute` if set. The value of this property is the name of the attribute in the group LDAP entry that associates them with a user. The value of that group attribute could be a dn or memberUid for instance. What value is expected is configured in the `Group Member Attribute - Referenced User Attribute`. (i.e. `member: cn=User 1,ou=users,o=nifi` vs. `memberUid: user1`)
+|`Group Member Attribute - Referenced User Attribute`| If blank, the value of the attribute defined in `Group Member Attribute` is expected to be the full dn of the user. If not blank, this property will define the attribute of the user LDAP entry that the value of the attribute defined in `Group Member Attribute` is referencing (i.e. `uid`). Use of this property requires that `User Search Base` is also configured. (i.e. `member: cn=User 1,ou=users,o=nifi` vs. `memberUid: user1`)
+|==================================================================================================================================================
+
+===== Composite Implementations
+
+Another option for the UserGroupProvider are composite implementations. This means that multiple sources/implementations can be configured and composed. For instance, an admin can configure users/groups to be loaded from a file and a directory server. There are two composite implementations, one that supports multiple UserGroupProviders and one that supports multiple UserGroupProviders and a single configurable UserGroupProvider.
+
+The CompositeUserGroupProvider will provide support for retrieving users and groups from multiple sources. The CompositeUserGroupProvider has the following properties:
+
+[options="header,footer"]
+|==================================================================================================================================================
+| Property Name | Description
+|`User Group Provider` | The identifier of user group providers to load from. The name of each property must be unique, for example: ``"User Group Provider A"``, ``"User Group Provider B"``, ``"User Group Provider C"`` or ``"User Group Provider 1"``, ``"User Group Provider 2"``, ``"User Group Provider 3"``
+|==================================================================================================================================================
+
+The CompositeConfigurableUserGroupProvider will provide support for retrieving users and groups from multiple sources. Additionally, a single configurable user group provider is required. Users from the configurable user group provider are configurable, however users loaded from one of the User Group Provider [unique key] will not be. The CompositeConfigurableUserGroupProvider has the following properties:
+
+[options="header,footer"]
+|==================================================================================================================================================
+| Property Name | Description
+|`Configurable User Group Provider` | A configurable user group provider.
+|`User Group Provider` | The identifier of user group providers to load from. The name of each property must be unique, for example: ``"User Group Provider A"``, ``"User Group Provider B"``, ``"User Group Provider C"`` or ``"User Group Provider 1"``, ``"User Group Provider 2"``, ``"User Group Provider 3"``
+|==================================================================================================================================================
+
+==== AccessPolicyProvider
+
+After you have configured a UserGroupProvider, you must configure an AccessPolicyProvider that will control Access Policies for the identities in the UserGroupProvider.
+
+===== FileAccessPolicyProvider
+
+The default AccessPolicyProvider is the FileAccessPolicyProvider, however, you can develop additional AccessPolicyProvider as extensions. The FileAccessPolicyProvider has the following properties:
+
+[options="header,footer"]
+|==================================================================================================================================================
+| Property Name | Description
+|`User Group Provider` | The identifier for an User Group Provider defined above that will be used to access users and groups for use in the managed access policies.
+|`Authorizations File`| The file where the FileAccessPolicyProvider will store policies. By default, _authorizations.xml_ in the `conf` directory is chosen.
+|`Initial Admin Identity`| The identity of an initial admin user that will be granted access to the UI and given the ability to create additional users, groups, and policies. For example, a certificate DN, LDAP identity, or Kerberos principal.
+|`NiFi Identity`| The identity of a NiFi instance/node that will be accessing this registry. Each NiFi Identity will be granted permission to proxy user requests, as well as read any bucket to perform synchronization status checks.
+|==================================================================================================================================================
+
+NOTE: The identities configured in the Initial Admin Identity and NiFi Identity properties must be available in the configured User Group Provider. Initial Admin Identity and NiFi Identity properties are only read by NiFi Registry when the Authorizations File is missing or empty on startup in order to seed the initial Authorizations File.
+Changes to the configured Initial Admin Identity and NiFi Identities will not take effect if the Authorizations File is populated.
+
+[[initial-admin-identity]]
+==== Initial Admin Identity  (New NiFi Registry Instance)
+
+If you are setting up a secured NiFi Registry instance for the first time, you must manually designate an “Initial Admin Identity” in the _authorizers.xml_ file.
+This initial admin user is granted access to the UI and given the ability to create additional users, groups, and policies.
+The value of this property could be a certificate DN , LDAP identity (DN or username), or a Kerberos principal.
+If you are the NiFi Registry administrator, add yourself as the “Initial Admin Identity”.
+
+After you have edited and saved the _authorizers.xml_ file, restart NiFi Registry.
+The _users.xml_ and _authorizations.xml_ files will be created, and the “Initial Admin Identity” user and administrative policies are added during start up.
+Once NiFi Registry starts, the “Initial Admin Identity” user is able to access the UI and begin managing users, groups, and policies.
+
+NOTE: If initial NiFi identities are not provided, they can be added through the UI at a later time by first creating a user for the given
+NiFi identity, and then giving that user both Proxy permissions and permission to Buckets/READ in order to read all buckets.
+
+Some common use cases are described below.
+
+===== File-based (LDAP Authentication)
+Here is an example certificate DN entry using the name John Smith:
+
+----
+<authorizers>
+
+    <userGroupProvider>
+        <identifier>file-user-group-provider</identifier>
+        <class>org.apache.nifi.registry.security.authorization.file.FileUserGroupProvider</class>
+        <property name="Users File">./conf/users.xml</property>
+        <property name="Legacy Authorized Users File"></property>
+        <property name="Initial User Identity 1">cn=John Smith,ou=people,dc=example,dc=com</property>
+    </userGroupProvider>
+
+    <accessPolicyProvider>
+        <identifier>file-access-policy-provider</identifier>
+        <class>org.apache.nifi.registry.security.authorization.file.FileAccessPolicyProvider</class>
+        <property name="User Group Provider">file-user-group-provider</property>
+        <property name="Authorizations File">./conf/authorizations.xml</property>
+        <property name="Initial Admin Identity">cn=John Smith,ou=people,dc=example,dc=com</property
+        <property name="NiFi Identity 1"></property>
+    </accessPolicyProvider>
+
+    <authorizer>
+        <identifier>managed-authorizer</identifier>
+        <class>org.apache.nifi.registry.security.authorization.StandardManagedAuthorizer</class>
+        <property name="Access Policy Provider">file-access-policy-provider</property>
+    </authorizer>
+</authorizers>
+----
+
+===== File-based (Kerberos Authentication)
+Here is an example Kerberos entry using the name John Smith and realm `NIFI.APACHE.ORG`:
+
+----
+<authorizers>
+
+    <userGroupProvider>
+        <identifier>file-user-group-provider</identifier>
+        <class>org.apache.nifi.registry.security.authorization.file.FileUserGroupProvider</class>
+        <property name="Users File">./conf/users.xml</property>
+        <property name="Initial User Identity 1">johnsmith@NIFI.APACHE.ORG</property>
+    </userGroupProvider>
+
+    <accessPolicyProvider>
+        <identifier>file-access-policy-provider</identifier>
+        <class>org.apache.nifi.registry.security.authorization.file.FileAccessPolicyProvider</class>
+        <property name="User Group Provider">file-user-group-provider</property>
+        <property name="Authorizations File">./conf/authorizations.xml</property>
+        <property name="Initial Admin Identity">johnsmith@NIFI.APACHE.ORG</property>
+        <property name="NiFi Identity 1"></property>
+    </accessPolicyProvider>
+
+    <authorizer>
+        <identifier>managed-authorizer</identifier>
+        <class>org.apache.nifi.registry.security.authorization.StandardManagedAuthorizer</class>
+        <property name="Access Policy Provider">file-access-policy-provider</property>
+    </authorizer>
+</authorizers>
+----
+
+===== LDAP-based Users/Groups Referencing User DN
+Here is an example loading users and groups from LDAP. Group membership will be driven through the member attribute of each group.
+Authorization will still use file-based access policies.
+
+Given the following LDAP entries exist:
+
+----
+dn: cn=User 1,ou=users,o=nifi
+objectClass: organizationalPerson
+objectClass: person
+objectClass: inetOrgPerson
+objectClass: top
+cn: User 1
+sn: User1
+uid: user1
+
+dn: cn=User 2,ou=users,o=nifi
+objectClass: organizationalPerson
+objectClass: person
+objectClass: inetOrgPerson
+objectClass: top
+cn: User 2
+sn: User2
+uid: user2
+
+dn: cn=users,ou=groups,o=nifi
+objectClass: groupOfNames
+objectClass: top
+cn: users
+member: cn=User 1,ou=users,o=nifi
+member: cn=User 2,ou=users,o=nifi
+----
+
+An Authorizer using an LdapUserGroupProvider would be configured as:
+
+----
+<authorizers>
+    <userGroupProvider>
+        <identifier>ldap-user-group-provider</identifier>
+        <class>org.apache.nifi.registry.security.ldap.tenants.LdapUserGroupProvider</class>
+        <property name="Authentication Strategy">ANONYMOUS</property>
+
+        <property name="Manager DN"></property>
+        <property name="Manager Password"></property>
+
+        <property name="TLS - Keystore"></property>
+        <property name="TLS - Keystore Password"></property>
+        <property name="TLS - Keystore Type"></property>
+        <property name="TLS - Truststore"></property>
+        <property name="TLS - Truststore Password"></property>
+        <property name="TLS - Truststore Type"></property>
+        <property name="TLS - Client Auth"></property>
+        <property name="TLS - Protocol"></property>
+        <property name="TLS - Shutdown Gracefully"></property>
+
+        <property name="Referral Strategy">FOLLOW</property>
+        <property name="Connect Timeout">10 secs</property>
+        <property name="Read Timeout">10 secs</property>
+
+        <property name="Url">ldap://localhost:10389</property>
+        <property name="Page Size"></property>
+        <property name="Sync Interval">30 mins</property>
+
+        <property name="User Search Base">ou=users,o=nifi</property>
+        <property name="User Object Class">person</property>
+        <property name="User Search Scope">ONE_LEVEL</property>
+        <property name="User Search Filter"></property>
+        <property name="User Identity Attribute">cn</property>
+        <property name="User Group Name Attribute"></property>
+        <property name="User Group Name Attribute - Referenced Group Attribute"></property>
+
+        <property name="Group Search Base">ou=groups,o=nifi</property>
+        <property name="Group Object Class">groupOfNames</property>
+        <property name="Group Search Scope">ONE_LEVEL</property>
+        <property name="Group Search Filter"></property>
+        <property name="Group Name Attribute">cn</property>
+        <property name="Group Member Attribute">member</property>
+        <property name="Group Member Attribute - Referenced User Attribute"></property>
+    </userGroupProvider>
+
+    <accessPolicyProvider>
+        <identifier>file-access-policy-provider</identifier>
+        <class>org.apache.nifi.registry.security.authorization.file.FileAccessPolicyProvider</class>
+        <property name="User Group Provider">ldap-user-group-provider</property>
+        <property name="Authorizations File">./conf/authorizations.xml</property>
+        <property name="Initial Admin Identity">User 1</property>
+        <property name="NiFi Identity 1"></property>
+    </accessPolicyProvider>
+
+    <authorizer>
+        <identifier>managed-authorizer</identifier>
+        <class>org.apache.nifi.registry.security.authorization.StandardManagedAuthorizer</class>
+        <property name="Access Policy Provider">file-access-policy-provider</property>
+    </authorizer>
+</authorizers>
+----
+
+The `Initial Admin Identity` value would have loaded from the cn of the User 1 entry based on the `User Identity Attribute` value.
+
+===== Composite - File and LDAP-based Users/Groups
+Here is an example composite implementation loading users and groups from LDAP and a local file. Group membership will be driven through
+the member attribute of each group. The users from LDAP will be read only while the users loaded from the file will be configurable in UI.
+
+----
+<authorizers>
+
+    <userGroupProvider>
+        <identifier>file-user-group-provider</identifier>
+        <class>org.apache.nifi.registry.security.authorization.file.FileUserGroupProvider</class>
+        <property name="Users File">./conf/users.xml</property>
+        <property name="Initial User Identity 1">cn=nifi-node1,ou=servers,dc=example,dc=com</property>
+        <property name="Initial User Identity 2">cn=nifi-node2,ou=servers,dc=example,dc=com</property>
+    </userGroupProvider>
+
+    <userGroupProvider>
+        <identifier>ldap-user-group-provider</identifier>
+        <class>org.apache.nifi.registry.security.ldap.tenants.LdapUserGroupProvider</class>
+        <property name="Authentication Strategy">ANONYMOUS</property>
+
+        <property name="Manager DN"></property>
+        <property name="Manager Password"></property>
+
+        <property name="TLS - Keystore"></property>
+        <property name="TLS - Keystore Password"></property>
+        <property name="TLS - Keystore Type"></property>
+        <property name="TLS - Truststore"></property>
+        <property name="TLS - Truststore Password"></property>
+        <property name="TLS - Truststore Type"></property>
+        <property name="TLS - Client Auth"></property>
+        <property name="TLS - Protocol"></property>
+        <property name="TLS - Shutdown Gracefully"></property>
+
+        <property name="Referral Strategy">FOLLOW</property>
+        <property name="Connect Timeout">10 secs</property>
+        <property name="Read Timeout">10 secs</property>
+
+        <property name="Url">ldap://localhost:10389</property>
+        <property name="Page Size"></property>
+        <property name="Sync Interval">30 mins</property>
+
+        <property name="User Search Base">ou=users,o=nifi</property>
+        <property name="User Object Class">person</property>
+        <property name="User Search Scope">ONE_LEVEL</property>
+        <property name="User Search Filter"></property>
+        <property name="User Identity Attribute">cn</property>
+        <property name="User Group Name Attribute"></property>
+        <property name="User Group Name Attribute - Referenced Group Attribute"></property>
+
+        <property name="Group Search Base">ou=groups,o=nifi</property>
+        <property name="Group Object Class">groupOfNames</property>
+        <property name="Group Search Scope">ONE_LEVEL</property>
+        <property name="Group Search Filter"></property>
+        <property name="Group Name Attribute">cn</property>
+        <property name="Group Member Attribute">member</property>
+        <property name="Group Member Attribute - Referenced User Attribute"></property>
+    </userGroupProvider>
+
+    <userGroupProvider>
+        <identifier>composite-user-group-provider</identifier>
+        <class>org.apache.nifi.registry.security.authorization.CompositeUserGroupProvider</class>
+        <property name="User Group Provider 1">file-user-group-provider</property>
+        <property name="User Group Provider 2">ldap-user-group-provider</property>
+    </userGroupProvider>
+
+    <accessPolicyProvider>
+        <identifier>file-access-policy-provider</identifier>
+        <class>org.apache.nifi.registry.security.authorization.file.FileAccessPolicyProvider</class>
+        <property name="User Group Provider">composite-user-group-provider</property>
+        <property name="Authorizations File">./conf/authorizations.xml</property>
+        <property name="Initial Admin Identity">User 1/property>
+        <property name="NiFi Identity 1">cn=nifi-node1,ou=servers,dc=example,dc=com</property>
+        <property name="NiFi Identity 2">cn=nifi-node2,ou=servers,dc=example,dc=com</property>
+    </accessPolicyProvider>
+
+    <authorizer>
+        <identifier>managed-authorizer</identifier>
+        <class>org.apache.nifi.registry.security.authorization.StandardManagedAuthorizer</class>
+        <property name="Access Policy Provider">file-access-policy-provider</property>
+    </authorizer>
+</authorizers>
+----
+
+In this example, the users and groups are loaded from LDAP but the servers are managed in a local file. The `Initial Admin Identity` value came
+from an attribute in a LDAP entry based on the `User Identity Attribute`. The `NiFi Identity` values are established in the local file using the
+`Initial User Identity` properties.
+
+
+== Encrypted Passwords in Configuration Files
+
+In order to facilitate the secure setup of NiFi Registry, you can use the `encrypt-config` command line utility to encrypt raw configuration values
+that NiFi Registry decrypts in memory on startup. This extensible protection scheme transparently allows NiFi Registry to use raw values in operation,
+while protecting them at rest.  In the future, hardware security modules (HSM) and external secure storage mechanisms will be integrated, but for now,
+an AES encryption provider is the default implementation.
+
+If no administrator action is taken, the configuration values remain unencrypted.
+
+NOTE: The `encrypt-config` tool for NiFi Registry is implemented as an additional mode to the existing tool in the `nifi-toolkit`. The following sections
+assume you have downloaded the binary for the nifi-toolkit.
+
+[[encrypt-config_tool]]
+=== Encrypt-Config Tool
+
+The `encrypt-config` command line tool can be used to encrypt NiFi Registry configuration by invoking the tool with the following command:
+
+----
+./bin/encrypt-config --nifiRegistry [options]
+----
+
+You can use the following command line options with the `encrypt-config` tool:
+
+ * `-h`,`--help`                                  Show usage information (this message)
+ * `-v`,`--verbose`                               Enables verbose mode (off by default)
+ * `-p`,`--password <password>`                   Protect the files using a password-derived key. If an argument is not provided to this flag, interactive mode will be triggered to prompt the user to enter the password.
+ * `-k`,`--key <keyhex>`                          Protect the files using a raw hexadecimal key. If an argument is not provided to this flag, interactive mode will be triggered to prompt the user to enter the key.
+ * `--oldPassword <password>`                     If the input files are already protected using a password-derived key, this specifies the old password so that the files can be unprotected before re-protecting.
+ * `--oldKey <keyhex>`                            If the input files are already protected using a key, this specifies the raw hexadecimal key so that the files can be unprotected before re-protecting.
+ * `-b`,`--bootstrapConf <file>`                  The _bootstrap.conf_ file containing no master key or an existing master key. If a new password/key is specified and no output bootstrap.conf file is specified, then this file will be overwritten to persist the new master key.
+ * `-B`,`--outputBootstrapConf <file>`            The destination _bootstrap.conf_ file to persist master key. If specified, the input _bootstrap.conf_ will not be modified.
+ * `-r`,`--nifiRegistryProperties <file>`         The _nifi-registry.properties_ file containing unprotected config values, overwritten if no output file specified.
+ * `-R`,`--outputNifiRegistryProperties <file>`   The destination _nifi-registry.properties_ file containing protected config values.
+ * `-a`,`--authorizersXml <file>`                 The _authorizers.xml_ file containing unprotected config values, overwritten if no output file specified.
+ * `-A`,`--outputAuthorizersXml <file>`           The destination _authorizers.xml_ file containing protected config values.
+ * `-i`,`--identityProvidersXml <file>`           The _identity-providers.xml_ file containing unprotected config values, overwritten if no output file specified.
+ * `-I`,`--outputIdentityProvidersXml <file>`     The destination _identity-providers.xml_ file containing protected config values.
+
+
+As an example of how the tool works, assume that you have installed the tool on a machine supporting 256-bit encryption and with the following existing values in the _nifi-registry.properties_ file:
+
+----
+# security properties #
+nifi.registry.security.keystore=/path/to/keystore.jks
+nifi.registry.security.keystoreType=JKS
+nifi.registry.security.keystorePasswd=thisIsABadKeystorePassword
+nifi.registry.security.keyPasswd=thisIsABadKeyPassword
+nifi.registry.security.truststore=
+nifi.registry.security.truststoreType=
+nifi.registry.security.truststorePasswd=
+----
+
+Enter the following arguments when using the tool:
+
+----
+./bin/encrypt-config.sh nifi-registry \
+-b bootstrap.conf \
+-k 0123456789ABCDEFFEDCBA98765432100123456789ABCDEFFEDCBA9876543210 \
+-r nifi-registry.properties
+----
+
+As a result, the _nifi-registry.properties_ file is overwritten with protected properties and sibling encryption identifiers (`aes/gcm/256`, the currently supported algorithm):
+
+----
+# security properties #
+nifi.registry.security.keystore=/path/to/keystore.jks
+nifi.registry.security.keystoreType=JKS
+nifi.registry.security.keystorePasswd=oBjT92hIGRElIGOh||MZ6uYuWNBrOA6usq/Jt3DaD2e4otNirZDytac/w/KFe0HOkrJR03vcbo
+nifi.registry.security.keystorePasswd.protected=aes/gcm/256
+nifi.registry.security.keyPasswd=ac/BaE35SL/esLiJ||+ULRvRLYdIDA2VqpE0eQXDEMjaLBMG2kbKOdOwBk/hGebDKlVg==
+nifi.registry.security.keyPasswd.protected=aes/gcm/256
+nifi.registry.security.truststore=
+nifi.registry.security.truststoreType=
+nifi.registry.security.truststorePasswd=
+----
+
+When applied to _identity-providers.xml_ or _authorizers.xml_, the property elements are updated with an `encryption` attribute. For example:
+
+----
+<!-- LDAP Provider -->
+<provider>
+   <identifier>ldap-provider</identifier>
+   <class>org.apache.nifi.registry.security.ldap.LdapProvider</class>
+   <property name="Authentication Strategy">START_TLS</property>
+   <property name="Manager DN">someuser</property>
+   <property name="Manager Password" encryption="aes/gcm/128">q4r7WIgN0MaxdAKM||SGgdCTPGSFEcuH4RraMYEdeyVbOx93abdWTVSWvh1w+klA</property>
+   <property name="TLS - Keystore">/path/to/keystore.jks</property>
+   <property name="TLS - Keystore Password" encryption="aes/gcm/128">Uah59TWX+Ru5GY5p||B44RT/LJtC08QWA5ehQf01JxIpf0qSJUzug25UwkF5a50g</property>
+   <property name="TLS - Keystore Type">JKS</property>
+   ...
+</provider>
+----
+
+Additionally, the _bootstrap.conf_ file is updated with the encryption key as follows:
+
+----
+# Master key in hexadecimal format for encrypted sensitive configuration values
+nifi.registry.bootstrap.sensitive.key=0123456789ABCDEFFEDCBA98765432100123456789ABCDEFFEDCBA9876543210
+----
+
+Sensitive configuration values are encrypted by the tool by default, however you can encrypt any additional properties, if desired.
+To encrypt additional properties, specify them as comma-separated values in the `nifi.registry.sensitive.props.additional.keys` property.
+
+
+If the _nifi-registry.properties_ file already has valid protected values and you wish to protect additional values using the
+same master key already present in your _bootstrap.conf_, then run the tool without specifying a new key:
+
+----
+# bootstrap.conf already contains master key property
+# nifi-registy.properties has been updated for nifi.registry.sensitive.props.additional.keys=...
+
+./bin/encrypt-config.sh --nifiRegistry -b bootstrap.conf -r nifi-registry.properties
+----
+
+[sensistive_property_key_migration]
+=== Sensitive Property Key Migration
+
+In order to change the key used to encrypt the sensitive values, provide the new key or password using the `-k` or `-p` flags as usual,
+and provide the existing key or password using `--old-key` or `--old-password` respectively. This will allow the toolkit to decrypt the
+existing values and re-encrypt them, and update _bootstrap.conf_ with the new key. Only one of the key or password needs to be specified
+for each phase (old vs. new), and any combination is sufficient:
+
+* old key -> new key
+* old key -> new password
+* old password -> new key
+* old password -> new password
+
+[[bootstrap_properties]]
+== Bootstrap Properties
+
+The _bootstrap.conf_ file in the `conf` directory allows users to configure settings for how NiFi Registry should be started. This includes parameters, such as the size of the Java Heap, what Java command to run, and Java System Properties.
+
+Here, we will address the different properties that are made available in the file. Any changes to this file will take effect only after NiFi Registry has been stopped and restarted.
+
+|====
+|*Property*|*Description*
+|`java`|Specifies the fully qualified java command to run. By default, it is simply `java` but could be changed to an absolute path or a reference an environment variable, such as `$JAVA_HOME/bin/java`
+|`run.as`|The username to run NiFi Registry as. For instance, if NiFi Registry should be run as the `nifi_registry` user, setting this value to `nifi_registry` will cause the NiFi Registry Process to be run as the `nifi_registry` user. This property is ignored on Windows. For Linux, the specified user may require sudo permissions.
+|`lib.dir`|The _lib_ directory to use for NiFi Registry. By default, this is set to `./lib`
+|`conf.dir`|The `conf` directory to use for NiFi Registry. By default, this is set to `./conf`
+|`graceful.shutdown.seconds`|When NiFi Registry is instructed to shutdown, the Bootstrap will wait this number of seconds for the process to shutdown cleanly. At this amount of time, if the service is still running, the Bootstrap will `kill` the process, or terminate it abruptly. By default, this is set to `20`.
+|`java.arg.N`|Any number of JVM arguments can be passed to the NiFi Registry JVM when the process is started. These arguments are defined by adding properties to _bootstrap.conf_ that begin with `java.arg.`. The rest of the property name is not relevant, other than to different property names, and will be ignored. The default includes properties for minimum and maximum Java Heap size, the garbage collector to use, etc.
+|====
+
+
+[[proxy_configuration]]
+== Proxy Configuration
+
+​When running Apache NiFi Registry behind a proxy there are a couple of key items to be aware of during deployment.
+
+* NiFi Registry is comprised of a number of web applications (web UI, web API, documentation), so the mapping needs to be configured for the *root path*.
+That way all context paths are passed through accordingly.
+
+* If NiFi Registry is running securely, any proxy needs to be authorized to proxy user requests. These can be configured in the NiFi Registry UI through the
+Users administration section, by selecting 'Proxy' for the given user. Once these permissions are in place, proxies can begin proxying user requests.
+The end user identity must be relayed in a HTTP header. For example, if the end user sent a request to the proxy, the proxy must authenticate the user. Following
+this the proxy can send the request to NiFi Registry. In this request an HTTP header should be added as follows.
+
+....
+X-ProxiedEntitiesChain: <end-user-identity>
+....
+
+If the proxy is configured to send to another proxy, the request to NiFi Registry from the second proxy should contain a header as follows.
+
+....
+X-ProxiedEntitiesChain: <end-user-identity><proxy-1-identity>
+....
+
+An example Apache proxy configuration that sets the required properties may look like the following. Complete proxy configuration is outside of the scope of this document.
+Please refer to the documentation of the proxy for guidance with your deployment environment and use case.
+
+....
+...
+<Location "/my-nifi">
+    ...
+	SSLEngine On
+	SSLCertificateFile /path/to/proxy/certificate.crt
+	SSLCertificateKeyFile /path/to/proxy/key.key
+	SSLCACertificateFile /path/to/ca/certificate.crt
+	SSLVerifyClient require
+	RequestHeader add X-ProxyScheme "https"
+	RequestHeader add X-ProxyHost "proxy-host"
+	RequestHeader add X-ProxyPort "443"
+	RequestHeader add X-ProxyContextPath "/my-nifi-registry"
+	RequestHeader add X-ProxiedEntitiesChain "<%{SSL_CLIENT_S_DN}>"
+	ProxyPass https://nifi-registry-host:8443
+	ProxyPassReverse https://nifi-registry-host:8443
+	...
+</Location>
+...
+....
+
+[[kerberos_service]]
+== Kerberos Service
+
+NiFi Registry can be configured to use Kerberos SPNEGO (or "Kerberos Service") for authentication. In this scenario, users will hit the REST endpoint `/access/token/kerberos`
+and the server will respond with a `401` status code and the challenge response header `WWW-Authenticate: Negotiate`. This communicates to the browser to use the GSS-API
+and load the user's Kerberos ticket and provide it as a Base64-encoded header value in the subsequent request. It will be of the form `Authorization: Negotiate YII...`.
+NiFi Registry will attempt to validate this ticket with the KDC. If it is successful, the user's _principal_ will be returned as the identity, and the flow will follow
+login/credential authentication, in that a JWT will be issued in the response to prevent the unnecessary overhead of Kerberos authentication on every subsequent request.
+If the ticket cannot be validated, it will return with the appropriate error response code. The user will then be able to provide their Kerberos credentials to the login
+form if the `KerberosIdentityProvider` has been configured. See <<kerberos_identity_provider, Kerberos Identity Provider>> for more details.
+
+NiFi Registry will only respond to Kerberos SPNEGO negotiation over an HTTPS connection, as unsecured requests are never authenticated.
+
+See <<kerberos_properties>> for complete documentation.
+
+[[kerberos_service_notes]]
+=== Notes
+
+* Kerberos is case-sensitive in many places and the error messages (or lack thereof) may not be sufficiently explanatory.
+  Check the case sensitivity of the service principal in your configuration files. The convention is `HTTP/fully.qualified.domain@REALM`.
+* Browsers have varying levels of restriction when dealing with SPNEGO negotiations.
+  Some will provide the local Kerberos ticket to any domain that requests it, while others whitelist the trusted domains. See link:http://docs.spring.io/autorepo/docs/spring-security-kerberos/1.0.2.BUILD-SNAPSHOT/reference/htmlsingle/#browserspnegoconfig[Spring Security Kerberos - Reference Documentation: Appendix E. Configure browsers for SPNEGO Negotiation^] for common browsers.
+* Some browsers (legacy IE) do not support recent encryption algorithms such as AES, and are restricted to legacy algorithms (DES). This should be noted when generating keytabs.
+* The KDC must be configured and a service principal defined for NiFi and a keytab exported. Comprehensive instructions for Kerberos server configuration and administration are beyond the scope of this document (see link:http://web.mit.edu/kerberos/krb5-current/doc/admin/index.html[MIT Kerberos Admin Guide^]), but an example is below.
+* Kerberos tickets may use AES encryption with keys up to 256-bits in length, and therefore unlimited strength encryption policies may be required for the Jave Runtime Environment (JRE) used for NiFi Registry when Kerberos SPNEGO is configured.
+
+Adding a service principal for a server at `nifi.nifi.apache.org` and exporting the keytab from the KDC:
+
+....
+root@kdc:/etc/krb5kdc# kadmin.local
+Authenticating as principal admin/admin@NIFI.APACHE.ORG with password.
+kadmin.local:  listprincs
+K/M@NIFI.APACHE.ORG
+admin/admin@NIFI.APACHE.ORG
+...
+kadmin.local:  addprinc -randkey HTTP/nifi.nifi.apache.org
+WARNING: no policy specified for HTTP/nifi.nifi.apache.org@NIFI.APACHE.ORG; defaulting to no policy
+Principal "HTTP/nifi.nifi.apache.org@NIFI.APACHE.ORG" created.
+kadmin.local:  ktadd -k /http-nifi.keytab HTTP/nifi.nifi.apache.org
+Entry for principal HTTP/nifi.nifi.apache.org with kvno 2, encryption type des3-cbc-sha1 added to keytab WRFILE:/http-nifi.keytab.
+Entry for principal HTTP/nifi.nifi.apache.org with kvno 2, encryption type des-cbc-crc added to keytab WRFILE:/http-nifi.keytab.
+kadmin.local:  listprincs
+HTTP/nifi.nifi.apache.org@NIFI.APACHE.ORG
+K/M@NIFI.APACHE.ORG
+admin/admin@NIFI.APACHE.ORG
+...
+kadmin.local: q
+root@kdc:~# ll /http*
+-rw------- 1 root root 162 Mar 14 21:43 /http-nifi.keytab
+root@kdc:~#
+....
+
+[[system_properties]]
+== System Properties
+
+The _nifi-registry.properties_ file in the `conf` directory is the main configuration file for controlling how NiFi Registry runs. This section
+provides an overview of the properties in this file and includes some notes on how to configure it in a way that will make upgrading easier.
+*After making changes to this file, restart NiFi Registry in order for the changes to take effect.*
+
+NOTE: Values for periods of time and data sizes must include the unit of measure, for example "10 secs" or "10 MB", not simply "10".
+
+=== Web Properties
+
+These properties pertain to the web-based User Interface.
+
+|====
+|*Property*|*Description*
+|`nifi.registry.web.war.directory`|This is the location of the web war directory. The default value is `./lib`.
+|`nifi.registry.web.http.host`|The HTTP host. It is blank by default.
+|`nifi.registry.web.http.port`|The HTTP port. The default value is `18080`.
+|`nifi.registry.web.https.host`|The HTTPS host. It is blank by default.
+|`nifi.registry.web.https.port`|The HTTPS port. It is blank by default. When configuring NiFi Registry to run securely, this port should be configured.
+|`nifi.registry.web.jetty.working.directory`|The location of the Jetty working directory. The default value is `./work/jetty`.
+|`nifi.registry.web.jetty.threads`|The number of Jetty threads. The default value is `200`.
+|====
+
+=== Security Properties
+
+These properties pertain to various security features in NiFi Registry. Many of these properties are covered in more detail in the
+<<security_configuration>> section.
+
+|====
+|*Property*|*Description*
+|`nifi.registry.security.keystore`|The full path and name of the keystore. It is blank by default.
+|`nifi.registry.security.keystoreType`|The keystore type. It is blank by default.
+|`nifi.registry.security.keystorePasswd`|The keystore password. It is blank by default.
+|`nifi.registry.security.keyPasswd`|The key password. It is blank by default.
+|`nifi.registry.security.truststore`|The full path and name of the truststore. It is blank by default.
+|`nifi.registry.security.truststoreType`|The truststore type. It is blank by default.
+|`nifi.registry.security.truststorePasswd`|The truststore password. It is blank by default.
+|`nifi.registry.security.needClientAuth`| This specifies that connecting clients must authenticate with a client cert. Setting this to `false` will specify that connecting clients may optionally authenticate with a client cert, but may also login with a username and password against a configured identity provider. The default value is `true`.
+|`nifi.registry.security.authorizers.configuration.file`|This is the location of the file that specifies how authorizers are defined. The default value is `./conf/authorizers.xml`.
+|`nifi.registry.security.authorizer`|Specifies which of the configured Authorizers in the _authorizers.xml_ file to use. By default, it is set to `managed-authorizer`.
+|`nifi.registry.security.identity.providers.configuration.file`|This is the location of the file that specifies how username/password authentication is performed. This file is only considered if `nifi.registry.security.identity.provider` is configured with a provider identifier. The default value is `./conf/identity-providers.xml`.
+|`nifi.registry.security.identity.provider`|This indicates what type of identity provider to use. The default value is blank, can be set to the identifier from a provider in the file specified in `nifi.registry.security.identity.providers.configuration.file`. Setting this property will trigger NiFi Registry to support username/password authentication.
+|====
+
+=== Providers Properties
+
+These properties pertain to flow persistence providers. NiFi Registry uses a pluggable flow persistence provider to store the
+content of the flows saved to the registry. For further details on persistence providers, refer <<Persistence Providers>>.
+
+|====
+|*Property*|*Description*
+|`nifi.registry.providers.configuration.file`|This is the location of the file where flow persistence providers are configured. The default value is `./conf/providers.xml`.
+|====
+
+=== Database Properties
+
+These properties define the settings for the Registry database, which keeps track of metadata about buckets and all items stored in buckets.
+
+The 0.1.0 release leveraged an embedded H2 database that was configured via the following properties:
+
+|====
+|*Property*|*Description*
+|`nifi.registry.db.directory`|The location of the Registry database directory. The default value is `./database`.
+|`nifi.registry.db.url.append`|This property specifies additional arguments to add to the connection string for the Registry database. The default value should be used and should not be changed. It is: `;LOCK_TIMEOUT=25000;WRITE_DELAY=0;AUTO_SERVER=FALSE`.
+|====
+
+The 0.2.0 release introduced a more flexible approach which allows leveraging an external database. This new approach
+is configured via the following properties:
+
+|====
+|*Property*|*Description*
+|`nifi.registry.db.url`| The full JDBC connection string. The default value will specify a new H2 database in the same location as the previous one. For example, `jdbc:h2:./database/nifi-registry-primary;`.
+|`nifi.registry.db.driver.class`| The class name of the JDBC driver. The default value is `org.h2.Driver`.
+|`nifi.registry.db.driver.directory`| An optional directory containing one or more JARs to add to the classpath. If not specified, it is assumed that the driver JAR is already on the classpath by copying it to the `lib` directory. The H2 driver is bundled with Registry so it is not necessary to do anything for the default case.
+|`nifi.registry.db.driver.username`| The username for the database. The default value is `nifireg`.
+|`nifi.registry.db.driver.password`| The password for the database. The default value is `nifireg`.
+|`nifi.registry.db.driver.maxConnections`| The max number of connections for the connection pool. The default value is `5`.
+|`nifi.registry.db.sql.debug`| Whether or not enable debug logging for SQL statements. The default value is `false`.
+|====
+
+NOTE: When upgrading from 0.1.0 to a future version, if `nifi.registry.db.directory` remains populated, the application will
+attempt to migrate the data from the original database to the new database specified with the new properties. This will only
+happen the first time the application starts with the new database properties.
+
+=== Extension Directories
+
+Each property beginning with `nifi.registry.extension.dir.` will be treated as location for an extension, and a class loader will be created for each location, with the system class loader as the parent.
+
+|====
+|*Property*|*Description*
+|`nifi.registry.extension.dir.1`| The full path on the filesystem to the location of the JARs for the given extension
+|====
+
+NOTE: Multiple extension directories can be specified by using the `nifi.registry.extension.dir.` prefix with unique suffixes and separate paths as values.
+For example, to provide an additional extension directory, a user could also specify additional properties with keys of: `nifi.registry.extension.dir.2=/path/to/extension2`,
+providing 2 total locations, including `nifi.registry.extension.dir.1`.
+
+
+[[kerberos_properties]]
+=== Kerberos Properties
+
+|====
+|*Property*|*Description*
+|`nifi.registry.kerberos.krb5.file`|The location of the krb5 file, if used. It is blank by default. At this time, only a single krb5 file is allowed to
+    be specified per NiFi instance, so this property is configured here to support SPNEGO and service principals rather than in individual Processors.
+    If necessary the krb5 file can support multiple realms.
+    Example: `/etc/krb5.conf`
+|`nifi.registry.kerberos.spnego.principal`|The name of the NiFi Registry Kerberos SPNEGO principal, if used. It is blank by default. Note that this property is used to authenticate NiFi Registry users.
+   Example: `HTTP/nifi.registry.example.com` or `HTTP/nifi.registry.example.com@EXAMPLE.COM`
+|`nifi.registry.kerberos.spnego.keytab.location`|The file path of the NiFi Registry Kerberos SPNEGO keytab, if used. It is blank by default. Note that this property is used to authenticate NiFi Registry users.
+  Example: `/etc/http-nifi-registry.keytab`
+|`nifi.registry.kerberos.spengo.authentication.expiration`|The expiration duration of a successful Kerberos user authentication, if used. The default value is `12 hours`.
+|====
+
+== Persistence Providers
+
+NiFi Registry uses a pluggable flow persistence provider to store the content of the flows saved to the registry. NiFi Registry provides `<<FileSystemFlowPersistenceProvider>>` and `<<GitFlowPersistenceProvider>>`.
+
+Each persistence provider has its own configuration parameters, those can be configured in a XML file specified in _<<Providers Properties,nifi-registry.properties>>_.
+
+The XML configuration file looks like below. It has a `flowPersistenceProvider` element in which qualified class name of a persistence provider implementation and its configuration properties are defined. See following sections for available configurations for each provider.
+
+.Example providers.xml
+[source,xml]
+....
+<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
+<providers>
+
+    <flowPersistenceProvider>
+        <class>persistence-provider-qualified-class-name</class>
+        <property name="property-1">property-value-1</property>
+        <property name="property-2">property-value-2</property>
+        <property name="property-n">property-value-n</property>
+    </flowPersistenceProvider>
+
+</providers>
+....
+
+
+=== FileSystemFlowPersistenceProvider
+
+FileSystemFlowPersistenceProvider simply stores serialized Flow contents into `{bucket-id}/{flow-id}/{version}` directories.
+
+Example of persisted files:
+....
+Flow Storage Directory/
+├── {bucket-id}/
+│   └── {flow-id}/
+│       ├── {version}/{version}.snapshot
+└── d1beba88-32e9-45d1-bfe9-057cc41f7ce8/
+    └── 219cf539-427f-43be-9294-0644fb07ca63/
+        ├── 1/1.snapshot
+        └── 2/2.snapshot
+....
+
+Qualified class name: `org.apache.nifi.registry.provider.flow.FileSystemFlowPersistenceProvider`
+
+|====
+|*Property*|*Description*
+|`Flow Storage Directory`|REQUIRED: File system path for a directory where flow contents files are persisted to. If the directory does not exist when NiFi Registry starts, it will be created. If the directory exists, it must be readable and writable from NiFi Registry.
+|====
+
+
+=== GitFlowPersistenceProvider
+
+`GitFlowPersistenceProvider` stores flow contents under a Git directory.
+
+In contrast to `FileSystemFlowPersistenceProvider`, this provider uses human friendly Bucket and Flow names so that those files can be accessed by external tools. However, it is NOT supported to modify stored files outside of NiFi Registry. Persisted files are only read when NiFi Registry starts up.
+
+Buckets are represented as directories and Flow contents are stored as files in a Bucket directory they belong to. Flow snapshot histories are managed as Git commits, meaning only the latest version of Buckets and Flows exist in the Git directory. Old versions are retrieved from Git commit histories.
+
+.Example persisted files
+....
+Flow Storage Directory/
+├── .git/
+├── Bucket_A/
+│   ├── bucket.yml
+│   ├── Flow_1.snapshot
+│   └── Flow_2.snapshot
+└── Bucket_B/
+    ├── bucket.yml
+    └── Flow_4.snapshot
+....
+
+Each Bucket directory contains a YAML file named `bucket.yml`. The file manages links from NiFi Registry Bucket and Flow IDs to actual directory and file names. When NiFi Registry starts, this provider reads through Git commit histories and lookup these `bucket.yml` files to restore Buckets and Flows for each snapshot version.
+
+.Example bucket.yml
+[source,yml]
+....
+layoutVer: 1
+bucketId: d1beba88-32e9-45d1-bfe9-057cc41f7ce8
+flows:
+  219cf539-427f-43be-9294-0644fb07ca63: {ver: 7, file: Flow_1.snapshot}
+  22cccb6c-3011-4493-a996-611f8f112969: {ver: 3, file: Flow_2.snapshot}
+....
+
+Qualified class name: `org.apache.nifi.registry.provider.flow.git.GitFlowPersistenceProvider`
+
+|====
+|*Property*|*Description*
+|`Flow Storage Directory`|REQUIRED: File system path for a directory where flow contents files are persisted to. The directory must exist when NiFi registry starts. Also must be initialized as a Git directory. See <<Initialize Git directory>> for detail.
+|`Remote To Push`|When a new flow snapshot is created, this persistence provider updated files in the specified Git directory, then create a commit to the local repository. If `Remote To Push` is defined, it also pushes to the specified remote repository. E.g. `origin`. To define more detailed remote spec such as branch names, use `Refspec`. See
+link:https://git-scm.com/book/en/v2/Git-Internals-The-Refspec[https://git-scm.com/book/en/v2/Git-Internals-The-Refspec^]
+|`Remote Access User`|This user name is used to make push requests to the remote repository when `Remote To Push` is enabled, and the remote repository is accessed by HTTP protocol. If SSH is used, user authentication is done with SSH keys.
+|`Remote Access Password`|Used with `Remote Access User`.
+|====
+
+==== Initialize Git directory
+
+In order to use `GitFlowPersistenceRepository`, you need to prepare a Git directory on the local file system. You can do so by initializing a directory with `git init` command, or clone an existing Git project from a remote Git repository by `git clone` command.
+
+- `git init` command
+link:https://git-scm.com/docs/git-init[https://git-scm.com/docs/git-init^]
+- `git clone` command
+link:https://git-scm.com/docs/git-clone[https://git-scm.com/docs/git-clone^]
+
+==== Git user configuration
+
+This persistence provider uses preconfigured Git user name and user email address when it creates Git commits. NiFi Registry user name is added to commit messages.
+
+.Example commit
+....
+commit 774d4bd125f2b1200f0a5ee1f1e9fedc6a415e83
+Author: git-user <gi...@example.com>
+Date:   Tue May 8 14:30:31 2018 +0900
+
+    Commit message.
+
+    By NiFi Registry user: nifi-registry-user-1
+....
+
+
+You can configure Git user name and email address by `git config` command.
+
+- `git config` command
+link:https://git-scm.com/docs/git-config[https://git-scm.com/docs/git-config^]
+
+
+==== Git user authentication
+
+By default, this persistence repository only create commits to local repository. No user authentication is needed to do so. However, if 'Commit To Push' is enabled, user authentication to the remote Git repository is required.
+
+If the remote repository is accessed by HTTP, then username and password for authentication can be configured in the providers XML configuration file.
+
+When SSH is used, SSH keys are used to identify a Git user. In order to pick the right key to a remote server, the SSH configuration file `${USER_HOME}/.ssh/config` is used. The SSH configuration file can contain multiple `Host` entries to specify a key file to login to a remote Git server. The `Host` must match with the target remote Git server hostname.
+
+.example SSH config file
+....
+Host git.example.com
+  HostName git.example.com
+  IdentityFile ~/.ssh/id_rsa
+
+Host github.com
+  HostName github.com
+  IdentityFile ~/.ssh/key-for-github
+
+Host bitbucket.org
+  HostName bitbucket.org
+  IdentityFile ~/.ssh/key-for-bitbucket
+....
+
+=== Switching from other Persistence Provider
+
+In order to switch the Persistence Provider to use, it is necessary to reset NiFi Registry.
+For example, to switch from `FileSystemFlowPersistenceProvider` to `GitFlowPersistenceProvider`, follow these steps:
+
+. Stop version control on all ProcessGroups in NiFi
+. Stop NiFi Registry
+. Move the H2 DB (specified as `nifi.registry.db.directory` in _nifi-registry.properties_) and `Flow Storage Directory` for `FileSystemFlowPersistenceProvider` directories somewhere for back up
+. Configure `GitFlowPersistenceProvider` provider in _providers.xml_
+. Start NiFi Registry
+. Recreate any buckets
+. Start version control on all ProcessGroups again
+
+=== Data model version of serialized Flow snapshots
+
+Serialized Flow snapshots saved by these persistence providers have versions, so that the data format and schema can evolve over time. Data model version update is done automatically by NiFi Registry when it reads and stores each Flow content.
+
+Here is the data model version histories:
+
+|====
+|*Data model version*|*Since NiFi Registry*|*Description*
+|2|0.2|JSON formatted text file. The root object contains header and Flow content object.
+|1|0.1|Binary format having header bytes at the beginning followed by Flow content represented as XML.
+|====
+
+== Event Hooks
+Event hooks are an integration point that allows for custom code to to be triggered when NiFi Registry application events occur.
+
+[options="header,footer"]
+|==================================================================================================================================================
+| Event Name | Description
+|`CREATE_BUCKET` | A new registry bucket is created.
+|`CREATE_FLOW` | A new flow is created in a specified bucket. Only triggered on first time creation of a flow with a given name.
+|`CREATE_FLOW_VERSION` | A new version for a flow has been saved in the registry.
+|`UPDATE_BUCKET` | A bucket has been updated.
+|`UPDATE_FLOW` | A flow that exist in a bucket has been updated.
+|`DELETE_BUCKET` | An existing bucket in the registry is deleted.
+|`DELETE_FLOW` | An existing flow in the registry is deleted.
+|`REGISTRY_START` | Invoked once the NiFi Registry application has been successfully started. This is only invoked after a complete and successful start.
+|==================================================================================================================================================
+
+=== Shared Event Hook Properties
+There are certain properties that are shared amongst all of the NiFi Registry provided Event Hook implementations. Those properties and
+their purpose are listed below.
+
+[options="header,footer"]
+|==================================================================================================================================================
+| Property Name | Description
+|`Whitelisted Event Type` | EventTypes the hook provider configured with this property should respond to. If this property is left blank or not provided all events will fire for the configured hook provider. Multiple 'Whitelisted Event Type' can be specified and often are.
+EX: <property name="Whitelisted Event Type 1">CREATE_FLOW</property> and <property name="Whitelisted Event Type 2">UPDATE_FLOW</property> would invoke the configured hook provider for the CREATE_FLOW and UPDATE_FLOW EventTypes.
+|==================================================================================================================================================
+
+=== ScriptEventHookProvider
+Hook provider for invoking a shell script that has been written by a user and placed on a file system that is accessible
+by the NiFi Registry instance that the provider is configured for.
+
+....
+<eventHookProvider>
+    <class>
+      org.apache.nifi.registry.provider.hook.ScriptEventHookProvider
+    </class>
+    <property name="Script Path"></property>
+    <property name="Working Directory"></property>
+    <!-- optional -->
+        <property name="Whitelisted Event Type 1">CREATE_FLOW</property>
+        <property name="Whitelisted Event Type 2">UPDATE_FLOW</property>
+</eventHookProvider>
+....
+
+[options="header,footer"]
+|==================================================================================================================================================
+| Property Name | Description
+|`Script Path` | Full path to a script that will executed for each event. The arguments to the script will be the event fields in the order they are specified for the given event type.
+|`Working Directory` | Working directory from where the commands will be executed.
+|==================================================================================================================================================
+
+=== LoggingEventHookProvider
+The LoggingEventHookProvider logs a string representation of each event using an SLF4J logger. The logger can be configured
+via NiFi Registry’s logback.xml, which by default contains an appender that writes to a log file named nifi-registry-event.log in the logs directory.
+
+....
+<eventHookProvider>
+    <class>
+      org.apache.nifi.registry.provider.hook.LoggingEventHookProvider
+    </class>
+</eventHookProvider>
+....
\ No newline at end of file


[11/51] [partial] nifi-registry git commit: NIFIREG-201 Refactoring project structure to better isolate extensions

Posted by kd...@apache.org.
http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/6f26290d/nifi-registry-core/nifi-registry-web-api/src/test/java/org/apache/nifi/registry/web/api/SecureLdapIT.java
----------------------------------------------------------------------
diff --git a/nifi-registry-core/nifi-registry-web-api/src/test/java/org/apache/nifi/registry/web/api/SecureLdapIT.java b/nifi-registry-core/nifi-registry-web-api/src/test/java/org/apache/nifi/registry/web/api/SecureLdapIT.java
new file mode 100644
index 0000000..543ea87
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-web-api/src/test/java/org/apache/nifi/registry/web/api/SecureLdapIT.java
@@ -0,0 +1,651 @@
+/*
+ * 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.nifi.registry.SecureLdapTestApiApplication;
+import org.apache.nifi.registry.authorization.AccessPolicy;
+import org.apache.nifi.registry.authorization.AccessPolicySummary;
+import org.apache.nifi.registry.authorization.CurrentUser;
+import org.apache.nifi.registry.authorization.Permissions;
+import org.apache.nifi.registry.authorization.Tenant;
+import org.apache.nifi.registry.bucket.Bucket;
+import org.apache.nifi.registry.extension.ExtensionManager;
+import org.apache.nifi.registry.properties.AESSensitivePropertyProvider;
+import org.apache.nifi.registry.properties.NiFiRegistryProperties;
+import org.apache.nifi.registry.properties.SensitivePropertyProvider;
+import org.apache.nifi.registry.security.authorization.Authorizer;
+import org.apache.nifi.registry.security.authorization.AuthorizerFactory;
+import org.apache.nifi.registry.security.crypto.BootstrapFileCryptoKeyProvider;
+import org.apache.nifi.registry.security.crypto.CryptoKeyProvider;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.skyscreamer.jsonassert.JSONAssert;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.boot.test.context.SpringBootTest;
+import org.springframework.boot.test.context.TestConfiguration;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.DependsOn;
+import org.springframework.context.annotation.Import;
+import org.springframework.context.annotation.Primary;
+import org.springframework.context.annotation.Profile;
+import org.springframework.test.context.jdbc.Sql;
+import org.springframework.test.context.junit4.SpringRunner;
+
+import javax.ws.rs.client.Entity;
+import javax.ws.rs.core.Form;
+import javax.ws.rs.core.MediaType;
+import javax.ws.rs.core.Response;
+import java.nio.charset.Charset;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Base64;
+import java.util.List;
+import java.util.Set;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertTrue;
+
+/**
+ * Deploy the Web API Application using an embedded Jetty Server for local integration testing, with the follow characteristics:
+ *
+ * - A NiFiRegistryProperties has to be explicitly provided to the ApplicationContext using a profile unique to this test suite.
+ * - A NiFiRegistryClientConfig has been configured to create a client capable of completing one-way TLS
+ * - The database is embed H2 using volatile (in-memory) persistence
+ * - Custom SQL is clearing the DB before each test method by default, unless method overrides this behavior
+ */
+@RunWith(SpringRunner.class)
+@SpringBootTest(
+        classes = SecureLdapTestApiApplication.class,
+        webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT,
+        properties = "spring.profiles.include=ITSecureLdap")
+@Import(SecureITClientConfiguration.class)
+@Sql(executionPhase = Sql.ExecutionPhase.BEFORE_TEST_METHOD, scripts = "classpath:db/clearDB.sql")
+public class SecureLdapIT extends IntegrationTestBase {
+
+    private static final String tokenLoginPath = "access/token/login";
+    private static final String tokenIdentityProviderPath = "access/token/identity-provider";
+
+    @TestConfiguration
+    @Profile("ITSecureLdap")
+    public static class LdapTestConfiguration {
+
+        static AuthorizerFactory authorizerFactory;
+
+        @Primary
+        @Bean
+        @DependsOn({"directoryServer"}) // Can't load LdapUserGroupProvider until the embedded LDAP server, which creates the "directoryServer" bean, is running
+        public static Authorizer getAuthorizer(@Autowired NiFiRegistryProperties properties, ExtensionManager extensionManager) throws Exception {
+            if (authorizerFactory == null) {
+                authorizerFactory = new AuthorizerFactory(properties, extensionManager, sensitivePropertyProvider());
+            }
+            return authorizerFactory.getAuthorizer();
+        }
+
+        @Primary
+        @Bean
+        public static SensitivePropertyProvider sensitivePropertyProvider() throws Exception {
+            return new AESSensitivePropertyProvider(getNiFiRegistryMasterKeyProvider().getKey());
+        }
+
+        private static CryptoKeyProvider getNiFiRegistryMasterKeyProvider() {
+            return new BootstrapFileCryptoKeyProvider("src/test/resources/conf/secure-ldap/bootstrap.conf");
+        }
+
+    }
+
+    private String adminAuthToken;
+    private List<AccessPolicy> beforeTestAccessPoliciesSnapshot;
+
+    @Before
+    public void setup() {
+        final String basicAuthCredentials = encodeCredentialsForBasicAuth("nifiadmin", "password");
+        final String token = client
+                .target(createURL(tokenIdentityProviderPath))
+                .request()
+                .header("Authorization", "Basic " + basicAuthCredentials)
+                .post(null, String.class);
+        adminAuthToken = token;
+
+        beforeTestAccessPoliciesSnapshot = createAccessPoliciesSnapshot();
+    }
+
+    @After
+    public void cleanup() {
+        restoreAccessPoliciesSnapshot(beforeTestAccessPoliciesSnapshot);
+    }
+
+    @Test
+    public void testTokenGenerationAndAccessStatus() throws Exception {
+
+        // Note: this test intentionally does not use the token generated
+        // for nifiadmin by the @Before method
+
+        // Given: the client and server have been configured correctly for LDAP authentication
+        String expectedJwtPayloadJson = "{" +
+                "\"sub\":\"nobel\"," +
+                "\"preferred_username\":\"nobel\"," +
+                "\"iss\":\"LdapIdentityProvider\"" +
+                "}";
+        String expectedAccessStatusJson = "{" +
+                "\"identity\":\"nobel\"," +
+                "\"anonymous\":false" +
+                "}";
+
+        // When: the /access/token/login endpoint is queried
+        final String basicAuthCredentials = encodeCredentialsForBasicAuth("nobel", "password");
+        final Response tokenResponse = client
+                .target(createURL(tokenIdentityProviderPath))
+                .request()
+                .header("Authorization", "Basic " + basicAuthCredentials)
+                .post(null, Response.class);
+
+        // Then: the server returns 200 OK with an access token
+        assertEquals(201, tokenResponse.getStatus());
+        String token = tokenResponse.readEntity(String.class);
+        assertTrue(StringUtils.isNotEmpty(token));
+        String[] jwtParts = token.split("\\.");
+        assertEquals(3, jwtParts.length);
+        String jwtPayload = new String(Base64.getDecoder().decode(jwtParts[1]), "UTF-8");
+        JSONAssert.assertEquals(expectedJwtPayloadJson, jwtPayload, false);
+
+        // When: the token is returned in the Authorization header
+        final Response accessResponse = client
+                .target(createURL("access"))
+                .request()
+                .header("Authorization", "Bearer " + token)
+                .get(Response.class);
+
+        // Then: the server acknowledges the client has access
+        assertEquals(200, accessResponse.getStatus());
+        String accessStatus = accessResponse.readEntity(String.class);
+        JSONAssert.assertEquals(expectedAccessStatusJson, accessStatus, false);
+
+    }
+
+    @Test
+    public void testTokenGenerationWithIdentityProvider() throws Exception {
+
+        // Given: the client and server have been configured correctly for LDAP authentication
+        String expectedJwtPayloadJson = "{" +
+                "\"sub\":\"nobel\"," +
+                "\"preferred_username\":\"nobel\"," +
+                "\"iss\":\"LdapIdentityProvider\"," +
+                "\"aud\":\"LdapIdentityProvider\"" +
+                "}";
+        String expectedAccessStatusJson = "{" +
+                "\"identity\":\"nobel\"," +
+                "\"anonymous\":false" +
+                "}";
+
+        // When: the /access/token/identity-provider endpoint is queried
+        final String basicAuthCredentials = encodeCredentialsForBasicAuth("nobel", "password");
+        final Response tokenResponse = client
+                .target(createURL(tokenIdentityProviderPath))
+                .request()
+                .header("Authorization", "Basic " + basicAuthCredentials)
+                .post(null, Response.class);
+
+        // Then: the server returns 200 OK with an access token
+        assertEquals(201, tokenResponse.getStatus());
+        String token = tokenResponse.readEntity(String.class);
+        assertTrue(StringUtils.isNotEmpty(token));
+        String[] jwtParts = token.split("\\.");
+        assertEquals(3, jwtParts.length);
+        String jwtPayload = new String(Base64.getDecoder().decode(jwtParts[1]), "UTF-8");
+        JSONAssert.assertEquals(expectedJwtPayloadJson, jwtPayload, false);
+
+        // When: the token is returned in the Authorization header
+        final Response accessResponse = client
+                .target(createURL("access"))
+                .request()
+                .header("Authorization", "Bearer " + token)
+                .get(Response.class);
+
+        // Then: the server acknowledges the client has access
+        assertEquals(200, accessResponse.getStatus());
+        String accessStatus = accessResponse.readEntity(String.class);
+        JSONAssert.assertEquals(expectedAccessStatusJson, accessStatus, false);
+
+    }
+
+    @Test
+    public void testGetCurrentUserFailsForAnonymous() throws Exception {
+
+        // Given: the client is connected to an unsecured NiFi Registry
+
+        // When: the /access endpoint is queried with no credentials
+        final Response response = client
+                .target(createURL("/access"))
+                .request()
+                .get(Response.class);
+
+        // Then: the server returns a 200 OK with the expected current user
+        assertEquals(401, response.getStatus());
+
+    }
+
+    @Test
+    public void testGetCurrentUser() throws Exception {
+
+        // Given: the client is connected to an unsecured NiFi Registry
+        String expectedJson = "{" +
+                "\"identity\":\"nifiadmin\"," +
+                "\"anonymous\":false," +
+                "\"resourcePermissions\":{" +
+                "\"anyTopLevelResource\":{\"canRead\":true,\"canWrite\":true,\"canDelete\":true}," +
+                "\"buckets\":{\"canRead\":true,\"canWrite\":true,\"canDelete\":true}," +
+                "\"tenants\":{\"canRead\":true,\"canWrite\":true,\"canDelete\":true}," +
+                "\"policies\":{\"canRead\":true,\"canWrite\":true,\"canDelete\":true}," +
+                "\"proxy\":{\"canRead\":false,\"canWrite\":true,\"canDelete\":false}}" +
+                "}";
+
+        // When: the /access endpoint is queried using a JWT for the nifiadmin LDAP user
+        final Response response = client
+                .target(createURL("/access"))
+                .request()
+                .header("Authorization", "Bearer " + adminAuthToken)
+                .get(Response.class);
+
+        // Then: the server returns a 200 OK with the expected current user
+        assertEquals(200, response.getStatus());
+        String actualJson = response.readEntity(String.class);
+        JSONAssert.assertEquals(expectedJson, actualJson, false);
+
+    }
+
+    @Test
+    public void testUsers() throws Exception {
+
+        // Given: the client and server have been configured correctly for LDAP authentication
+        String expectedJson = "[" +
+                "{\"identity\":\"nifiadmin\",\"userGroups\":[],\"configurable\":false," +
+                    "\"resourcePermissions\":{" +
+                    "\"anyTopLevelResource\":{\"canRead\":true,\"canWrite\":true,\"canDelete\":true}," +
+                    "\"buckets\":{\"canRead\":true,\"canWrite\":true,\"canDelete\":true}," +
+                    "\"tenants\":{\"canRead\":true,\"canWrite\":true,\"canDelete\":true}," +
+                    "\"policies\":{\"canRead\":true,\"canWrite\":true,\"canDelete\":true}," +
+                    "\"proxy\":{\"canRead\":false,\"canWrite\":true,\"canDelete\":false}}}," +
+                "{\"identity\":\"euler\",\"userGroups\":[{\"identity\":\"mathematicians\"}],\"accessPolicies\":[],\"configurable\":false}," +
+                "{\"identity\":\"euclid\",\"userGroups\":[{\"identity\":\"mathematicians\"}],\"accessPolicies\":[],\"configurable\":false}," +
+                "{\"identity\":\"boyle\",\"userGroups\":[{\"identity\":\"chemists\"}],\"accessPolicies\":[],\"configurable\":false}," +
+                "{\"identity\":\"newton\",\"userGroups\":[{\"identity\":\"scientists\"}],\"accessPolicies\":[],\"configurable\":false}," +
+                "{\"identity\":\"riemann\",\"userGroups\":[{\"identity\":\"mathematicians\"}],\"accessPolicies\":[],\"configurable\":false}," +
+                "{\"identity\":\"gauss\",\"userGroups\":[{\"identity\":\"mathematicians\"}],\"accessPolicies\":[],\"configurable\":false}," +
+                "{\"identity\":\"galileo\",\"userGroups\":[{\"identity\":\"scientists\"},{\"identity\":\"italians\"}],\"accessPolicies\":[],\"configurable\":false}," +
+                "{\"identity\":\"nobel\",\"userGroups\":[{\"identity\":\"chemists\"}],\"accessPolicies\":[],\"configurable\":false}," +
+                "{\"identity\":\"pasteur\",\"userGroups\":[{\"identity\":\"chemists\"}],\"accessPolicies\":[],\"configurable\":false}," +
+                "{\"identity\":\"tesla\",\"userGroups\":[{\"identity\":\"scientists\"}],\"accessPolicies\":[],\"configurable\":false}," +
+                "{\"identity\":\"nogroup\",\"userGroups\":[],\"accessPolicies\":[],\"configurable\":false}," +
+                "{\"identity\":\"einstein\",\"userGroups\":[{\"identity\":\"scientists\"}],\"accessPolicies\":[],\"configurable\":false}," +
+                "{\"identity\":\"curie\",\"userGroups\":[{\"identity\":\"chemists\"}],\"accessPolicies\":[],\"configurable\":false}]";
+
+        // When: the /tenants/users endpoint is queried
+        final String usersJson = client
+                .target(createURL("tenants/users"))
+                .request()
+                .header("Authorization", "Bearer " + adminAuthToken)
+                .get(String.class);
+
+        // Then: the server returns a list of all users (see test-ldap-data.ldif)
+        JSONAssert.assertEquals(expectedJson, usersJson, false);
+    }
+
+    @Test
+    public void testUserGroups() throws Exception {
+
+        // Given: the client and server have been configured correctly for LDAP authentication
+        String expectedJson = "[" +
+                "{" +
+                    "\"identity\":\"chemists\"," +
+                    "\"users\":[{\"identity\":\"pasteur\"},{\"identity\":\"boyle\"},{\"identity\":\"curie\"},{\"identity\":\"nobel\"}]," +
+                    "\"accessPolicies\":[]," +
+                    "\"configurable\":false" +
+                "}," +
+                "{" +
+                    "\"identity\":\"mathematicians\"," +
+                    "\"users\":[{\"identity\":\"gauss\"},{\"identity\":\"euclid\"},{\"identity\":\"riemann\"},{\"identity\":\"euler\"}]," +
+                    "\"accessPolicies\":[]," +
+                    "\"configurable\":false" +
+                "}," +
+                "{" +
+                    "\"identity\":\"scientists\"," +
+                    "\"users\":[{\"identity\":\"einstein\"},{\"identity\":\"tesla\"},{\"identity\":\"newton\"},{\"identity\":\"galileo\"}]," +
+                    "\"accessPolicies\":[]," +
+                    "\"configurable\":false" +
+                "}," +
+                "{" +
+                    "\"identity\":\"italians\"," +
+                    "\"users\":[{\"identity\":\"galileo\"}]," +
+                    "\"accessPolicies\":[]," +
+                    "\"configurable\":false" +
+                "}]";
+
+        // When: the /tenants/users endpoint is queried
+        final String groupsJson = client
+                .target(createURL("tenants/user-groups"))
+                .request()
+                .header("Authorization", "Bearer " + adminAuthToken)
+                .get(String.class);
+
+        // Then: the server returns a list of all users (see test-ldap-data.ldif)
+        JSONAssert.assertEquals(expectedJson, groupsJson, false);
+    }
+
+    @Test
+    public void testCreateTenantFails() throws Exception {
+
+        // Given: the server has been configured with the LdapUserGroupProvider, which is non-configurable,
+        //   and: the client wants to create a tenant
+        Tenant tenant = new Tenant();
+        tenant.setIdentity("new_tenant");
+
+        // When: the POST /tenants/users endpoint is accessed
+        final Response createUserResponse = client
+                .target(createURL("tenants/users"))
+                .request()
+                .header("Authorization", "Bearer " + adminAuthToken)
+                .post(Entity.entity(tenant, MediaType.APPLICATION_JSON_TYPE), Response.class);
+
+        // Then: an error is returned
+        assertEquals(409, createUserResponse.getStatus());
+
+        // When: the POST /tenants/users endpoint is accessed
+        final Response createUserGroupResponse = client
+                .target(createURL("tenants/user-groups"))
+                .request()
+                .header("Authorization", "Bearer " + adminAuthToken)
+                .post(Entity.entity(tenant, MediaType.APPLICATION_JSON_TYPE), Response.class);
+
+        // Then: an error is returned because the UserGroupProvider is non-configurable
+        assertEquals(409, createUserGroupResponse.getStatus());
+    }
+
+    @Test
+    public void testAccessPolicyCreation() throws Exception {
+
+        // Given: the server has been configured with an initial admin "nifiadmin" and a user with no accessPolicies "nobel"
+        String nobelId = getTenantIdentifierByIdentity("nobel");
+        String chemistsId = getTenantIdentifierByIdentity("chemists"); // a group containing user "nobel"
+
+        final String basicAuthCredentials = encodeCredentialsForBasicAuth("nobel", "password");
+        final String nobelAuthToken = client
+                .target(createURL(tokenIdentityProviderPath))
+                .request()
+                .header("Authorization", "Basic " + basicAuthCredentials)
+                .post(null, String.class);
+
+        // When: user nobel re-checks top-level permissions
+        final CurrentUser currentUser = client
+                .target(createURL("/access"))
+                .request()
+                .header("Authorization", "Bearer " + nobelAuthToken)
+                .get(CurrentUser.class);
+
+        // Then: 200 OK is returned indicating user has access to no top-level resources
+        assertEquals(new Permissions(), currentUser.getResourcePermissions().getBuckets());
+        assertEquals(new Permissions(), currentUser.getResourcePermissions().getTenants());
+        assertEquals(new Permissions(), currentUser.getResourcePermissions().getPolicies());
+        assertEquals(new Permissions(), currentUser.getResourcePermissions().getProxy());
+
+        // When: nifiadmin creates a bucket
+        final Bucket bucket = new Bucket();
+        bucket.setName("Integration Test Bucket");
+        bucket.setDescription("A bucket created by an integration test.");
+        Response adminCreatesBucketResponse = client
+                .target(createURL("buckets"))
+                .request()
+                .header("Authorization", "Bearer " + adminAuthToken)
+                .post(Entity.entity(bucket, MediaType.APPLICATION_JSON), Response.class);
+
+        // Then: the server returns a 200 OK
+        assertEquals(200, adminCreatesBucketResponse.getStatus());
+        Bucket createdBucket = adminCreatesBucketResponse.readEntity(Bucket.class);
+
+
+        // When: user nobel initial queries /buckets
+        final Bucket[] buckets1 = client
+                .target(createURL("buckets"))
+                .request()
+                .header("Authorization", "Bearer " + nobelAuthToken)
+                .get(Bucket[].class);
+
+        // Then: an empty list is returned (nobel has no read access yet)
+        assertNotNull(buckets1);
+        assertEquals(0, buckets1.length);
+
+
+        // When: nifiadmin grants read access on createdBucket to 'chemists' a group containing nobel
+        AccessPolicy readPolicy = new AccessPolicy();
+        readPolicy.setResource("/buckets/" + createdBucket.getIdentifier());
+        readPolicy.setAction("read");
+        readPolicy.addUserGroups(Arrays.asList(new Tenant(chemistsId, "chemists")));
+        Response adminGrantsReadAccessResponse = client
+                .target(createURL("policies"))
+                .request()
+                .header("Authorization", "Bearer " + adminAuthToken)
+                .post(Entity.entity(readPolicy, MediaType.APPLICATION_JSON), Response.class);
+
+        // Then: the server returns a 201 Created
+        assertEquals(201, adminGrantsReadAccessResponse.getStatus());
+
+
+        // When: nifiadmin tries to list all buckets
+        final Bucket[] adminBuckets = client
+                .target(createURL("buckets"))
+                .request()
+                .header("Authorization", "Bearer " + adminAuthToken)
+                .get(Bucket[].class);
+
+        // Then: the full list is returned (verifies that per-bucket access policies are additive to base /buckets policy)
+        assertNotNull(adminBuckets);
+        assertEquals(1, adminBuckets.length);
+        assertEquals(createdBucket.getIdentifier(), adminBuckets[0].getIdentifier());
+        assertEquals(new Permissions().withCanRead(true).withCanWrite(true).withCanDelete(true), adminBuckets[0].getPermissions());
+
+
+        // When: user nobel re-queries /buckets
+        final Bucket[] buckets2 = client
+                .target(createURL("buckets"))
+                .request()
+                .header("Authorization", "Bearer " + nobelAuthToken)
+                .get(Bucket[].class);
+
+        // Then: the created bucket is now present
+        assertNotNull(buckets2);
+        assertEquals(1, buckets2.length);
+        assertEquals(createdBucket.getIdentifier(), buckets2[0].getIdentifier());
+        assertEquals(new Permissions().withCanRead(true), buckets2[0].getPermissions());
+
+
+        // When: nifiadmin grants write access on createdBucket to user 'nobel'
+        AccessPolicy writePolicy = new AccessPolicy();
+        writePolicy.setResource("/buckets/" + createdBucket.getIdentifier());
+        writePolicy.setAction("write");
+        writePolicy.addUsers(Arrays.asList(new Tenant(nobelId, "nobel")));
+        Response adminGrantsWriteAccessResponse = client
+                .target(createURL("policies"))
+                .request()
+                .header("Authorization", "Bearer " + adminAuthToken)
+                .post(Entity.entity(writePolicy, MediaType.APPLICATION_JSON), Response.class);
+
+        // Then: the server returns a 201 Created
+        assertEquals(201, adminGrantsWriteAccessResponse.getStatus());
+
+
+        // When: user nobel re-queries /buckets
+        final Bucket[] buckets3 = client
+                .target(createURL("buckets"))
+                .request()
+                .header("Authorization", "Bearer " + nobelAuthToken)
+                .get(Bucket[].class);
+
+        // Then: the authorizedActions are updated
+        assertNotNull(buckets3);
+        assertEquals(1, buckets3.length);
+        assertEquals(createdBucket.getIdentifier(), buckets3[0].getIdentifier());
+        assertEquals(new Permissions().withCanRead(true).withCanWrite(true), buckets3[0].getPermissions());
+
+    }
+
+    /** A helper method to lookup identifiers for tenant identities using the REST API
+     *
+     * @param tenantIdentity - the identity to lookup
+     * @return A string containing the identifier of the tenant, or null if the tenant identity is not found.
+     */
+    private String getTenantIdentifierByIdentity(String tenantIdentity) {
+
+        final Tenant[] users = client
+                .target(createURL("tenants/users"))
+                .request()
+                .header("Authorization", "Bearer " + adminAuthToken)
+                .get(Tenant[].class);
+
+        final Tenant[] groups = client
+                .target(createURL("tenants/user-groups"))
+                .request()
+                .header("Authorization", "Bearer " + adminAuthToken)
+                .get(Tenant[].class);
+
+        final Tenant matchedTenant = Stream.concat(Arrays.stream(users), Arrays.stream(groups))
+                .filter(tenant -> tenant.getIdentity().equalsIgnoreCase(tenantIdentity))
+                .findFirst()
+                .orElse(null);
+
+        return matchedTenant != null ? matchedTenant.getIdentifier() : null;
+    }
+
+    /** A helper method to lookup access policies
+     *
+     * @return A string containing the identifier of the policy, or null if the policy identity is not found.
+     */
+    private AccessPolicy getPolicyByResourceAction(String action, String resource) {
+
+        final AccessPolicySummary[] policies = client
+                .target(createURL("policies"))
+                .request()
+                .header("Authorization", "Bearer " + adminAuthToken)
+                .get(AccessPolicySummary[].class);
+
+        final AccessPolicySummary matchedPolicy = Arrays.stream(policies)
+                .filter(p -> p.getAction().equalsIgnoreCase(action) && p.getResource().equalsIgnoreCase(resource))
+                .findFirst()
+                .orElse(null);
+
+        if (matchedPolicy == null) {
+            return null;
+        }
+
+        String policyId = matchedPolicy.getIdentifier();
+
+        final AccessPolicy policy = client
+                .target(createURL("policies/" + policyId))
+                .request()
+                .header("Authorization", "Bearer " + adminAuthToken)
+                .get(AccessPolicy.class);
+
+        return policy;
+    }
+
+    private List<AccessPolicy> createAccessPoliciesSnapshot() {
+
+        final AccessPolicySummary[] policySummaries = client
+                .target(createURL("policies"))
+                .request()
+                .header("Authorization", "Bearer " + adminAuthToken)
+                .get(AccessPolicySummary[].class);
+
+        final List<AccessPolicy> policies = new ArrayList<>(policySummaries.length);
+        for (AccessPolicySummary s : policySummaries) {
+            AccessPolicy policy = client
+                    .target(createURL("policies/" + s.getIdentifier()))
+                    .request()
+                    .header("Authorization", "Bearer " + adminAuthToken)
+                    .get(AccessPolicy.class);
+            policies.add(policy);
+        }
+
+        return policies;
+    }
+
+    private void restoreAccessPoliciesSnapshot(List<AccessPolicy> accessPoliciesSnapshot) {
+
+        List<AccessPolicy> currentAccessPolicies = createAccessPoliciesSnapshot();
+
+        Set<String> policiesToRestore = accessPoliciesSnapshot.stream()
+                .map(AccessPolicy::getIdentifier)
+                .collect(Collectors.toSet());
+
+        Set<String> policiesToDelete = currentAccessPolicies.stream()
+                .filter(p -> !policiesToRestore.contains(p.getIdentifier()))
+                .map(AccessPolicy::getIdentifier)
+                .collect(Collectors.toSet());
+
+        for (AccessPolicy originalPolicy : accessPoliciesSnapshot) {
+
+            Response getCurrentPolicy = client
+                    .target(createURL("policies/" + originalPolicy.getIdentifier()))
+                    .request()
+                    .header("Authorization", "Bearer " + adminAuthToken)
+                    .get(Response.class);
+
+            if (getCurrentPolicy.getStatus() == 200) {
+                // update policy to match original
+                client.target(createURL("policies/" + originalPolicy.getIdentifier()))
+                        .request()
+                        .header("Authorization", "Bearer " + adminAuthToken)
+                        .put(Entity.entity(originalPolicy, MediaType.APPLICATION_JSON));
+            } else {
+                // post the original policy
+                client.target(createURL("policies"))
+                        .request()
+                        .header("Authorization", "Bearer " + adminAuthToken)
+                        .post(Entity.entity(originalPolicy, MediaType.APPLICATION_JSON));
+            }
+
+        }
+
+        for (String id : policiesToDelete) {
+            try {
+                client.target(createURL("policies/" + id))
+                        .request()
+                        .header("Authorization", "Bearer " + adminAuthToken)
+                        .delete();
+            } catch (Exception e) {
+                // do nothing
+            }
+        }
+
+    }
+
+    private static Form encodeCredentialsForURLFormParams(String username, String password) {
+        return new Form()
+                .param("username", username)
+                .param("password", password);
+    }
+
+    private static String encodeCredentialsForBasicAuth(String username, String password) {
+        final String credentials = username + ":" + password;
+        final String base64credentials =  new String(Base64.getEncoder().encode(credentials.getBytes(Charset.forName("UTF-8"))));
+        return base64credentials;
+    }
+}

http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/6f26290d/nifi-registry-core/nifi-registry-web-api/src/test/java/org/apache/nifi/registry/web/api/SecureNiFiRegistryClientIT.java
----------------------------------------------------------------------
diff --git a/nifi-registry-core/nifi-registry-web-api/src/test/java/org/apache/nifi/registry/web/api/SecureNiFiRegistryClientIT.java b/nifi-registry-core/nifi-registry-web-api/src/test/java/org/apache/nifi/registry/web/api/SecureNiFiRegistryClientIT.java
new file mode 100644
index 0000000..cb14b90
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-web-api/src/test/java/org/apache/nifi/registry/web/api/SecureNiFiRegistryClientIT.java
@@ -0,0 +1,217 @@
+/*
+ * 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.NiFiRegistryTestApiApplication;
+import org.apache.nifi.registry.authorization.Permissions;
+import org.apache.nifi.registry.bucket.Bucket;
+import org.apache.nifi.registry.client.BucketClient;
+import org.apache.nifi.registry.client.FlowClient;
+import org.apache.nifi.registry.client.FlowSnapshotClient;
+import org.apache.nifi.registry.client.NiFiRegistryClient;
+import org.apache.nifi.registry.client.NiFiRegistryClientConfig;
+import org.apache.nifi.registry.client.NiFiRegistryException;
+import org.apache.nifi.registry.client.UserClient;
+import org.apache.nifi.registry.client.impl.JerseyNiFiRegistryClient;
+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.flow.VersionedProcessGroup;
+import org.apache.nifi.registry.authorization.CurrentUser;
+import org.junit.After;
+import org.junit.Assert;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.boot.test.context.SpringBootTest;
+import org.springframework.context.annotation.Import;
+import org.springframework.test.context.jdbc.Sql;
+import org.springframework.test.context.junit4.SpringRunner;
+
+import javax.ws.rs.ForbiddenException;
+import java.io.IOException;
+import java.util.List;
+
+@RunWith(SpringRunner.class)
+@SpringBootTest(
+        classes = NiFiRegistryTestApiApplication.class,
+        webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT,
+        properties = "spring.profiles.include=ITSecureFile")
+@Import(SecureITClientConfiguration.class)
+@Sql(executionPhase = Sql.ExecutionPhase.BEFORE_TEST_METHOD, scripts = {"classpath:db/clearDB.sql", "classpath:db/FlowsIT.sql"})
+public class SecureNiFiRegistryClientIT extends IntegrationTestBase {
+
+    static final Logger LOGGER = LoggerFactory.getLogger(SecureNiFiRegistryClientIT.class);
+
+    private NiFiRegistryClient client;
+
+    @Before
+    public void setup() {
+        final String baseUrl = createBaseURL();
+        LOGGER.info("Using base url = " + baseUrl);
+
+        final NiFiRegistryClientConfig clientConfig = createClientConfig(baseUrl);
+        Assert.assertNotNull(clientConfig);
+
+        final NiFiRegistryClient client = new JerseyNiFiRegistryClient.Builder()
+                .config(clientConfig)
+                .build();
+        Assert.assertNotNull(client);
+        this.client = client;
+    }
+
+    @After
+    public void teardown() {
+        try {
+            client.close();
+        } catch (Exception e) {
+
+        }
+    }
+
+    @Test
+    public void testGetAccessStatus() throws IOException, NiFiRegistryException {
+        final UserClient userClient = client.getUserClient();
+        final CurrentUser currentUser = userClient.getAccessStatus();
+        Assert.assertEquals("CN=user1, OU=nifi", currentUser.getIdentity());
+        Assert.assertFalse(currentUser.isAnonymous());
+        Assert.assertNotNull(currentUser.getResourcePermissions());
+        Permissions fullAccess = new Permissions().withCanRead(true).withCanWrite(true).withCanDelete(true);
+        Assert.assertEquals(fullAccess, currentUser.getResourcePermissions().getAnyTopLevelResource());
+        Assert.assertEquals(fullAccess, currentUser.getResourcePermissions().getBuckets());
+        Assert.assertEquals(fullAccess, currentUser.getResourcePermissions().getTenants());
+        Assert.assertEquals(fullAccess, currentUser.getResourcePermissions().getPolicies());
+        Assert.assertEquals(new Permissions().withCanWrite(true), currentUser.getResourcePermissions().getProxy());
+    }
+
+    @Test
+    public void testCrudOperations() throws IOException, NiFiRegistryException {
+        final Bucket bucket = new Bucket();
+        bucket.setName("Bucket 1 " + System.currentTimeMillis());
+        bucket.setDescription("This is bucket 1");
+
+        final BucketClient bucketClient = client.getBucketClient();
+        final Bucket createdBucket = bucketClient.create(bucket);
+        Assert.assertNotNull(createdBucket);
+        Assert.assertNotNull(createdBucket.getIdentifier());
+
+        final List<Bucket> buckets = bucketClient.getAll();
+        Assert.assertEquals(4, buckets.size());
+
+        final VersionedFlow flow = new VersionedFlow();
+        flow.setBucketIdentifier(createdBucket.getIdentifier());
+        flow.setName("Flow 1 - " + System.currentTimeMillis());
+
+        final FlowClient flowClient = client.getFlowClient();
+        final VersionedFlow createdFlow = flowClient.create(flow);
+        Assert.assertNotNull(createdFlow);
+        Assert.assertNotNull(createdFlow.getIdentifier());
+
+        final VersionedFlowSnapshotMetadata snapshotMetadata = new VersionedFlowSnapshotMetadata();
+        snapshotMetadata.setBucketIdentifier(createdFlow.getBucketIdentifier());
+        snapshotMetadata.setFlowIdentifier(createdFlow.getIdentifier());
+        snapshotMetadata.setVersion(1);
+        snapshotMetadata.setComments("This is snapshot #1");
+
+        final VersionedProcessGroup rootProcessGroup = new VersionedProcessGroup();
+        rootProcessGroup.setIdentifier("root-pg");
+        rootProcessGroup.setName("Root Process Group");
+
+        final VersionedFlowSnapshot snapshot = new VersionedFlowSnapshot();
+        snapshot.setSnapshotMetadata(snapshotMetadata);
+        snapshot.setFlowContents(rootProcessGroup);
+
+        final FlowSnapshotClient snapshotClient = client.getFlowSnapshotClient();
+        final VersionedFlowSnapshot createdSnapshot = snapshotClient.create(snapshot);
+        Assert.assertNotNull(createdSnapshot);
+        Assert.assertEquals("CN=user1, OU=nifi", createdSnapshot.getSnapshotMetadata().getAuthor());
+    }
+
+    @Test
+    public void testGetAccessStatusWithProxiedEntity() throws IOException, NiFiRegistryException {
+        final String proxiedEntity = "user2";
+        final UserClient userClient = client.getUserClient(proxiedEntity);
+        final CurrentUser status = userClient.getAccessStatus();
+        Assert.assertEquals("user2", status.getIdentity());
+        Assert.assertFalse(status.isAnonymous());
+    }
+
+    @Test
+    public void testCreatedBucketWithProxiedEntity() throws IOException, NiFiRegistryException {
+        final String proxiedEntity = "user2";
+        final BucketClient bucketClient = client.getBucketClient(proxiedEntity);
+
+        final Bucket bucket = new Bucket();
+        bucket.setName("Bucket 1");
+        bucket.setDescription("This is bucket 1");
+
+        try {
+            bucketClient.create(bucket);
+            Assert.fail("Shouldn't have been able to create a bucket");
+        } catch (Exception e) {
+
+        }
+    }
+
+    @Test
+    public void testDirectFlowAccess() throws IOException {
+        // this user shouldn't have access to anything
+        final String proxiedEntity = "CN=no-access, OU=nifi";
+
+        final FlowClient proxiedFlowClient = client.getFlowClient(proxiedEntity);
+        final FlowSnapshotClient proxiedFlowSnapshotClient = client.getFlowSnapshotClient(proxiedEntity);
+
+        try {
+            proxiedFlowClient.get("1");
+            Assert.fail("Shouldn't have been able to retrieve flow");
+        } catch (NiFiRegistryException e) {
+            Assert.assertTrue(e.getCause()  instanceof ForbiddenException);
+        }
+
+        try {
+            proxiedFlowSnapshotClient.getLatest("1");
+            Assert.fail("Shouldn't have been able to retrieve flow");
+        } catch (NiFiRegistryException e) {
+            Assert.assertTrue(e.getCause()  instanceof ForbiddenException);
+        }
+
+        try {
+            proxiedFlowSnapshotClient.getLatestMetadata("1");
+            Assert.fail("Shouldn't have been able to retrieve flow");
+        } catch (NiFiRegistryException e) {
+            Assert.assertTrue(e.getCause()  instanceof ForbiddenException);
+        }
+
+        try {
+            proxiedFlowSnapshotClient.get("1", 1);
+            Assert.fail("Shouldn't have been able to retrieve flow");
+        } catch (NiFiRegistryException e) {
+            Assert.assertTrue(e.getCause()  instanceof ForbiddenException);
+        }
+
+        try {
+            proxiedFlowSnapshotClient.getSnapshotMetadata("1");
+            Assert.fail("Shouldn't have been able to retrieve flow");
+        } catch (NiFiRegistryException e) {
+            Assert.assertTrue(e.getCause()  instanceof ForbiddenException);
+        }
+
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/6f26290d/nifi-registry-core/nifi-registry-web-api/src/test/java/org/apache/nifi/registry/web/api/UnsecuredITBase.java
----------------------------------------------------------------------
diff --git a/nifi-registry-core/nifi-registry-web-api/src/test/java/org/apache/nifi/registry/web/api/UnsecuredITBase.java b/nifi-registry-core/nifi-registry-web-api/src/test/java/org/apache/nifi/registry/web/api/UnsecuredITBase.java
new file mode 100644
index 0000000..a0c981b
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-web-api/src/test/java/org/apache/nifi/registry/web/api/UnsecuredITBase.java
@@ -0,0 +1,42 @@
+/*
+ * 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.NiFiRegistryTestApiApplication;
+import org.junit.runner.RunWith;
+import org.springframework.boot.test.context.SpringBootTest;
+import org.springframework.test.context.jdbc.Sql;
+import org.springframework.test.context.junit4.SpringRunner;
+
+/**
+ * Deploy the Web API Application using an embedded Jetty Server for local integration testing, with the follow characteristics:
+ *
+ * - A NiFiRegistryProperties has to be explicitly provided to the ApplicationContext using a profile unique to this test suite.
+ * - The database is embed H2 using volatile (in-memory) persistence
+ * - Custom SQL is clearing the DB before each test method by default, unless method overrides this behavior
+ */
+@RunWith(SpringRunner.class)
+@SpringBootTest(
+        classes = NiFiRegistryTestApiApplication.class,
+        webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT,
+        properties = "spring.profiles.include=ITUnsecured")
+@Sql(executionPhase = Sql.ExecutionPhase.BEFORE_TEST_METHOD, scripts = "classpath:db/clearDB.sql")
+public class UnsecuredITBase extends IntegrationTestBase {
+
+    // Tests cases defined in subclasses
+
+}

http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/6f26290d/nifi-registry-core/nifi-registry-web-api/src/test/java/org/apache/nifi/registry/web/api/UnsecuredNiFiRegistryClientIT.java
----------------------------------------------------------------------
diff --git a/nifi-registry-core/nifi-registry-web-api/src/test/java/org/apache/nifi/registry/web/api/UnsecuredNiFiRegistryClientIT.java b/nifi-registry-core/nifi-registry-web-api/src/test/java/org/apache/nifi/registry/web/api/UnsecuredNiFiRegistryClientIT.java
new file mode 100644
index 0000000..2410234
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-web-api/src/test/java/org/apache/nifi/registry/web/api/UnsecuredNiFiRegistryClientIT.java
@@ -0,0 +1,403 @@
+/*
+ * 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.CurrentUser;
+import org.apache.nifi.registry.authorization.Permissions;
+import org.apache.nifi.registry.bucket.Bucket;
+import org.apache.nifi.registry.bucket.BucketItem;
+import org.apache.nifi.registry.client.BucketClient;
+import org.apache.nifi.registry.client.FlowClient;
+import org.apache.nifi.registry.client.FlowSnapshotClient;
+import org.apache.nifi.registry.client.ItemsClient;
+import org.apache.nifi.registry.client.NiFiRegistryClient;
+import org.apache.nifi.registry.client.NiFiRegistryClientConfig;
+import org.apache.nifi.registry.client.NiFiRegistryException;
+import org.apache.nifi.registry.client.UserClient;
+import org.apache.nifi.registry.client.impl.JerseyNiFiRegistryClient;
+import org.apache.nifi.registry.diff.VersionedFlowDifference;
+import org.apache.nifi.registry.field.Fields;
+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.flow.VersionedProcessGroup;
+import org.apache.nifi.registry.flow.VersionedProcessor;
+import org.apache.nifi.registry.flow.VersionedPropertyDescriptor;
+import org.junit.After;
+import org.junit.Assert;
+import org.junit.Before;
+import org.junit.Test;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * Test all basic functionality of JerseyNiFiRegistryClient.
+ */
+public class UnsecuredNiFiRegistryClientIT extends UnsecuredITBase {
+
+    static final Logger LOGGER = LoggerFactory.getLogger(UnsecuredNiFiRegistryClientIT.class);
+
+    private NiFiRegistryClient client;
+
+    @Before
+    public void setup() {
+        final String baseUrl = createBaseURL();
+        LOGGER.info("Using base url = " + baseUrl);
+
+        final NiFiRegistryClientConfig clientConfig = new NiFiRegistryClientConfig.Builder()
+                .baseUrl(baseUrl)
+                .build();
+
+        Assert.assertNotNull(clientConfig);
+
+        final NiFiRegistryClient client = new JerseyNiFiRegistryClient.Builder()
+                .config(clientConfig)
+                .build();
+
+        Assert.assertNotNull(client);
+        this.client = client;
+    }
+
+    @After
+    public void teardown() {
+        try {
+            client.close();
+        } catch (Exception e) {
+
+        }
+    }
+
+    @Test
+    public void testGetAccessStatus() throws IOException, NiFiRegistryException {
+        final UserClient userClient = client.getUserClient();
+        final CurrentUser currentUser = userClient.getAccessStatus();
+        Assert.assertEquals("anonymous", currentUser.getIdentity());
+        Assert.assertTrue(currentUser.isAnonymous());
+        Assert.assertNotNull(currentUser.getResourcePermissions());
+        Permissions fullAccess = new Permissions().withCanRead(true).withCanWrite(true).withCanDelete(true);
+        Assert.assertEquals(fullAccess, currentUser.getResourcePermissions().getAnyTopLevelResource());
+        Assert.assertEquals(fullAccess, currentUser.getResourcePermissions().getBuckets());
+        Assert.assertEquals(fullAccess, currentUser.getResourcePermissions().getTenants());
+        Assert.assertEquals(fullAccess, currentUser.getResourcePermissions().getPolicies());
+        Assert.assertEquals(fullAccess, currentUser.getResourcePermissions().getProxy());
+    }
+
+    @Test
+    public void testNiFiRegistryClient() throws IOException, NiFiRegistryException {
+        // ---------------------- TEST BUCKETS --------------------------//
+
+        final BucketClient bucketClient = client.getBucketClient();
+
+        // create buckets
+        final int numBuckets = 10;
+        final List<Bucket> createdBuckets = new ArrayList<>();
+
+        for (int i=0; i < numBuckets; i++) {
+            final Bucket createdBucket = createBucket(bucketClient, i);
+            LOGGER.info("Created bucket # " + i + " with id " + createdBucket.getIdentifier());
+            createdBuckets.add(createdBucket);
+        }
+
+        // get each bucket
+        for (final Bucket bucket : createdBuckets) {
+            final Bucket retrievedBucket = bucketClient.get(bucket.getIdentifier());
+            Assert.assertNotNull(retrievedBucket);
+            LOGGER.info("Retrieved bucket " + retrievedBucket.getIdentifier());
+        }
+
+        //final Bucket nonExistentBucket = bucketClient.get("does-not-exist");
+        //Assert.assertNull(nonExistentBucket);
+
+        // get bucket fields
+        final Fields bucketFields = bucketClient.getFields();
+        Assert.assertNotNull(bucketFields);
+        LOGGER.info("Retrieved bucket fields, size = " + bucketFields.getFields().size());
+        Assert.assertTrue(bucketFields.getFields().size() > 0);
+
+        // get all buckets
+        final List<Bucket> allBuckets = bucketClient.getAll();
+        LOGGER.info("Retrieved buckets, size = " + allBuckets.size());
+        Assert.assertEquals(numBuckets, allBuckets.size());
+        allBuckets.stream().forEach(b -> System.out.println("Retrieve bucket " + b.getIdentifier()));
+
+        // update each bucket
+        for (final Bucket bucket : createdBuckets) {
+            final Bucket bucketUpdate = new Bucket();
+            bucketUpdate.setIdentifier(bucket.getIdentifier());
+            bucketUpdate.setDescription(bucket.getDescription() + " UPDATE");
+
+            final Bucket updatedBucket = bucketClient.update(bucketUpdate);
+            Assert.assertNotNull(updatedBucket);
+            LOGGER.info("Updated bucket " + updatedBucket.getIdentifier());
+        }
+
+        // ---------------------- TEST FLOWS --------------------------//
+
+        final FlowClient flowClient = client.getFlowClient();
+
+        // create flows
+        final Bucket flowsBucket = createdBuckets.get(0);
+
+        final VersionedFlow flow1 = createFlow(flowClient, flowsBucket, 1);
+        LOGGER.info("Created flow # 1 with id " + flow1.getIdentifier());
+
+        final VersionedFlow flow2 = createFlow(flowClient, flowsBucket, 2);
+        LOGGER.info("Created flow # 2 with id " + flow2.getIdentifier());
+
+        // get flow
+        final VersionedFlow retrievedFlow1 = flowClient.get(flowsBucket.getIdentifier(), flow1.getIdentifier());
+        Assert.assertNotNull(retrievedFlow1);
+        LOGGER.info("Retrieved flow # 1 with id " + retrievedFlow1.getIdentifier());
+
+        final VersionedFlow retrievedFlow2 = flowClient.get(flowsBucket.getIdentifier(), flow2.getIdentifier());
+        Assert.assertNotNull(retrievedFlow2);
+        LOGGER.info("Retrieved flow # 2 with id " + retrievedFlow2.getIdentifier());
+
+        // get flow without bucket
+        final VersionedFlow retrievedFlow1WithoutBucket = flowClient.get(flow1.getIdentifier());
+        Assert.assertNotNull(retrievedFlow1WithoutBucket);
+        Assert.assertEquals(flow1.getIdentifier(), retrievedFlow1WithoutBucket.getIdentifier());
+        LOGGER.info("Retrieved flow # 1 without bucket id, with id " + retrievedFlow1WithoutBucket.getIdentifier());
+
+        // update flows
+        final VersionedFlow flow1Update = new VersionedFlow();
+        flow1Update.setIdentifier(flow1.getIdentifier());
+        flow1Update.setName(flow1.getName() + " UPDATED");
+
+        final VersionedFlow updatedFlow1 = flowClient.update(flowsBucket.getIdentifier(), flow1Update);
+        Assert.assertNotNull(updatedFlow1);
+        LOGGER.info("Updated flow # 1 with id " + updatedFlow1.getIdentifier());
+
+        // get flow fields
+        final Fields flowFields = flowClient.getFields();
+        Assert.assertNotNull(flowFields);
+        LOGGER.info("Retrieved flow fields, size = " + flowFields.getFields().size());
+        Assert.assertTrue(flowFields.getFields().size() > 0);
+
+        // get flows in bucket
+        final List<VersionedFlow> flowsInBucket = flowClient.getByBucket(flowsBucket.getIdentifier());
+        Assert.assertNotNull(flowsInBucket);
+        Assert.assertEquals(2, flowsInBucket.size());
+        flowsInBucket.stream().forEach(f -> LOGGER.info("Flow in bucket, flow id " + f.getIdentifier()));
+
+        // ---------------------- TEST SNAPSHOTS --------------------------//
+
+        final FlowSnapshotClient snapshotClient = client.getFlowSnapshotClient();
+
+        // create snapshots
+        final VersionedFlow snapshotFlow = flow1;
+
+        final VersionedFlowSnapshot snapshot1 = createSnapshot(snapshotClient, snapshotFlow, 1);
+        LOGGER.info("Created snapshot # 1 with version " + snapshot1.getSnapshotMetadata().getVersion());
+
+        final VersionedFlowSnapshot snapshot2 = createSnapshot(snapshotClient, snapshotFlow, 2);
+        LOGGER.info("Created snapshot # 2 with version " + snapshot2.getSnapshotMetadata().getVersion());
+
+        // get snapshot
+        final VersionedFlowSnapshot retrievedSnapshot1 = snapshotClient.get(snapshotFlow.getBucketIdentifier(), snapshotFlow.getIdentifier(), 1);
+        Assert.assertNotNull(retrievedSnapshot1);
+        Assert.assertFalse(retrievedSnapshot1.isLatest());
+        LOGGER.info("Retrieved snapshot # 1 with version " + retrievedSnapshot1.getSnapshotMetadata().getVersion());
+
+        final VersionedFlowSnapshot retrievedSnapshot2 = snapshotClient.get(snapshotFlow.getBucketIdentifier(), snapshotFlow.getIdentifier(), 2);
+        Assert.assertNotNull(retrievedSnapshot2);
+        Assert.assertTrue(retrievedSnapshot2.isLatest());
+        LOGGER.info("Retrieved snapshot # 2 with version " + retrievedSnapshot2.getSnapshotMetadata().getVersion());
+
+        // get snapshot without bucket
+        final VersionedFlowSnapshot retrievedSnapshot1WithoutBucket = snapshotClient.get(snapshotFlow.getIdentifier(), 1);
+        Assert.assertNotNull(retrievedSnapshot1WithoutBucket);
+        Assert.assertFalse(retrievedSnapshot1WithoutBucket.isLatest());
+        Assert.assertEquals(snapshotFlow.getIdentifier(), retrievedSnapshot1WithoutBucket.getSnapshotMetadata().getFlowIdentifier());
+        Assert.assertEquals(1, retrievedSnapshot1WithoutBucket.getSnapshotMetadata().getVersion());
+        LOGGER.info("Retrieved snapshot # 1 without using bucket id, with version " + retrievedSnapshot1WithoutBucket.getSnapshotMetadata().getVersion());
+
+        // get latest
+        final VersionedFlowSnapshot retrievedSnapshotLatest = snapshotClient.getLatest(snapshotFlow.getBucketIdentifier(), snapshotFlow.getIdentifier());
+        Assert.assertNotNull(retrievedSnapshotLatest);
+        Assert.assertEquals(snapshot2.getSnapshotMetadata().getVersion(), retrievedSnapshotLatest.getSnapshotMetadata().getVersion());
+        Assert.assertTrue(retrievedSnapshotLatest.isLatest());
+        LOGGER.info("Retrieved latest snapshot with version " + retrievedSnapshotLatest.getSnapshotMetadata().getVersion());
+
+        // get latest without bucket
+        final VersionedFlowSnapshot retrievedSnapshotLatestWithoutBucket = snapshotClient.getLatest(snapshotFlow.getIdentifier());
+        Assert.assertNotNull(retrievedSnapshotLatestWithoutBucket);
+        Assert.assertEquals(snapshot2.getSnapshotMetadata().getVersion(), retrievedSnapshotLatestWithoutBucket.getSnapshotMetadata().getVersion());
+        Assert.assertTrue(retrievedSnapshotLatestWithoutBucket.isLatest());
+        LOGGER.info("Retrieved latest snapshot without bucket, with version " + retrievedSnapshotLatestWithoutBucket.getSnapshotMetadata().getVersion());
+
+        // get metadata
+        final List<VersionedFlowSnapshotMetadata> retrievedMetadata = snapshotClient.getSnapshotMetadata(snapshotFlow.getBucketIdentifier(), snapshotFlow.getIdentifier());
+        Assert.assertNotNull(retrievedMetadata);
+        Assert.assertEquals(2, retrievedMetadata.size());
+        Assert.assertEquals(2, retrievedMetadata.get(0).getVersion());
+        Assert.assertEquals(1, retrievedMetadata.get(1).getVersion());
+        retrievedMetadata.stream().forEach(s -> LOGGER.info("Retrieved snapshot metadata " + s.getVersion()));
+
+        // get metadata without bucket
+        final List<VersionedFlowSnapshotMetadata> retrievedMetadataWithoutBucket = snapshotClient.getSnapshotMetadata(snapshotFlow.getIdentifier());
+        Assert.assertNotNull(retrievedMetadataWithoutBucket);
+        Assert.assertEquals(2, retrievedMetadataWithoutBucket.size());
+        Assert.assertEquals(2, retrievedMetadataWithoutBucket.get(0).getVersion());
+        Assert.assertEquals(1, retrievedMetadataWithoutBucket.get(1).getVersion());
+        retrievedMetadataWithoutBucket.stream().forEach(s -> LOGGER.info("Retrieved snapshot metadata " + s.getVersion()));
+
+        // get latest metadata
+        final VersionedFlowSnapshotMetadata latestMetadata = snapshotClient.getLatestMetadata(snapshotFlow.getBucketIdentifier(), snapshotFlow.getIdentifier());
+        Assert.assertNotNull(latestMetadata);
+        Assert.assertEquals(2, latestMetadata.getVersion());
+
+        // get latest metadata that doesn't exist
+        try {
+            snapshotClient.getLatestMetadata(snapshotFlow.getBucketIdentifier(), "DOES-NOT-EXIST");
+            Assert.fail("Should have thrown exception");
+        } catch (NiFiRegistryException nfe) {
+            Assert.assertEquals("Error retrieving latest snapshot metadata: The specified flow ID does not exist in this bucket.", nfe.getMessage());
+        }
+
+        // get latest metadata without bucket
+        final VersionedFlowSnapshotMetadata latestMetadataWithoutBucket = snapshotClient.getLatestMetadata(snapshotFlow.getIdentifier());
+        Assert.assertNotNull(latestMetadataWithoutBucket);
+        Assert.assertEquals(snapshotFlow.getIdentifier(), latestMetadataWithoutBucket.getFlowIdentifier());
+        Assert.assertEquals(2, latestMetadataWithoutBucket.getVersion());
+
+        // ---------------------- TEST ITEMS --------------------------//
+
+        final ItemsClient itemsClient = client.getItemsClient();
+
+        // get fields
+        final Fields itemFields = itemsClient.getFields();
+        Assert.assertNotNull(itemFields.getFields());
+        Assert.assertTrue(itemFields.getFields().size() > 0);
+
+        // get all items
+        final List<BucketItem> allItems = itemsClient.getAll();
+        Assert.assertEquals(2, allItems.size());
+        allItems.stream().forEach(i -> Assert.assertNotNull(i.getBucketName()));
+        allItems.stream().forEach(i -> LOGGER.info("All items, item " + i.getIdentifier()));
+
+        // get items for bucket
+        final List<BucketItem> bucketItems = itemsClient.getByBucket(flowsBucket.getIdentifier());
+        Assert.assertEquals(2, bucketItems.size());
+        allItems.stream().forEach(i -> Assert.assertNotNull(i.getBucketName()));
+        bucketItems.stream().forEach(i -> LOGGER.info("Items in bucket, item " + i.getIdentifier()));
+
+        // ----------------------- TEST DIFF ---------------------------//
+
+        final VersionedFlowSnapshot snapshot3 = buildSnapshot(snapshotFlow, 3);
+        final VersionedProcessGroup newlyAddedPG = new VersionedProcessGroup();
+        newlyAddedPG.setIdentifier("new-pg");
+        newlyAddedPG.setName("NEW Process Group");
+        snapshot3.getFlowContents().getProcessGroups().add(newlyAddedPG);
+        snapshotClient.create(snapshot3);
+
+        VersionedFlowDifference diff = flowClient.diff(snapshotFlow.getBucketIdentifier(), snapshotFlow.getIdentifier(), 3, 2);
+        Assert.assertNotNull(diff);
+        Assert.assertEquals(1, diff.getComponentDifferenceGroups().size());
+
+        // ---------------------- DELETE DATA --------------------------//
+
+        final VersionedFlow deletedFlow1 = flowClient.delete(flowsBucket.getIdentifier(), flow1.getIdentifier());
+        Assert.assertNotNull(deletedFlow1);
+        LOGGER.info("Deleted flow " + deletedFlow1.getIdentifier());
+
+        final VersionedFlow deletedFlow2 = flowClient.delete(flowsBucket.getIdentifier(), flow2.getIdentifier());
+        Assert.assertNotNull(deletedFlow2);
+        LOGGER.info("Deleted flow " + deletedFlow2.getIdentifier());
+
+        // delete each bucket
+        for (final Bucket bucket : createdBuckets) {
+            final Bucket deletedBucket = bucketClient.delete(bucket.getIdentifier());
+            Assert.assertNotNull(deletedBucket);
+            LOGGER.info("Deleted bucket " + deletedBucket.getIdentifier());
+        }
+        Assert.assertEquals(0, bucketClient.getAll().size());
+
+        LOGGER.info("!!! SUCCESS !!!");
+
+    }
+
+    private static Bucket createBucket(BucketClient bucketClient, int num) throws IOException, NiFiRegistryException {
+        final Bucket bucket = new Bucket();
+        bucket.setName("Bucket #" + num);
+        bucket.setDescription("This is bucket #" + num);
+        return bucketClient.create(bucket);
+    }
+
+    private static VersionedFlow createFlow(FlowClient client, Bucket bucket, int num) throws IOException, NiFiRegistryException {
+        final VersionedFlow versionedFlow = new VersionedFlow();
+        versionedFlow.setName(bucket.getName() + " Flow #" + num);
+        versionedFlow.setDescription("This is " + bucket.getName() + " flow #" + num);
+        versionedFlow.setBucketIdentifier(bucket.getIdentifier());
+        return client.create(versionedFlow);
+    }
+
+    private static VersionedFlowSnapshot buildSnapshot(VersionedFlow flow, int num) {
+        final VersionedFlowSnapshotMetadata snapshotMetadata = new VersionedFlowSnapshotMetadata();
+        snapshotMetadata.setBucketIdentifier(flow.getBucketIdentifier());
+        snapshotMetadata.setFlowIdentifier(flow.getIdentifier());
+        snapshotMetadata.setVersion(num);
+        snapshotMetadata.setComments("This is snapshot #" + num);
+
+        final VersionedProcessGroup rootProcessGroup = new VersionedProcessGroup();
+        rootProcessGroup.setIdentifier("root-pg");
+        rootProcessGroup.setName("Root Process Group");
+
+        final VersionedProcessGroup subProcessGroup = new VersionedProcessGroup();
+        subProcessGroup.setIdentifier("sub-pg");
+        subProcessGroup.setName("Sub Process Group");
+        rootProcessGroup.getProcessGroups().add(subProcessGroup);
+
+        final Map<String,String> processorProperties = new HashMap<>();
+        processorProperties.put("Prop 1", "Val 1");
+        processorProperties.put("Prop 2", "Val 2");
+
+        final Map<String, VersionedPropertyDescriptor> propertyDescriptors = new HashMap<>();
+
+        final VersionedProcessor processor1 = new VersionedProcessor();
+        processor1.setIdentifier("p1");
+        processor1.setName("Processor 1");
+        processor1.setProperties(processorProperties);
+        processor1.setPropertyDescriptors(propertyDescriptors);
+
+        final VersionedProcessor processor2 = new VersionedProcessor();
+        processor2.setIdentifier("p2");
+        processor2.setName("Processor 2");
+        processor2.setProperties(processorProperties);
+        processor2.setPropertyDescriptors(propertyDescriptors);
+
+        subProcessGroup.getProcessors().add(processor1);
+        subProcessGroup.getProcessors().add(processor2);
+
+        final VersionedFlowSnapshot snapshot = new VersionedFlowSnapshot();
+        snapshot.setSnapshotMetadata(snapshotMetadata);
+        snapshot.setFlowContents(rootProcessGroup);
+        return snapshot;
+    }
+
+    private static VersionedFlowSnapshot createSnapshot(FlowSnapshotClient client, VersionedFlow flow, int num) throws IOException, NiFiRegistryException {
+        final VersionedFlowSnapshot snapshot = buildSnapshot(flow, num);
+
+        return client.create(snapshot);
+    }
+}

http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/6f26290d/nifi-registry-core/nifi-registry-web-api/src/test/java/org/apache/nifi/registry/web/link/TestLinkService.java
----------------------------------------------------------------------
diff --git a/nifi-registry-core/nifi-registry-web-api/src/test/java/org/apache/nifi/registry/web/link/TestLinkService.java b/nifi-registry-core/nifi-registry-web-api/src/test/java/org/apache/nifi/registry/web/link/TestLinkService.java
new file mode 100644
index 0000000..bfc9a46
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-web-api/src/test/java/org/apache/nifi/registry/web/link/TestLinkService.java
@@ -0,0 +1,124 @@
+/*
+ * 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.link;
+
+import org.apache.nifi.registry.bucket.Bucket;
+import org.apache.nifi.registry.bucket.BucketItem;
+import org.apache.nifi.registry.flow.VersionedFlow;
+import org.apache.nifi.registry.flow.VersionedFlowSnapshotMetadata;
+import org.junit.Assert;
+import org.junit.Before;
+import org.junit.Test;
+
+import java.util.ArrayList;
+import java.util.List;
+
+public class TestLinkService {
+
+    private LinkService linkService;
+
+    private List<Bucket> buckets;
+    private List<VersionedFlow> flows;
+    private List<VersionedFlowSnapshotMetadata> snapshots;
+    private List<BucketItem> items;
+
+    @Before
+    public void setup() {
+        linkService = new LinkService();
+
+        // setup buckets
+        final Bucket bucket1 = new Bucket();
+        bucket1.setIdentifier("b1");
+        bucket1.setName("Bucket 1");
+
+        final Bucket bucket2 = new Bucket();
+        bucket2.setIdentifier("b2");
+        bucket2.setName("Bucket 2");
+
+        buckets = new ArrayList<>();
+        buckets.add(bucket1);
+        buckets.add(bucket2);
+
+        // setup flows
+        final VersionedFlow flow1 = new VersionedFlow();
+        flow1.setIdentifier("f1");
+        flow1.setName("Flow 1");
+        flow1.setBucketIdentifier(bucket1.getIdentifier());
+
+        final VersionedFlow flow2 = new VersionedFlow();
+        flow2.setIdentifier("f2");
+        flow2.setName("Flow 2");
+        flow2.setBucketIdentifier(bucket1.getIdentifier());
+
+        flows = new ArrayList<>();
+        flows.add(flow1);
+        flows.add(flow2);
+
+        //setup snapshots
+        final VersionedFlowSnapshotMetadata snapshotMetadata1 = new VersionedFlowSnapshotMetadata();
+        snapshotMetadata1.setFlowIdentifier(flow1.getIdentifier());
+        snapshotMetadata1.setVersion(1);
+        snapshotMetadata1.setBucketIdentifier(bucket1.getIdentifier());
+
+        final VersionedFlowSnapshotMetadata snapshotMetadata2 = new VersionedFlowSnapshotMetadata();
+        snapshotMetadata2.setFlowIdentifier(flow1.getIdentifier());
+        snapshotMetadata2.setVersion(2);
+        snapshotMetadata2.setBucketIdentifier(bucket1.getIdentifier());
+
+        snapshots = new ArrayList<>();
+        snapshots.add(snapshotMetadata1);
+        snapshots.add(snapshotMetadata2);
+
+        // setup items
+        items = new ArrayList<>();
+        items.add(flow1);
+        items.add(flow2);
+    }
+
+    @Test
+    public void testPopulateBucketLinks() {
+        buckets.stream().forEach(b -> Assert.assertNull(b.getLink()));
+        linkService.populateBucketLinks(buckets);
+        buckets.stream().forEach(b -> Assert.assertEquals(
+                "buckets/" + b.getIdentifier(), b.getLink().getUri().toString()));
+    }
+
+    @Test
+    public void testPopulateFlowLinks() {
+        flows.stream().forEach(f -> Assert.assertNull(f.getLink()));
+        linkService.populateFlowLinks(flows);
+        flows.stream().forEach(f -> Assert.assertEquals(
+                "buckets/" + f.getBucketIdentifier() + "/flows/" + f.getIdentifier(), f.getLink().getUri().toString()));
+    }
+
+    @Test
+    public void testPopulateSnapshotLinks() {
+        snapshots.stream().forEach(s -> Assert.assertNull(s.getLink()));
+        linkService.populateSnapshotLinks(snapshots);
+        snapshots.stream().forEach(s -> Assert.assertEquals(
+                "buckets/" + s.getBucketIdentifier() + "/flows/" + s.getFlowIdentifier() + "/versions/" + s.getVersion(), s.getLink().getUri().toString()));
+    }
+
+    @Test
+    public void testPopulateItemLinks() {
+        items.stream().forEach(i -> Assert.assertNull(i.getLink()));
+        linkService.populateItemLinks(items);
+        items.stream().forEach(i -> Assert.assertEquals(
+                "buckets/" + i.getBucketIdentifier() + "/flows/" + i.getIdentifier(), i.getLink().getUri().toString()));
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/6f26290d/nifi-registry-core/nifi-registry-web-api/src/test/resources/application-ITSecureFile.properties
----------------------------------------------------------------------
diff --git a/nifi-registry-core/nifi-registry-web-api/src/test/resources/application-ITSecureFile.properties b/nifi-registry-core/nifi-registry-web-api/src/test/resources/application-ITSecureFile.properties
new file mode 100644
index 0000000..3ea5398
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-web-api/src/test/resources/application-ITSecureFile.properties
@@ -0,0 +1,36 @@
+#
+# 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.
+#
+
+
+# Properties for Spring Boot integration tests
+# Documentation for common Spring Boot application properties can be found at:
+# https://docs.spring.io/spring-boot/docs/current/reference/html/common-application-properties.html
+
+
+# Custom (non-standard to Spring Boot) properties
+nifi.registry.properties.file: src/test/resources/conf/secure-file/nifi-registry.properties
+nifi.registry.client.properties.file: src/test/resources/conf/secure-file/nifi-registry-client.properties
+
+
+# Embedded Server SSL Context Config
+server.ssl.client-auth: need
+server.ssl.key-store: ./target/test-classes/keys/localhost-ks.jks
+server.ssl.key-store-password: localhostKeystorePassword
+server.ssl.key-password: localhostKeystorePassword
+server.ssl.protocol: TLS
+server.ssl.trust-store: ./target/test-classes/keys/localhost-ts.jks
+server.ssl.trust-store-password: localhostTruststorePassword

http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/6f26290d/nifi-registry-core/nifi-registry-web-api/src/test/resources/application-ITSecureKerberos.properties
----------------------------------------------------------------------
diff --git a/nifi-registry-core/nifi-registry-web-api/src/test/resources/application-ITSecureKerberos.properties b/nifi-registry-core/nifi-registry-web-api/src/test/resources/application-ITSecureKerberos.properties
new file mode 100644
index 0000000..6ce3665
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-web-api/src/test/resources/application-ITSecureKerberos.properties
@@ -0,0 +1,36 @@
+#
+# 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.
+#
+
+
+# Properties for Spring Boot integration tests
+# Documentation for common Spring Boot application properties can be found at:
+# https://docs.spring.io/spring-boot/docs/current/reference/html/common-application-properties.html
+
+
+# Custom (non-standard to Spring Boot) properties
+nifi.registry.properties.file: src/test/resources/conf/secure-kerberos/nifi-registry.properties
+nifi.registry.client.properties.file: src/test/resources/conf/secure-kerberos/nifi-registry-client.properties
+
+
+# Embedded Server SSL Context Config
+#server.ssl.client-auth: need  # LDAP-configured server does not require two-way TLS
+server.ssl.key-store: ./target/test-classes/keys/localhost-ks.jks
+server.ssl.key-store-password: localhostKeystorePassword
+server.ssl.key-password: localhostKeystorePassword
+server.ssl.protocol: TLS
+server.ssl.trust-store: ./target/test-classes/keys/localhost-ts.jks
+server.ssl.trust-store-password: localhostTruststorePassword

http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/6f26290d/nifi-registry-core/nifi-registry-web-api/src/test/resources/application-ITSecureLdap.properties
----------------------------------------------------------------------
diff --git a/nifi-registry-core/nifi-registry-web-api/src/test/resources/application-ITSecureLdap.properties b/nifi-registry-core/nifi-registry-web-api/src/test/resources/application-ITSecureLdap.properties
new file mode 100644
index 0000000..ffcc43e
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-web-api/src/test/resources/application-ITSecureLdap.properties
@@ -0,0 +1,48 @@
+#
+# 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.
+#
+
+
+# Properties for Spring Boot integration tests
+# Documentation for common Spring Boot application properties can be found at:
+# https://docs.spring.io/spring-boot/docs/current/reference/html/common-application-properties.html
+
+
+# Custom (non-standard to Spring Boot) properties
+nifi.registry.properties.file: src/test/resources/conf/secure-ldap/nifi-registry.properties
+nifi.registry.client.properties.file: src/test/resources/conf/secure-ldap/nifi-registry-client.properties
+
+
+# Embedded Server SSL Context Config
+#server.ssl.client-auth: need  # LDAP-configured server does not require two-way TLS
+server.ssl.key-store: ./target/test-classes/keys/localhost-ks.jks
+server.ssl.key-store-password: localhostKeystorePassword
+server.ssl.key-password: localhostKeystorePassword
+server.ssl.protocol: TLS
+server.ssl.trust-store: ./target/test-classes/keys/localhost-ts.jks
+server.ssl.trust-store-password: localhostTruststorePassword
+
+# Embedded LDAP Config
+spring.ldap.embedded.base-dn: dc=example,dc=com
+spring.ldap.embedded.credential.username: cn=read-only-admin,dc=example,dc=com
+spring.ldap.embedded.credential.password: password
+spring.ldap.embedded.ldif: classpath:conf/secure-ldap/test-ldap-data.ldif
+spring.ldap.embedded.port: 8389
+spring.ldap.embedded.validation.enabled: false
+
+# Additional Logging Config
+logging.level.org.springframework.security.ldap: DEBUG
+logging.level.org.springframework.ldap: DEBUG
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/6f26290d/nifi-registry-core/nifi-registry-web-api/src/test/resources/application-ITUnsecured.properties
----------------------------------------------------------------------
diff --git a/nifi-registry-core/nifi-registry-web-api/src/test/resources/application-ITUnsecured.properties b/nifi-registry-core/nifi-registry-web-api/src/test/resources/application-ITUnsecured.properties
new file mode 100644
index 0000000..bcd338c
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-web-api/src/test/resources/application-ITUnsecured.properties
@@ -0,0 +1,21 @@
+#
+# 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.
+#
+
+# Integration Test Profile for running an unsecured NiFi Registry instance
+
+# Custom (non-standard to Spring Boot) properties
+nifi.registry.properties.file = src/test/resources/conf/unsecured/nifi-registry.properties

http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/6f26290d/nifi-registry-core/nifi-registry-web-api/src/test/resources/application.properties
----------------------------------------------------------------------
diff --git a/nifi-registry-core/nifi-registry-web-api/src/test/resources/application.properties b/nifi-registry-core/nifi-registry-web-api/src/test/resources/application.properties
new file mode 100644
index 0000000..efa0290
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-web-api/src/test/resources/application.properties
@@ -0,0 +1,25 @@
+#
+# 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.
+#
+
+# Properties for Spring Boot integration tests
+# Documentation for commoon Spring Boot application properties can be found at:
+# https://docs.spring.io/spring-boot/docs/current/reference/html/common-application-properties.html
+
+# These verbose log levels can be enabled locally for dev testing, but disable them in the repo to minimize travis logs.
+#logging.level.org.springframework.core.io.support: DEBUG
+#logging.level.org.springframework.context.annotation: DEBUG
+#logging.level.org.springframework.web: DEBUG

http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/6f26290d/nifi-registry-core/nifi-registry-web-api/src/test/resources/banner.txt
----------------------------------------------------------------------
diff --git a/nifi-registry-core/nifi-registry-web-api/src/test/resources/banner.txt b/nifi-registry-core/nifi-registry-web-api/src/test/resources/banner.txt
new file mode 100644
index 0000000..2f54644
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-web-api/src/test/resources/banner.txt
@@ -0,0 +1,8 @@
+
+  Apache NiFi   _     _
+ _ __ ___  __ _(_)___| |_ _ __ _   _
+| '__/ _ \/ _` | / __| __| '__| | | |
+| | |  __/ (_| | \__ \ |_| |  | |_| |
+|_|  \___|\__, |_|___/\__|_|   \__, |
+==========|___/================|___/=
+               Integration Test

http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/6f26290d/nifi-registry-core/nifi-registry-web-api/src/test/resources/conf/providers.xml
----------------------------------------------------------------------
diff --git a/nifi-registry-core/nifi-registry-web-api/src/test/resources/conf/providers.xml b/nifi-registry-core/nifi-registry-web-api/src/test/resources/conf/providers.xml
new file mode 100644
index 0000000..fd002be
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-web-api/src/test/resources/conf/providers.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
+<!--
+  ~ 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.
+  -->
+<providers>
+
+    <flowPersistenceProvider>
+        <class>org.apache.nifi.registry.provider.flow.FileSystemFlowPersistenceProvider</class>
+        <property name="Flow Storage Directory">./target/test-classes/flow_storage</property>
+    </flowPersistenceProvider>
+
+</providers>
\ No newline at end of file


[12/51] [partial] nifi-registry git commit: NIFIREG-201 Refactoring project structure to better isolate extensions

Posted by kd...@apache.org.
http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/6f26290d/nifi-registry-core/nifi-registry-web-api/src/test/java/org/apache/nifi/registry/web/api/FlowsIT.java
----------------------------------------------------------------------
diff --git a/nifi-registry-core/nifi-registry-web-api/src/test/java/org/apache/nifi/registry/web/api/FlowsIT.java b/nifi-registry-core/nifi-registry-web-api/src/test/java/org/apache/nifi/registry/web/api/FlowsIT.java
new file mode 100644
index 0000000..eea6969
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-web-api/src/test/java/org/apache/nifi/registry/web/api/FlowsIT.java
@@ -0,0 +1,445 @@
+/*
+ * 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.bucket.BucketItemType;
+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.flow.VersionedProcessGroup;
+import org.junit.Assert;
+import org.junit.Test;
+import org.skyscreamer.jsonassert.JSONAssert;
+import org.springframework.test.context.jdbc.Sql;
+
+import javax.ws.rs.WebApplicationException;
+import javax.ws.rs.client.Entity;
+import javax.ws.rs.client.WebTarget;
+import javax.ws.rs.core.MediaType;
+import javax.ws.rs.core.Response;
+
+import static org.apache.nifi.registry.web.api.IntegrationTestUtils.assertFlowSnapshotMetadataEqual;
+import static org.apache.nifi.registry.web.api.IntegrationTestUtils.assertFlowSnapshotsEqual;
+import static org.apache.nifi.registry.web.api.IntegrationTestUtils.assertFlowsEqual;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertTrue;
+
+@Sql(executionPhase = Sql.ExecutionPhase.BEFORE_TEST_METHOD, scripts = {"classpath:db/clearDB.sql", "classpath:db/FlowsIT.sql"})
+public class FlowsIT extends UnsecuredITBase {
+
+    @Test
+    public void testGetFlowsEmpty() throws Exception {
+
+        // Given: an empty bucket with id "3" (see FlowsIT.sql)
+        final String emptyBucketId = "3";
+
+        // When: the /buckets/{id}/flows endpoint is queried
+
+        final VersionedFlow[] flows = client
+                .target(createURL("buckets/{bucketId}/flows"))
+                .resolveTemplate("bucketId", emptyBucketId)
+                .request()
+                .get(VersionedFlow[].class);
+
+        // Then: an empty array is returned
+
+        assertNotNull(flows);
+        assertEquals(0, flows.length);
+    }
+
+    @Test
+    public void testGetFlows() throws Exception {
+
+        // Given: a few buckets and flows have been populated in the DB (see FlowsIT.sql)
+
+        final String prePopulatedBucketId = "1";
+        final String expected = "[" +
+                "{\"identifier\":\"1\"," +
+                "\"name\":\"Flow 1\"," +
+                "\"description\":\"This is flow 1\"," +
+                "\"bucketIdentifier\":\"1\"," +
+                "\"createdTimestamp\":1505091360000," +
+                "\"modifiedTimestamp\":1505091360000," +
+                "\"type\":\"Flow\"," +
+                "\"permissions\":{\"canRead\":true,\"canWrite\":true,\"canDelete\":true}," +
+                "\"link\":{\"params\":{\"rel\":\"self\"},\"href\":\"buckets/1/flows/1\"}}," +
+                "{\"identifier\":\"2\",\"name\":\"Flow 2\"," +
+                "\"description\":\"This is flow 2\"," +
+                "\"bucketIdentifier\":\"1\"," +
+                "\"createdTimestamp\":1505091360000," +
+                "\"modifiedTimestamp\":1505091360000," +
+                "\"type\":\"Flow\"," +
+                "\"permissions\":{\"canRead\":true,\"canWrite\":true,\"canDelete\":true}," +
+                "\"versionCount\":0," +
+                "\"link\":{\"params\":{\"rel\":\"self\"},\"href\":\"buckets/1/flows/2\"}}" +
+                "]";
+
+        // When: the /buckets/{id}/flows endpoint is queried
+
+        final String flowsJson = client
+                .target(createURL("buckets/{bucketId}/flows"))
+                .resolveTemplate("bucketId", prePopulatedBucketId)
+                .request()
+                .get(String.class);
+
+        // Then: the pre-populated list of flows is returned
+
+        JSONAssert.assertEquals(expected, flowsJson, false);
+    }
+
+    @Test
+    public void testCreateFlowGetFlow() throws Exception {
+
+        // Given: an empty bucket with id "3" (see FlowsIT.sql)
+
+        long testStartTime = System.currentTimeMillis();
+        final String bucketId = "3";
+
+        // When: a flow is created
+
+        final VersionedFlow flow = new VersionedFlow();
+        flow.setBucketIdentifier(bucketId);
+        flow.setName("Test Flow");
+        flow.setDescription("This is a flow created by an integration test.");
+
+        final VersionedFlow createdFlow = client
+                .target(createURL("buckets/{bucketId}/flows"))
+                .resolveTemplate("bucketId", bucketId)
+                .request()
+                .post(Entity.entity(flow, MediaType.APPLICATION_JSON), VersionedFlow.class);
+
+        // Then: the server returns the created flow, with server-set fields populated correctly
+
+        assertFlowsEqual(flow, createdFlow, false);
+        assertNotNull(createdFlow.getIdentifier());
+        assertNotNull(createdFlow.getBucketName());
+        assertEquals(0, createdFlow.getVersionCount());
+        assertEquals(createdFlow.getType(), BucketItemType.Flow);
+        assertTrue(createdFlow.getCreatedTimestamp() - testStartTime > 0L); // both server and client in same JVM, so there shouldn't be skew
+        assertEquals(createdFlow.getCreatedTimestamp(), createdFlow.getModifiedTimestamp());
+        assertNotNull(createdFlow.getLink());
+        assertNotNull(createdFlow.getLink().getUri());
+
+        // And when .../flows is queried, then the newly created flow is returned in the list
+
+        final VersionedFlow[] flows = client
+                .target(createURL("buckets/{bucketId}/flows"))
+                .resolveTemplate("bucketId", bucketId)
+                .request()
+                .get(VersionedFlow[].class);
+        assertNotNull(flows);
+        assertEquals(1, flows.length);
+        assertFlowsEqual(createdFlow, flows[0], true);
+
+        // And when the link URI is queried, then the newly created flow is returned
+
+        final VersionedFlow flowByLink = client
+                .target(createURL(flows[0].getLink().getUri().toString()))
+                .request()
+                .get(VersionedFlow.class);
+        assertFlowsEqual(createdFlow, flowByLink, true);
+
+        // And when the bucket is queried by .../flows/ID, then the newly created flow is returned
+
+        final VersionedFlow flowById = client
+                .target(createURL("buckets/{bucketId}/flows/{flowId}"))
+                .resolveTemplate("bucketId", bucketId)
+                .resolveTemplate("flowId", createdFlow.getIdentifier())
+                .request()
+                .get(VersionedFlow.class);
+        assertFlowsEqual(createdFlow, flowById, true);
+
+    }
+
+    @Test
+    public void testUpdateFlow() throws Exception {
+
+        // Given: a flow exists on the server
+
+        final String bucketId = "3";
+        final VersionedFlow flow = new VersionedFlow();
+        flow.setBucketIdentifier(bucketId);
+        flow.setName("Test Flow");
+        flow.setDescription("This is a flow created by an integration test.");
+        final VersionedFlow createdFlow = client
+                .target(createURL("buckets/{bucketId}/flows"))
+                .resolveTemplate("bucketId", bucketId)
+                .request()
+                .post(Entity.entity(flow, MediaType.APPLICATION_JSON), VersionedFlow.class);
+
+        // When: the flow is modified by the client and updated on the server
+
+        createdFlow.setName("Renamed Flow");
+        createdFlow.setDescription("This flow has been updated by an integration test.");
+
+        final VersionedFlow updatedFlow = client
+                .target(createURL("buckets/{bucketId}/flows/{flowId}"))
+                .resolveTemplate("bucketId", bucketId)
+                .resolveTemplate("flowId", createdFlow.getIdentifier())
+                .request()
+                .put(Entity.entity(createdFlow, MediaType.APPLICATION_JSON), VersionedFlow.class);
+
+        // Then: the server returns the updated flow, with a new modified timestamp
+
+        assertTrue(updatedFlow.getModifiedTimestamp() > createdFlow.getModifiedTimestamp());
+        createdFlow.setModifiedTimestamp(updatedFlow.getModifiedTimestamp());
+        assertFlowsEqual(createdFlow, updatedFlow, true);
+
+    }
+
+    @Test
+    public void testDeleteBucket() throws Exception {
+
+        // Given: a flow exists on the server
+
+        final String bucketId = "3";
+        final VersionedFlow flow = new VersionedFlow();
+        flow.setBucketIdentifier(bucketId);
+        flow.setName("Test Flow");
+        flow.setDescription("This is a flow created by an integration test.");
+        final VersionedFlow createdFlow = client
+                .target(createURL("buckets/{bucketId}/flows"))
+                .resolveTemplate("bucketId", bucketId)
+                .request()
+                .post(Entity.entity(flow, MediaType.APPLICATION_JSON), VersionedFlow.class);
+
+        // When: the flow is deleted
+
+        final VersionedFlow deletedFlow = client
+                .target(createURL("buckets/{bucketId}/flows/{flowId}"))
+                .resolveTemplate("bucketId", bucketId)
+                .resolveTemplate("flowId", createdFlow.getIdentifier())
+                .request()
+                .delete(VersionedFlow.class);
+
+        // Then: the body of the server response matches the flow that was deleted
+        //  and: the flow is no longer accessible (resource not found)
+
+        createdFlow.setLink(null); // self URI will not be present in deletedBucket
+        assertFlowsEqual(createdFlow, deletedFlow, true);
+
+        final Response response = client
+                .target(createURL("buckets/{bucketId}/flows/{flowId}"))
+                .resolveTemplate("bucketId", bucketId)
+                .resolveTemplate("flowId", createdFlow.getIdentifier())
+                .request()
+                .get();
+        assertEquals(404, response.getStatus());
+
+    }
+
+    @Test
+    public void testGetFlowVersionsEmpty() throws Exception {
+
+        // Given: a Bucket "2" containing a flow "3" with no snapshots (see FlowsIT.sql)
+        final String bucketId = "2";
+        final String flowId = "3";
+
+        // When: the /buckets/{id}/flows/{id}/versions endpoint is queried
+
+        final VersionedFlowSnapshot[] flowSnapshots = client
+                .target(createURL("buckets/{bucketId}/flows/{flowId}/versions"))
+                .resolveTemplate("bucketId", bucketId)
+                .resolveTemplate("flowId", flowId)
+                .request()
+                .get(VersionedFlowSnapshot[].class);
+
+        // Then: an empty array is returned
+
+        assertNotNull(flowSnapshots);
+        assertEquals(0, flowSnapshots.length);
+    }
+
+    @Test
+    public void testGetFlowVersions() throws Exception {
+
+        // Given: a bucket "1" with flow "1" with existing snapshots has been populated in the DB (see FlowsIT.sql)
+
+        final String prePopulatedBucketId = "1";
+        final String prePopulatedFlowId = "1";
+        // For this test case, the order of the expected list matters as we are asserting a strict equality check
+        final String expected = "[" +
+                "{\"bucketIdentifier\":\"1\"," +
+                "\"flowIdentifier\":\"1\"," +
+                "\"version\":2," +
+                "\"timestamp\":1505091480000," +
+                "\"author\" : \"user2\"," +
+                "\"comments\":\"This is flow 1 snapshot 2\"," +
+                "\"link\":{\"params\":{\"rel\":\"content\"},\"href\":\"buckets/1/flows/1/versions/2\"}}," +
+                "{\"bucketIdentifier\":\"1\"," +
+                "\"flowIdentifier\":\"1\"," +
+                "\"version\":1," +
+                "\"timestamp\":1505091420000," +
+                "\"author\" : \"user1\"," +
+                "\"comments\":\"This is flow 1 snapshot 1\"," +
+                "\"link\":{\"params\":{\"rel\":\"content\"},\"href\":\"buckets/1/flows/1/versions/1\"}}" +
+                "]";
+
+        // When: the /buckets/{id}/flows/{id}/versions endpoint is queried
+        final String flowSnapshotsJson = client
+                .target(createURL("buckets/{bucketId}/flows/{flowId}/versions"))
+                .resolveTemplate("bucketId", prePopulatedBucketId)
+                .resolveTemplate("flowId", prePopulatedFlowId)
+                .request()
+                .get(String.class);
+
+        // Then: the pre-populated list of flow versions is returned, in descending order
+        JSONAssert.assertEquals(expected, flowSnapshotsJson, true);
+
+    }
+
+    @Test
+    public void testCreateFlowVersionGetFlowVersion() throws Exception {
+
+        // Given: an empty Bucket "3" (see FlowsIT.sql) with a newly created flow
+
+        long testStartTime = System.currentTimeMillis();
+        final String bucketId = "2";
+        final VersionedFlow flow = new VersionedFlow();
+        flow.setBucketIdentifier(bucketId);
+        flow.setName("Test Flow for creating snapshots");
+        flow.setDescription("This is a randomly named flow created by an integration test for the purpose of holding snapshots.");
+        final VersionedFlow createdFlow = client
+                .target(createURL("buckets/{bucketId}/flows"))
+                .resolveTemplate("bucketId", bucketId)
+                .request()
+                .post(Entity.entity(flow, MediaType.APPLICATION_JSON), VersionedFlow.class);
+        final String flowId = createdFlow.getIdentifier();
+
+        // When: an initial flow snapshot is created *without* a version
+
+        final VersionedFlowSnapshotMetadata flowSnapshotMetadata = new VersionedFlowSnapshotMetadata();
+        flowSnapshotMetadata.setBucketIdentifier("2");
+        flowSnapshotMetadata.setFlowIdentifier(flowId);
+        flowSnapshotMetadata.setComments("This is snapshot 1, created by an integration test.");
+        final VersionedFlowSnapshot flowSnapshot = new VersionedFlowSnapshot();
+        flowSnapshot.setSnapshotMetadata(flowSnapshotMetadata);
+        flowSnapshot.setFlowContents(new VersionedProcessGroup()); // an empty root process group
+
+        WebTarget clientRequestTarget = client
+                .target(createURL("buckets/{bucketId}/flows/{flowId}/versions"))
+                .resolveTemplate("bucketId", bucketId)
+                .resolveTemplate("flowId", flowId);
+        final Response response =
+                clientRequestTarget.request().post(Entity.entity(flowSnapshot, MediaType.APPLICATION_JSON), Response.class);
+
+        // Then: an error is returned because version != 1
+
+        assertEquals(400, response.getStatus());
+
+        // But When: an initial flow snapshot is created with version == 1
+
+        flowSnapshot.getSnapshotMetadata().setVersion(1);
+        final VersionedFlowSnapshot createdFlowSnapshot =
+                clientRequestTarget.request().post(Entity.entity(flowSnapshot, MediaType.APPLICATION_JSON), VersionedFlowSnapshot.class);
+
+        // Then: the server returns the created flow snapshot, with server-set fields populated correctly :)
+
+        assertFlowSnapshotsEqual(flowSnapshot, createdFlowSnapshot, false);
+        assertTrue(createdFlowSnapshot.getSnapshotMetadata().getTimestamp() - testStartTime > 0L); // both server and client in same JVM, so there shouldn't be skew
+        assertEquals("anonymous", createdFlowSnapshot.getSnapshotMetadata().getAuthor());
+        assertNotNull(createdFlowSnapshot.getSnapshotMetadata().getLink());
+        assertNotNull(createdFlowSnapshot.getSnapshotMetadata().getLink().getUri());
+        assertNotNull(createdFlowSnapshot.getFlow());
+        assertEquals(1, createdFlowSnapshot.getFlow().getVersionCount());
+        assertNotNull(createdFlowSnapshot.getBucket());
+
+        // And when .../flows/{id}/versions is queried, then the newly created flow snapshot is returned in the list
+
+        final VersionedFlowSnapshotMetadata[] versionedFlowSnapshots =
+                clientRequestTarget.request().get(VersionedFlowSnapshotMetadata[].class);
+        assertNotNull(versionedFlowSnapshots);
+        assertEquals(1, versionedFlowSnapshots.length);
+        assertFlowSnapshotMetadataEqual(createdFlowSnapshot.getSnapshotMetadata(), versionedFlowSnapshots[0], true);
+
+        // And when the link URI is queried, then the newly created flow snapshot is returned
+
+        final VersionedFlowSnapshot flowSnapshotByLink = client
+                .target(createURL(versionedFlowSnapshots[0].getLink().getUri().toString()))
+                .request()
+                .get(VersionedFlowSnapshot.class);
+        assertFlowSnapshotsEqual(createdFlowSnapshot, flowSnapshotByLink, true);
+        assertNotNull(flowSnapshotByLink.getFlow());
+        assertNotNull(flowSnapshotByLink.getBucket());
+
+        // And when the bucket is queried by .../versions/{v}, then the newly created flow snapshot is returned
+
+        final VersionedFlowSnapshot flowSnapshotByVersionNumber = clientRequestTarget.path("/1").request().get(VersionedFlowSnapshot.class);
+        assertFlowSnapshotsEqual(createdFlowSnapshot, flowSnapshotByVersionNumber, true);
+        assertNotNull(flowSnapshotByVersionNumber.getFlow());
+        assertNotNull(flowSnapshotByVersionNumber.getBucket());
+
+        // And when the latest URI is queried, then the newly created flow snapshot is returned
+
+        final VersionedFlowSnapshot flowSnapshotByLatest = clientRequestTarget.path("/latest").request().get(VersionedFlowSnapshot.class);
+        assertFlowSnapshotsEqual(createdFlowSnapshot, flowSnapshotByLatest, true);
+        assertNotNull(flowSnapshotByLatest.getFlow());
+        assertNotNull(flowSnapshotByLatest.getBucket());
+
+    }
+
+    @Test
+    public void testFlowNameUniquePerBucket() throws Exception {
+
+        final String flowName = "Flow 1";
+
+        // verify we have an existing flow with the name "Flow 1" in bucket 1
+        final VersionedFlow existingFlow = client
+                .target(createURL("buckets/1/flows/1"))
+                .request()
+                .get(VersionedFlow.class);
+
+        assertNotNull(existingFlow);
+        assertEquals(flowName, existingFlow.getName());
+
+        // create a new flow with the same name
+
+        final String bucketId = "3";
+
+        final VersionedFlow flow = new VersionedFlow();
+        flow.setBucketIdentifier(bucketId);
+        flow.setName(flowName);
+        flow.setDescription("This is a flow created by an integration test.");
+
+        // saving this flow to bucket 3 should work because bucket 3 is empty
+
+        final VersionedFlow createdFlow = client
+                .target(createURL("buckets/3/flows"))
+                .resolveTemplate("bucketId", bucketId)
+                .request()
+                .post(Entity.entity(flow, MediaType.APPLICATION_JSON), VersionedFlow.class);
+
+        assertNotNull(createdFlow);
+
+        // saving the flow to bucket 1 should not work because there is a flow with the same name
+        flow.setBucketIdentifier("1");
+        try {
+            client.target(createURL("buckets/1/flows"))
+                    .resolveTemplate("bucketId", bucketId)
+                    .request()
+                    .post(Entity.entity(flow, MediaType.APPLICATION_JSON), VersionedFlow.class);
+
+            Assert.fail("Should have thrown exception");
+        } catch (WebApplicationException e) {
+            final String errorMessage = e.getResponse().readEntity(String.class);
+            Assert.assertEquals("A versioned flow with the same name already exists in the selected bucket", errorMessage);
+        }
+
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/6f26290d/nifi-registry-core/nifi-registry-web-api/src/test/java/org/apache/nifi/registry/web/api/IntegrationTestBase.java
----------------------------------------------------------------------
diff --git a/nifi-registry-core/nifi-registry-web-api/src/test/java/org/apache/nifi/registry/web/api/IntegrationTestBase.java b/nifi-registry-core/nifi-registry-web-api/src/test/java/org/apache/nifi/registry/web/api/IntegrationTestBase.java
new file mode 100644
index 0000000..9f3d439
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-web-api/src/test/java/org/apache/nifi/registry/web/api/IntegrationTestBase.java
@@ -0,0 +1,219 @@
+/*
+ * 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 com.fasterxml.jackson.annotation.JsonInclude;
+import com.fasterxml.jackson.databind.DeserializationFeature;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.module.jaxb.JaxbAnnotationIntrospector;
+import org.apache.nifi.registry.client.NiFiRegistryClientConfig;
+import org.apache.nifi.registry.properties.NiFiRegistryProperties;
+import org.glassfish.jersey.client.ClientConfig;
+import org.glassfish.jersey.jackson.internal.jackson.jaxrs.json.JacksonJaxbJsonProvider;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.boot.test.context.TestConfiguration;
+import org.springframework.boot.web.embedded.jetty.JettyServletWebServerFactory;
+import org.springframework.boot.web.server.LocalServerPort;
+import org.springframework.context.annotation.Bean;
+
+import javax.annotation.PostConstruct;
+import javax.net.ssl.HostnameVerifier;
+import javax.net.ssl.SSLContext;
+import javax.ws.rs.client.Client;
+import javax.ws.rs.client.ClientBuilder;
+import java.io.FileReader;
+import java.io.IOException;
+import java.util.concurrent.locks.Lock;
+import java.util.concurrent.locks.ReentrantReadWriteLock;
+
+/**
+ * A base class to simplify creating integration tests against an API application running with an embedded server and volatile DB.
+ */
+public abstract class IntegrationTestBase {
+
+    private static final String CONTEXT_PATH = "/nifi-registry-api";
+
+    @TestConfiguration
+    public static class TestConfigurationClass {
+
+        /* REQUIRED: Any subclass extending IntegrationTestBase must add a Spring profile that defines a
+         * property value for this key containing the path to the nifi-registy.properties file to use to
+         * create a NiFiRegistryProperties Bean in the ApplicationContext.  */
+        @Value("${nifi.registry.properties.file}")
+        private String propertiesFileLocation;
+
+        private final ReentrantReadWriteLock lock = new ReentrantReadWriteLock();
+        private final Lock readLock = lock.readLock();
+        private NiFiRegistryProperties testProperties;
+
+        @Bean
+        public JettyServletWebServerFactory jettyEmbeddedServletContainerFactory() {
+            JettyServletWebServerFactory jettyContainerFactory = new JettyServletWebServerFactory();
+            jettyContainerFactory.setContextPath(CONTEXT_PATH);
+            return jettyContainerFactory;
+        }
+
+        @Bean
+        public NiFiRegistryProperties getNiFiRegistryProperties() {
+            readLock.lock();
+            try {
+                if (testProperties == null) {
+                    testProperties = loadNiFiRegistryProperties(propertiesFileLocation);
+                }
+            } finally {
+                readLock.unlock();
+            }
+            return testProperties;
+        }
+
+    }
+
+    @Autowired
+    private NiFiRegistryProperties properties;
+
+    /* OPTIONAL: Any subclass that extends this base class MAY provide or specify a @TestConfiguration that provides a
+     * NiFiRegistryClientConfig @Bean. The properties specified should correspond with the integration test cases in
+     * the concrete subclass. See SecureFileIT for an example. */
+    @Autowired(required = false)
+    private NiFiRegistryClientConfig clientConfig;
+
+    /* This will be injected with the random port assigned to the embedded Jetty container. */
+    @LocalServerPort
+    private int port;
+
+    /**
+     * Subclasses can access this auto-configured JAX-RS client to communicate to the NiFi Registry Server
+     */
+    protected Client client;
+
+    @PostConstruct
+    void initialize() {
+        if (this.clientConfig != null) {
+            this.client = createClientFromConfig(this.clientConfig);
+        } else {
+            this.client = ClientBuilder.newClient();
+        }
+
+    }
+
+    /**
+     * Subclasses can utilize this method to build a URL that has the correct protocol, hostname, and port
+     * for a given path.
+     *
+     * @param relativeResourcePath the path component of the resource you wish to access, relative to the
+     *                             base API URL, where the base includes the servlet context path.
+     *
+     * @return a String containing the absolute URL of the resource.
+     */
+    String createURL(String relativeResourcePath) {
+        if (relativeResourcePath == null) {
+            throw new IllegalArgumentException("Resource path cannot be null");
+        }
+
+        final StringBuilder baseUriBuilder = new StringBuilder(createBaseURL()).append(CONTEXT_PATH);
+
+        if (!relativeResourcePath.startsWith("/")) {
+            baseUriBuilder.append('/');
+        }
+        baseUriBuilder.append(relativeResourcePath);
+
+        return baseUriBuilder.toString();
+    }
+
+    /**
+     * Sub-classes can utilize this method to obtain the base-url for a client.
+     *
+     * @return a string containing the base url which includes the scheme, host, and port
+     */
+    String createBaseURL() {
+        final boolean isSecure = this.properties.getSslPort() != null;
+        final String protocolSchema = isSecure ? "https" : "http";
+
+        final StringBuilder baseUriBuilder = new StringBuilder()
+                .append(protocolSchema).append("://localhost:").append(port);
+
+        return baseUriBuilder.toString();
+    }
+
+    NiFiRegistryClientConfig createClientConfig(String baseUrl) {
+        final NiFiRegistryClientConfig.Builder builder = new NiFiRegistryClientConfig.Builder();
+        builder.baseUrl(baseUrl);
+
+        if (this.clientConfig != null) {
+            if (this.clientConfig.getSslContext() != null) {
+                builder.sslContext(this.clientConfig.getSslContext());
+            }
+
+            if (this.clientConfig.getHostnameVerifier() != null) {
+                builder.hostnameVerifier(this.clientConfig.getHostnameVerifier());
+            }
+        }
+
+        return builder.build();
+    }
+
+    /**
+     * A helper method for loading NiFiRegistryProperties by reading *.properties files from disk.
+     *
+     * @param propertiesFilePath The location of the properties file
+     * @return A NiFIRegistryProperties instance based on the properties file contents
+     */
+    static NiFiRegistryProperties loadNiFiRegistryProperties(String propertiesFilePath) {
+        NiFiRegistryProperties properties = new NiFiRegistryProperties();
+        try (final FileReader reader = new FileReader(propertiesFilePath)) {
+            properties.load(reader);
+        } catch (final IOException ioe) {
+            throw new RuntimeException("Unable to load properties: " + ioe, ioe);
+        }
+        return properties;
+    }
+
+    private static Client createClientFromConfig(NiFiRegistryClientConfig registryClientConfig) {
+
+        final ClientConfig clientConfig = new ClientConfig();
+        clientConfig.register(jacksonJaxbJsonProvider());
+
+        final ClientBuilder clientBuilder = ClientBuilder.newBuilder().withConfig(clientConfig);
+
+        final SSLContext sslContext = registryClientConfig.getSslContext();
+        if (sslContext != null) {
+            clientBuilder.sslContext(sslContext);
+        }
+
+        final HostnameVerifier hostnameVerifier = registryClientConfig.getHostnameVerifier();
+        if (hostnameVerifier != null) {
+            clientBuilder.hostnameVerifier(hostnameVerifier);
+        }
+
+        return clientBuilder.build();
+    }
+
+    private static JacksonJaxbJsonProvider jacksonJaxbJsonProvider() {
+        JacksonJaxbJsonProvider jacksonJaxbJsonProvider = new JacksonJaxbJsonProvider();
+
+        ObjectMapper mapper = new ObjectMapper();
+        mapper.setPropertyInclusion(JsonInclude.Value.construct(JsonInclude.Include.NON_NULL, JsonInclude.Include.NON_NULL));
+        mapper.setAnnotationIntrospector(new JaxbAnnotationIntrospector(mapper.getTypeFactory()));
+        // Ignore unknown properties so that deployed client remain compatible with future versions of NiFi Registry that add new fields
+        mapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
+
+        jacksonJaxbJsonProvider.setMapper(mapper);
+        return jacksonJaxbJsonProvider;
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/6f26290d/nifi-registry-core/nifi-registry-web-api/src/test/java/org/apache/nifi/registry/web/api/IntegrationTestUtils.java
----------------------------------------------------------------------
diff --git a/nifi-registry-core/nifi-registry-web-api/src/test/java/org/apache/nifi/registry/web/api/IntegrationTestUtils.java b/nifi-registry-core/nifi-registry-web-api/src/test/java/org/apache/nifi/registry/web/api/IntegrationTestUtils.java
new file mode 100644
index 0000000..8cfcb38
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-web-api/src/test/java/org/apache/nifi/registry/web/api/IntegrationTestUtils.java
@@ -0,0 +1,120 @@
+/*
+ * 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.bucket.Bucket;
+import org.apache.nifi.registry.flow.VersionedComponent;
+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.flow.VersionedProcessGroup;
+
+import java.util.Set;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertTrue;
+
+class IntegrationTestUtils {
+
+    public static void assertBucketsEqual(Bucket expected, Bucket actual, boolean checkServerSetFields) {
+        assertNotNull(actual);
+        assertEquals(expected.getName(), actual.getName());
+        assertEquals(expected.getDescription(), actual.getDescription());
+        if (checkServerSetFields) {
+            assertEquals(expected.getIdentifier(), actual.getIdentifier());
+            assertEquals(expected.getCreatedTimestamp(), actual.getCreatedTimestamp());
+            assertEquals(expected.getPermissions(), actual.getPermissions());
+            assertEquals(expected.getLink(), actual.getLink());
+        }
+    }
+
+    public static void assertFlowsEqual(VersionedFlow expected, VersionedFlow actual, boolean checkServerSetFields) {
+        assertNotNull(actual);
+        assertEquals(expected.getName(), actual.getName());
+        assertEquals(expected.getDescription(), actual.getDescription());
+        assertEquals(expected.getBucketIdentifier(), actual.getBucketIdentifier());
+        if (checkServerSetFields) {
+            assertEquals(expected.getIdentifier(), actual.getIdentifier());
+            assertEquals(expected.getVersionCount(), actual.getVersionCount());
+            assertEquals(expected.getCreatedTimestamp(), actual.getCreatedTimestamp());
+            assertEquals(expected.getModifiedTimestamp(), actual.getModifiedTimestamp());
+            assertEquals(expected.getType(), actual.getType());
+            assertEquals(expected.getLink(), actual.getLink());
+        }
+    }
+
+    public static void assertFlowSnapshotsEqual(VersionedFlowSnapshot expected, VersionedFlowSnapshot actual, boolean checkServerSetFields) {
+
+        assertNotNull(actual);
+
+        if (expected.getSnapshotMetadata() != null) {
+            assertFlowSnapshotMetadataEqual(expected.getSnapshotMetadata(), actual.getSnapshotMetadata(), checkServerSetFields);
+        }
+
+        if (expected.getFlowContents() != null) {
+            assertVersionedProcessGroupsEqual(expected.getFlowContents(), actual.getFlowContents());
+        }
+
+        if (checkServerSetFields) {
+            assertFlowsEqual(expected.getFlow(), actual.getFlow(), false); // false because if we are checking a newly created snapshot, the versionsCount won't match
+            assertBucketsEqual(expected.getBucket(), actual.getBucket(), true);
+        }
+
+    }
+
+    public static void assertFlowSnapshotMetadataEqual(
+            VersionedFlowSnapshotMetadata expected, VersionedFlowSnapshotMetadata actual, boolean checkServerSetFields) {
+
+        assertNotNull(actual);
+        assertEquals(expected.getBucketIdentifier(), actual.getBucketIdentifier());
+        assertEquals(expected.getFlowIdentifier(), actual.getFlowIdentifier());
+        assertEquals(expected.getVersion(), actual.getVersion());
+        assertEquals(expected.getComments(), actual.getComments());
+        if (checkServerSetFields) {
+            assertEquals(expected.getTimestamp(), actual.getTimestamp());
+        }
+    }
+
+    private static void assertVersionedProcessGroupsEqual(VersionedProcessGroup expected, VersionedProcessGroup actual) {
+        assertNotNull(actual);
+
+        assertEquals(((VersionedComponent)expected), ((VersionedComponent)actual));
+
+        // Poor man's set equality assertion as we are only checking the base type and not doing a recursive check
+        // TODO, this would be a stronger assertion by replacing this with a true VersionedProcessGroup.equals() method that does a deep equality check
+        assertSetsEqual(expected.getProcessGroups(), actual.getProcessGroups());
+        assertSetsEqual(expected.getRemoteProcessGroups(), actual.getRemoteProcessGroups());
+        assertSetsEqual(expected.getProcessors(), actual.getProcessors());
+        assertSetsEqual(expected.getInputPorts(), actual.getInputPorts());
+        assertSetsEqual(expected.getOutputPorts(), actual.getOutputPorts());
+        assertSetsEqual(expected.getConnections(), actual.getConnections());
+        assertSetsEqual(expected.getLabels(), actual.getLabels());
+        assertSetsEqual(expected.getFunnels(), actual.getFunnels());
+        assertSetsEqual(expected.getControllerServices(), actual.getControllerServices());
+    }
+
+
+    private static void assertSetsEqual(Set<? extends VersionedComponent> expected, Set<? extends VersionedComponent> actual) {
+        if (expected != null) {
+            assertNotNull(actual);
+            assertEquals(expected.size(), actual.size());
+            assertTrue(actual.containsAll(expected));
+        }
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/6f26290d/nifi-registry-core/nifi-registry-web-api/src/test/java/org/apache/nifi/registry/web/api/SecureFileIT.java
----------------------------------------------------------------------
diff --git a/nifi-registry-core/nifi-registry-web-api/src/test/java/org/apache/nifi/registry/web/api/SecureFileIT.java b/nifi-registry-core/nifi-registry-web-api/src/test/java/org/apache/nifi/registry/web/api/SecureFileIT.java
new file mode 100644
index 0000000..67cb2e2
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-web-api/src/test/java/org/apache/nifi/registry/web/api/SecureFileIT.java
@@ -0,0 +1,172 @@
+/*
+ * 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.NiFiRegistryTestApiApplication;
+import org.apache.nifi.registry.authorization.ResourcePermissions;
+import org.apache.nifi.registry.authorization.Tenant;
+import org.apache.nifi.registry.authorization.User;
+import org.apache.nifi.registry.authorization.UserGroup;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.skyscreamer.jsonassert.JSONAssert;
+import org.springframework.boot.test.context.SpringBootTest;
+import org.springframework.context.annotation.Import;
+import org.springframework.test.context.jdbc.Sql;
+import org.springframework.test.context.junit4.SpringRunner;
+
+import javax.ws.rs.client.Entity;
+import javax.ws.rs.core.MediaType;
+import javax.ws.rs.core.Response;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+
+/**
+ * Deploy the Web API Application using an embedded Jetty Server for local integration testing, with the follow characteristics:
+ *
+ * - A NiFiRegistryProperties has to be explicitly provided to the ApplicationContext using a profile unique to this test suite.
+ * - A NiFiRegistryClientConfig has been configured to create a client capable of completing two-way TLS
+ * - The database is embed H2 using volatile (in-memory) persistence
+ * - Custom SQL is clearing the DB before each test method by default, unless method overrides this behavior
+ */
+@RunWith(SpringRunner.class)
+@SpringBootTest(
+        classes = NiFiRegistryTestApiApplication.class,
+        webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT,
+        properties = "spring.profiles.include=ITSecureFile")
+@Import(SecureITClientConfiguration.class)
+@Sql(executionPhase = Sql.ExecutionPhase.BEFORE_TEST_METHOD, scripts = "classpath:db/clearDB.sql")
+public class SecureFileIT extends IntegrationTestBase {
+
+    @Test
+    public void testAccessStatus() throws Exception {
+
+        // Given: the client and server have been configured correctly for two-way TLS
+        String expectedJson = "{" +
+                "\"identity\":\"CN=user1, OU=nifi\"," +
+                "\"anonymous\":false," +
+                "\"resourcePermissions\":{" +
+                "\"anyTopLevelResource\":{\"canRead\":true,\"canWrite\":true,\"canDelete\":true}," +
+                "\"buckets\":{\"canRead\":true,\"canWrite\":true,\"canDelete\":true}," +
+                "\"tenants\":{\"canRead\":true,\"canWrite\":true,\"canDelete\":true}," +
+                "\"policies\":{\"canRead\":true,\"canWrite\":true,\"canDelete\":true}," +
+                "\"proxy\":{\"canRead\":false,\"canWrite\":true,\"canDelete\":false}}" +
+                "}";
+
+        // When: the /access endpoint is queried
+        final Response response = client
+                .target(createURL("access"))
+                .request()
+                .get(Response.class);
+
+        // Then: the server returns 200 OK with the expected client identity
+        assertEquals(200, response.getStatus());
+        String actualJson = response.readEntity(String.class);
+        JSONAssert.assertEquals(expectedJson, actualJson, false);
+    }
+
+    @Test
+    public void testRetrieveResources() throws Exception {
+
+        // Given: an empty registry returns these resources
+        String expected = "[" +
+                "{\"identifier\":\"/actuator\",\"name\":\"Actuator\"}," +
+                "{\"identifier\":\"/swagger\",\"name\":\"Swagger\"}," +
+                "{\"identifier\":\"/policies\",\"name\":\"Access Policies\"}," +
+                "{\"identifier\":\"/tenants\",\"name\":\"Tenants\"}," +
+                "{\"identifier\":\"/proxy\",\"name\":\"Proxy User Requests\"}," +
+                "{\"identifier\":\"/buckets\",\"name\":\"Buckets\"}" +
+                "]";
+
+        // When: the /resources endpoint is queried
+        final String resourcesJson = client
+                .target(createURL("/policies/resources"))
+                .request()
+                .get(String.class);
+
+        // Then: the expected array of resources is returned
+        JSONAssert.assertEquals(expected, resourcesJson, false);
+    }
+
+    @Test
+    public void testCreateUser() throws Exception {
+
+        // Given: the server has been configured with FileUserGroupProvider, which is configurable,
+        //   and: the initial admin client wants to create a tenant
+        Tenant tenant = new Tenant();
+        tenant.setIdentity("New User");
+
+        // When: the POST /tenants/users endpoint is accessed
+        final Response createUserResponse = client
+                .target(createURL("tenants/users"))
+                .request()
+                .post(Entity.entity(tenant, MediaType.APPLICATION_JSON_TYPE), Response.class);
+
+        // Then: "201 created" is returned with the expected user
+        assertEquals(201, createUserResponse.getStatus());
+        User actualUser = createUserResponse.readEntity(User.class);
+        assertNotNull(actualUser.getIdentifier());
+        try {
+            assertEquals(tenant.getIdentity(), actualUser.getIdentity());
+            assertEquals(true, actualUser.getConfigurable());
+            assertEquals(0, actualUser.getUserGroups().size());
+            assertEquals(0, actualUser.getAccessPolicies().size());
+            assertEquals(new ResourcePermissions(), actualUser.getResourcePermissions());
+        } finally {
+            // cleanup user for other tests
+            client.target(createURL("tenants/users/" + actualUser.getIdentifier()))
+                    .request()
+                    .delete();
+        }
+
+    }
+
+    @Test
+    public void testCreateUserGroup() throws Exception {
+
+        // Given: the server has been configured with FileUserGroupProvider, which is configurable,
+        //   and: the initial admin client wants to create a tenant
+        Tenant tenant = new Tenant();
+        tenant.setIdentity("New Group");
+
+        // When: the POST /tenants/user-groups endpoint is used
+        final Response createUserGroupResponse = client
+                .target(createURL("tenants/user-groups"))
+                .request()
+                .post(Entity.entity(tenant, MediaType.APPLICATION_JSON_TYPE), Response.class);
+
+        // Then: 201 created is returned with the expected group
+        assertEquals(201, createUserGroupResponse.getStatus());
+        UserGroup actualUserGroup = createUserGroupResponse.readEntity(UserGroup.class);
+        assertNotNull(actualUserGroup.getIdentifier());
+        try {
+            assertEquals(tenant.getIdentity(), actualUserGroup.getIdentity());
+            assertEquals(true, actualUserGroup.getConfigurable());
+            assertEquals(0, actualUserGroup.getUsers().size());
+            assertEquals(0, actualUserGroup.getAccessPolicies().size());
+            assertEquals(new ResourcePermissions(), actualUserGroup.getResourcePermissions());
+        } finally {
+            // cleanup user for other tests
+            client.target(createURL("tenants/user-groups/" + actualUserGroup.getIdentifier()))
+                    .request()
+                    .delete();
+        }
+
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/6f26290d/nifi-registry-core/nifi-registry-web-api/src/test/java/org/apache/nifi/registry/web/api/SecureITClientConfiguration.java
----------------------------------------------------------------------
diff --git a/nifi-registry-core/nifi-registry-web-api/src/test/java/org/apache/nifi/registry/web/api/SecureITClientConfiguration.java b/nifi-registry-core/nifi-registry-web-api/src/test/java/org/apache/nifi/registry/web/api/SecureITClientConfiguration.java
new file mode 100644
index 0000000..ab07a08
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-web-api/src/test/java/org/apache/nifi/registry/web/api/SecureITClientConfiguration.java
@@ -0,0 +1,91 @@
+/*
+ * 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.client.NiFiRegistryClientConfig;
+import org.apache.nifi.registry.properties.NiFiRegistryProperties;
+import org.apache.nifi.registry.security.util.KeystoreType;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.context.annotation.Bean;
+
+import java.util.concurrent.locks.Lock;
+import java.util.concurrent.locks.ReentrantReadWriteLock;
+
+import static org.apache.nifi.registry.web.api.IntegrationTestBase.loadNiFiRegistryProperties;
+
+// Do not add Spring annotations that would cause this class to be picked up by a ComponentScan. It must be imported manually.
+public class SecureITClientConfiguration {
+
+    @Value("${nifi.registry.client.properties.file}")
+    String clientPropertiesFileLocation;
+
+    private final ReentrantReadWriteLock lock = new ReentrantReadWriteLock();
+    private final Lock readLock = lock.readLock();
+    private NiFiRegistryClientConfig clientConfig;
+
+    @Bean
+    public NiFiRegistryClientConfig getNiFiRegistryClientConfig() {
+        readLock.lock();
+        try {
+            if (clientConfig == null) {
+                final NiFiRegistryProperties clientProperties = loadNiFiRegistryProperties(clientPropertiesFileLocation);
+                clientConfig = createNiFiRegistryClientConfig(clientProperties);
+            }
+        } finally {
+            readLock.unlock();
+        }
+        return clientConfig;
+    }
+
+    /**
+     * A helper method for loading a NiFiRegistryClientConfig corresponding to a NiFiRegistryProperties object
+     * holding the values needed to create a client configuration context.
+     *
+     * @param clientProperties A NiFiRegistryProperties object holding the config for client keystore, truststore, etc.
+     * @return A NiFiRegistryClientConfig instance based on the properties file contents
+     */
+    private static NiFiRegistryClientConfig createNiFiRegistryClientConfig(NiFiRegistryProperties clientProperties) {
+
+        NiFiRegistryClientConfig.Builder configBuilder = new NiFiRegistryClientConfig.Builder();
+
+        // load keystore/truststore if applicable
+        if (clientProperties.getKeyStorePath() != null) {
+            configBuilder.keystoreFilename(clientProperties.getKeyStorePath());
+        }
+        if (clientProperties.getKeyStoreType() != null) {
+            configBuilder.keystoreType(KeystoreType.valueOf(clientProperties.getKeyStoreType()));
+        }
+        if (clientProperties.getKeyStorePassword() != null) {
+            configBuilder.keystorePassword(clientProperties.getKeyStorePassword());
+        }
+        if (clientProperties.getKeyPassword() != null) {
+            configBuilder.keyPassword(clientProperties.getKeyPassword());
+        }
+        if (clientProperties.getTrustStorePath() != null) {
+            configBuilder.truststoreFilename(clientProperties.getTrustStorePath());
+        }
+        if (clientProperties.getTrustStoreType() != null) {
+            configBuilder.truststoreType(KeystoreType.valueOf(clientProperties.getTrustStoreType()));
+        }
+        if (clientProperties.getTrustStorePassword() != null) {
+            configBuilder.truststorePassword(clientProperties.getTrustStorePassword());
+        }
+
+        return configBuilder.build();
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/6f26290d/nifi-registry-core/nifi-registry-web-api/src/test/java/org/apache/nifi/registry/web/api/SecureKerberosIT.java
----------------------------------------------------------------------
diff --git a/nifi-registry-core/nifi-registry-web-api/src/test/java/org/apache/nifi/registry/web/api/SecureKerberosIT.java b/nifi-registry-core/nifi-registry-web-api/src/test/java/org/apache/nifi/registry/web/api/SecureKerberosIT.java
new file mode 100644
index 0000000..8d8ea97
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-web-api/src/test/java/org/apache/nifi/registry/web/api/SecureKerberosIT.java
@@ -0,0 +1,216 @@
+/*
+ * 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.nifi.registry.NiFiRegistryTestApiApplication;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.skyscreamer.jsonassert.JSONAssert;
+import org.springframework.boot.test.context.SpringBootTest;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.context.annotation.Import;
+import org.springframework.context.annotation.Primary;
+import org.springframework.context.annotation.Profile;
+import org.springframework.security.authentication.BadCredentialsException;
+import org.springframework.security.kerberos.authentication.KerberosTicketValidation;
+import org.springframework.security.kerberos.authentication.KerberosTicketValidator;
+import org.springframework.test.context.jdbc.Sql;
+import org.springframework.test.context.junit4.SpringRunner;
+
+import javax.ws.rs.core.Response;
+import java.io.UnsupportedEncodingException;
+import java.nio.charset.Charset;
+import java.util.Arrays;
+import java.util.Base64;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertTrue;
+
+/**
+ * Deploy the Web API Application using an embedded Jetty Server for local integration testing, with the follow characteristics:
+ *
+ * - A NiFiRegistryProperties has to be explicitly provided to the ApplicationContext using a profile unique to this test suite.
+ * - A NiFiRegistryClientConfig has been configured to create a client capable of completing one-way TLS
+ * - The database is embed H2 using volatile (in-memory) persistence
+ * - Custom SQL is clearing the DB before each test method by default, unless method overrides this behavior
+ */
+@RunWith(SpringRunner.class)
+@SpringBootTest(
+        webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT,
+        properties = "spring.profiles.include=ITSecureKerberos")
+@Sql(executionPhase = Sql.ExecutionPhase.BEFORE_TEST_METHOD, scripts = "classpath:db/clearDB.sql")
+public class SecureKerberosIT extends IntegrationTestBase {
+
+    private static final String validKerberosTicket = "authenticate_me";
+    private static final String invalidKerberosTicket = "do_not_authenticate_me";
+
+    public static class MockKerberosTicketValidator implements KerberosTicketValidator {
+
+        @Override
+        public KerberosTicketValidation validateTicket(byte[] token) throws BadCredentialsException {
+
+            boolean validTicket;
+            try {
+                 validTicket = Arrays.equals(validKerberosTicket.getBytes("UTF-8"), token);
+            } catch (UnsupportedEncodingException e) {
+                throw new IllegalStateException(e);
+            }
+
+            if (!validTicket) {
+                throw new BadCredentialsException(MockKerberosTicketValidator.class.getSimpleName() + " does not validate token");
+            }
+
+            return new KerberosTicketValidation(
+                    "kerberosUser@LOCALHOST",
+                    "HTTP/localhsot@LOCALHOST",
+                    null,
+                    null);
+        }
+    }
+
+    @Configuration
+    @Profile("ITSecureKerberos")
+    @Import({NiFiRegistryTestApiApplication.class, SecureITClientConfiguration.class})
+    public static class KerberosSpnegoTestConfiguration {
+
+        @Primary
+        @Bean
+        public static KerberosTicketValidator kerberosTicketValidator() {
+            return new MockKerberosTicketValidator();
+        }
+
+    }
+
+    private String adminAuthToken;
+
+    @Before
+    public void generateAuthToken() {
+        String validTicket = new String(Base64.getEncoder().encode(validKerberosTicket.getBytes(Charset.forName("UTF-8"))));
+        final String token = client
+                .target(createURL("/access/token/kerberos"))
+                .request()
+                .header("Authorization", "Negotiate " + validTicket)
+                .post(null, String.class);
+        adminAuthToken = token;
+    }
+
+    @Test
+    public void testTokenGenerationAndAccessStatus() throws Exception {
+
+        // Note: this test intentionally does not use the token generated
+        // for nifiadmin by the @Before method
+
+        // Given: the client and server have been configured correctly for Kerberos SPNEGO authentication
+        String expectedJwtPayloadJson = "{" +
+                "\"sub\":\"kerberosUser@LOCALHOST\"," +
+                "\"preferred_username\":\"kerberosUser@LOCALHOST\"," +
+                "\"iss\":\"KerberosSpnegoIdentityProvider\"" +
+                "}";
+        String expectedAccessStatusJson = "{" +
+                "\"identity\":\"kerberosUser@LOCALHOST\"," +
+                "\"anonymous\":false}";
+
+        // When: the /access/token/kerberos endpoint is accessed with no credentials
+        final Response tokenResponse1 = client
+                .target(createURL("/access/token/kerberos"))
+                .request()
+                .post(null, Response.class);
+
+        // Then: the server returns 401 Unauthorized with an authenticate challenge header
+        assertEquals(401, tokenResponse1.getStatus());
+        assertNotNull(tokenResponse1.getHeaders().get("www-authenticate"));
+        assertEquals(1, tokenResponse1.getHeaders().get("www-authenticate").size());
+        assertEquals("Negotiate", tokenResponse1.getHeaders().get("www-authenticate").get(0));
+
+        // When: the /access/token/kerberos endpoint is accessed again with an invalid ticket
+        String invalidTicket = new String(java.util.Base64.getEncoder().encode(invalidKerberosTicket.getBytes(Charset.forName("UTF-8"))));
+        final Response tokenResponse2 = client
+                .target(createURL("/access/token/kerberos"))
+                .request()
+                .header("Authorization", "Negotiate " + invalidTicket)
+                .post(null, Response.class);
+
+        // Then: the server returns 401 Unauthorized
+        assertEquals(401, tokenResponse2.getStatus());
+
+        // When: the /access/token/kerberos endpoint is accessed with a valid ticket
+        String validTicket = new String(Base64.getEncoder().encode(validKerberosTicket.getBytes(Charset.forName("UTF-8"))));
+        final Response tokenResponse3 = client
+                .target(createURL("/access/token/kerberos"))
+                .request()
+                .header("Authorization", "Negotiate " + validTicket)
+                .post(null, Response.class);
+
+        // Then: the server returns 200 OK with a JWT in the body
+        assertEquals(201, tokenResponse3.getStatus());
+        String token = tokenResponse3.readEntity(String.class);
+        assertTrue(StringUtils.isNotEmpty(token));
+        String[] jwtParts = token.split("\\.");
+        assertEquals(3, jwtParts.length);
+        String jwtPayload = new String(Base64.getDecoder().decode(jwtParts[1]), "UTF-8");
+        JSONAssert.assertEquals(expectedJwtPayloadJson, jwtPayload, false);
+
+        // When: the token is returned in the Authorization header
+        final Response accessResponse = client
+                .target(createURL("access"))
+                .request()
+                .header("Authorization", "Bearer " + token)
+                .get(Response.class);
+
+        // Then: the server acknowledges the client has access
+        assertEquals(200, accessResponse.getStatus());
+        String accessStatus = accessResponse.readEntity(String.class);
+        JSONAssert.assertEquals(expectedAccessStatusJson, accessStatus, false);
+
+    }
+
+    @Test
+    public void testGetCurrentUser() throws Exception {
+
+        // Given: the client is connected to an unsecured NiFi Registry
+        String expectedJson = "{" +
+                "\"identity\":\"kerberosUser@LOCALHOST\"," +
+                "\"anonymous\":false," +
+                "\"resourcePermissions\":{" +
+                "\"anyTopLevelResource\":{\"canRead\":true,\"canWrite\":true,\"canDelete\":true}," +
+                "\"buckets\":{\"canRead\":true,\"canWrite\":true,\"canDelete\":true}," +
+                "\"tenants\":{\"canRead\":true,\"canWrite\":true,\"canDelete\":true}," +
+                "\"policies\":{\"canRead\":true,\"canWrite\":true,\"canDelete\":true}," +
+                "\"proxy\":{\"canRead\":false,\"canWrite\":true,\"canDelete\":false}}" +
+                "}";
+
+        // When: the /access endpoint is queried using a JWT for the kerberos user
+        final Response response = client
+                .target(createURL("/access"))
+                .request()
+                .header("Authorization", "Bearer " + adminAuthToken)
+                .get(Response.class);
+
+        // Then: the server returns a 200 OK with the expected current user
+        assertEquals(200, response.getStatus());
+        String actualJson = response.readEntity(String.class);
+        JSONAssert.assertEquals(expectedJson, actualJson, false);
+
+    }
+
+
+}


[46/51] [partial] nifi-registry git commit: NIFIREG-201 Refactoring project structure to better isolate extensions

Posted by kd...@apache.org.
http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/6f26290d/nifi-registry-core/nifi-registry-data-model/src/main/java/org/apache/nifi/registry/RegistryConfiguration.java
----------------------------------------------------------------------
diff --git a/nifi-registry-core/nifi-registry-data-model/src/main/java/org/apache/nifi/registry/RegistryConfiguration.java b/nifi-registry-core/nifi-registry-data-model/src/main/java/org/apache/nifi/registry/RegistryConfiguration.java
new file mode 100644
index 0000000..6c16a68
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-data-model/src/main/java/org/apache/nifi/registry/RegistryConfiguration.java
@@ -0,0 +1,78 @@
+/*
+ * 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;
+
+import io.swagger.annotations.ApiModel;
+import io.swagger.annotations.ApiModelProperty;
+
+import javax.xml.bind.annotation.XmlRootElement;
+
+@XmlRootElement
+@ApiModel(value = "registryConfiguration")
+public class RegistryConfiguration {
+
+    private Boolean supportsManagedAuthorizer;
+    private Boolean supportsConfigurableAuthorizer;
+    private Boolean supportsConfigurableUsersAndGroups;
+
+    /**
+     * @return whether this NiFi Registry supports a managed authorizer. Managed authorizers can visualize users, groups,
+     * and policies in the UI. This value is read only
+     */
+    @ApiModelProperty(
+            value = "Whether this NiFi Registry supports a managed authorizer. Managed authorizers can visualize users, groups, and policies in the UI.",
+            readOnly = true
+    )
+    public Boolean getSupportsManagedAuthorizer() {
+        return supportsManagedAuthorizer;
+    }
+
+    public void setSupportsManagedAuthorizer(Boolean supportsManagedAuthorizer) {
+        this.supportsManagedAuthorizer = supportsManagedAuthorizer;
+    }
+
+    /**
+     * @return whether this NiFi Registry supports configurable users and groups. This value is read only
+     */
+    @ApiModelProperty(
+            value = "Whether this NiFi Registry supports configurable users and groups.",
+            readOnly = true
+    )
+    public Boolean getSupportsConfigurableUsersAndGroups() {
+        return supportsConfigurableUsersAndGroups;
+    }
+
+    public void setSupportsConfigurableUsersAndGroups(Boolean supportsConfigurableUsersAndGroups) {
+        this.supportsConfigurableUsersAndGroups = supportsConfigurableUsersAndGroups;
+    }
+
+    /**
+     * @return whether this NiFi Registry supports a configurable authorizer. This value is read only
+     */
+    @ApiModelProperty(
+            value = "Whether this NiFi Registry supports a configurable authorizer.",
+            readOnly = true
+    )
+    public Boolean getSupportsConfigurableAuthorizer() {
+        return supportsConfigurableAuthorizer;
+    }
+
+    public void setSupportsConfigurableAuthorizer(Boolean supportsConfigurableAuthorizer) {
+        this.supportsConfigurableAuthorizer = supportsConfigurableAuthorizer;
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/6f26290d/nifi-registry-core/nifi-registry-data-model/src/main/java/org/apache/nifi/registry/authorization/AccessPolicy.java
----------------------------------------------------------------------
diff --git a/nifi-registry-core/nifi-registry-data-model/src/main/java/org/apache/nifi/registry/authorization/AccessPolicy.java b/nifi-registry-core/nifi-registry-data-model/src/main/java/org/apache/nifi/registry/authorization/AccessPolicy.java
new file mode 100644
index 0000000..2cf51f0
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-data-model/src/main/java/org/apache/nifi/registry/authorization/AccessPolicy.java
@@ -0,0 +1,72 @@
+/*
+ * 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.authorization;
+
+import io.swagger.annotations.ApiModel;
+import io.swagger.annotations.ApiModelProperty;
+
+import java.util.Collection;
+import java.util.HashSet;
+import java.util.Set;
+
+/**
+ * Access policy details, including the users and user groups to which the policy applies.
+ */
+@ApiModel("accessPolicy")
+public class AccessPolicy extends AccessPolicySummary {
+
+    private Set<Tenant> users;
+    private Set<Tenant> userGroups;
+
+    @ApiModelProperty(value = "The set of user IDs associated with this access policy.")
+    public Set<Tenant> getUsers() {
+        return users;
+    }
+
+    public void setUsers(Set<Tenant> users) {
+        this.users = users;
+    }
+
+    public void addUsers(Collection<? extends Tenant> users) {
+        if (users != null) {
+            if (this.users == null) {
+                this.users = new HashSet<>();
+            }
+            this.users.addAll(users);
+        }
+    }
+
+    @ApiModelProperty(value = "The set of user group IDs associated with this access policy.")
+    public Set<Tenant> getUserGroups() {
+        return userGroups;
+    }
+
+    public void setUserGroups(Set<Tenant> userGroups) {
+        this.userGroups = userGroups;
+    }
+
+    public void addUserGroups(Collection<? extends Tenant> userGroups) {
+        if (userGroups != null) {
+            if (this.userGroups == null) {
+                this.userGroups = new HashSet<>();
+            }
+            this.userGroups.addAll(userGroups);
+        }
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/6f26290d/nifi-registry-core/nifi-registry-data-model/src/main/java/org/apache/nifi/registry/authorization/AccessPolicySummary.java
----------------------------------------------------------------------
diff --git a/nifi-registry-core/nifi-registry-data-model/src/main/java/org/apache/nifi/registry/authorization/AccessPolicySummary.java b/nifi-registry-core/nifi-registry-data-model/src/main/java/org/apache/nifi/registry/authorization/AccessPolicySummary.java
new file mode 100644
index 0000000..525eb19
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-data-model/src/main/java/org/apache/nifi/registry/authorization/AccessPolicySummary.java
@@ -0,0 +1,74 @@
+/*
+ * 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.authorization;
+
+import io.swagger.annotations.ApiModel;
+import io.swagger.annotations.ApiModelProperty;
+
+/**
+ * Access policy summary of which actions ("read', "write", "delete") are allowable for a specified web resource.
+ */
+@ApiModel("accessPolicySummary")
+public class AccessPolicySummary {
+
+    private String identifier;
+    private String resource;
+    private String action;
+    private Boolean configurable;
+
+    @ApiModelProperty(value = "The id of the policy. Set by server at creation time.", readOnly = true)
+    public String getIdentifier() {
+        return identifier;
+    }
+
+    public void setIdentifier(String identifier) {
+        this.identifier = identifier;
+    }
+
+    @ApiModelProperty(value = "The resource for this access policy.", required = true
+    )
+    public String getResource() {
+        return resource;
+    }
+
+    public void setResource(String resource) {
+        this.resource = resource;
+    }
+
+    @ApiModelProperty(
+            value = "The action associated with this access policy.",
+            allowableValues = "read, write, delete",
+            required = true
+    )
+    public String getAction() {
+        return action;
+    }
+
+    public void setAction(String action) {
+        this.action = action;
+    }
+
+    @ApiModelProperty(value = "Indicates if this access policy is configurable, based on which Authorizer has been configured to manage it.", readOnly = true)
+    public Boolean getConfigurable() {
+        return configurable;
+    }
+
+    public void setConfigurable(Boolean configurable) {
+        this.configurable = configurable;
+    }
+}

http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/6f26290d/nifi-registry-core/nifi-registry-data-model/src/main/java/org/apache/nifi/registry/authorization/CurrentUser.java
----------------------------------------------------------------------
diff --git a/nifi-registry-core/nifi-registry-data-model/src/main/java/org/apache/nifi/registry/authorization/CurrentUser.java b/nifi-registry-core/nifi-registry-data-model/src/main/java/org/apache/nifi/registry/authorization/CurrentUser.java
new file mode 100644
index 0000000..0d21c62
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-data-model/src/main/java/org/apache/nifi/registry/authorization/CurrentUser.java
@@ -0,0 +1,55 @@
+/*
+ * 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.authorization;
+
+import io.swagger.annotations.ApiModel;
+import io.swagger.annotations.ApiModelProperty;
+
+@ApiModel("currentUser")
+public class CurrentUser {
+
+    private String identity;
+    private boolean anonymous;
+    private ResourcePermissions resourcePermissions;
+
+    @ApiModelProperty(value = "The identity of the current user", readOnly = true)
+    public String getIdentity() {
+        return identity;
+    }
+
+    public void setIdentity(String identity) {
+        this.identity = identity;
+    }
+
+    @ApiModelProperty(value = "Indicates if the current user is anonymous", readOnly = true)
+    public boolean isAnonymous() {
+        return anonymous;
+    }
+
+    public void setAnonymous(boolean anonymous) {
+        this.anonymous = anonymous;
+    }
+
+    @ApiModelProperty(value = "The access that the current user has to top level resources", readOnly = true)
+    public ResourcePermissions getResourcePermissions() {
+        return resourcePermissions;
+    }
+
+    public void setResourcePermissions(ResourcePermissions resourcePermissions) {
+        this.resourcePermissions = resourcePermissions;
+    }
+}

http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/6f26290d/nifi-registry-core/nifi-registry-data-model/src/main/java/org/apache/nifi/registry/authorization/Permissions.java
----------------------------------------------------------------------
diff --git a/nifi-registry-core/nifi-registry-data-model/src/main/java/org/apache/nifi/registry/authorization/Permissions.java b/nifi-registry-core/nifi-registry-data-model/src/main/java/org/apache/nifi/registry/authorization/Permissions.java
new file mode 100644
index 0000000..c76a41f
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-data-model/src/main/java/org/apache/nifi/registry/authorization/Permissions.java
@@ -0,0 +1,130 @@
+/*
+ * 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.authorization;
+
+import io.swagger.annotations.ApiModel;
+import io.swagger.annotations.ApiModelProperty;
+
+@ApiModel("permissions")
+public class Permissions {
+
+    private boolean canRead = false;
+    private boolean canWrite = false;
+    private boolean canDelete = false;
+
+    public Permissions() {
+    }
+
+    public Permissions(Permissions permissions) {
+        if (permissions == null) {
+            throw new IllegalArgumentException("Cannot call copy constructor with null argument");
+        }
+
+        this.canRead = permissions.getCanRead();
+        this.canWrite = permissions.getCanWrite();
+        this.canDelete = permissions.getCanDelete();
+    }
+
+    /**
+     * @return Indicates whether the user can read a given resource.
+     */
+    @ApiModelProperty(
+            value = "Indicates whether the user can read a given resource.",
+            readOnly = true
+    )
+    public boolean getCanRead() {
+        return canRead;
+    }
+
+    public void setCanRead(boolean canRead) {
+        this.canRead = canRead;
+    }
+
+    public Permissions withCanRead(boolean canRead) {
+        setCanRead(canRead);
+        return this;
+    }
+
+    /**
+     * @return Indicates whether the user can write a given resource.
+     */
+    @ApiModelProperty(
+            value = "Indicates whether the user can write a given resource.",
+            readOnly = true
+    )
+    public boolean getCanWrite() {
+        return canWrite;
+    }
+
+    public void setCanWrite(boolean canWrite) {
+        this.canWrite = canWrite;
+    }
+
+    public Permissions withCanWrite(boolean canWrite) {
+        setCanWrite(canWrite);
+        return this;
+    }
+
+    /**
+     * @return Indicates whether the user can delete a given resource.
+     */
+    @ApiModelProperty(
+            value = "Indicates whether the user can delete a given resource.",
+            readOnly = true
+    )
+    public boolean getCanDelete() {
+        return canDelete;
+    }
+
+    public void setCanDelete(boolean canDelete) {
+        this.canDelete = canDelete;
+    }
+
+    public Permissions withCanDelete(boolean canDelete) {
+        setCanDelete(canDelete);
+        return this;
+    }
+
+    @Override
+    public boolean equals(Object o) {
+        if (this == o) return true;
+        if (o == null || getClass() != o.getClass()) return false;
+
+        Permissions that = (Permissions) o;
+
+        if (canRead != that.canRead) return false;
+        if (canWrite != that.canWrite) return false;
+        return canDelete == that.canDelete;
+    }
+
+    @Override
+    public int hashCode() {
+        int result = (canRead ? 1 : 0);
+        result = 31 * result + (canWrite ? 1 : 0);
+        result = 31 * result + (canDelete ? 1 : 0);
+        return result;
+    }
+
+    @Override
+    public String toString() {
+        return "Permissions{" +
+                "canRead=" + canRead +
+                ", canWrite=" + canWrite +
+                ", canDelete=" + canDelete +
+                '}';
+    }
+}

http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/6f26290d/nifi-registry-core/nifi-registry-data-model/src/main/java/org/apache/nifi/registry/authorization/Resource.java
----------------------------------------------------------------------
diff --git a/nifi-registry-core/nifi-registry-data-model/src/main/java/org/apache/nifi/registry/authorization/Resource.java b/nifi-registry-core/nifi-registry-data-model/src/main/java/org/apache/nifi/registry/authorization/Resource.java
new file mode 100644
index 0000000..7dd4493
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-data-model/src/main/java/org/apache/nifi/registry/authorization/Resource.java
@@ -0,0 +1,56 @@
+/*
+ * 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.authorization;
+
+import io.swagger.annotations.ApiModel;
+import io.swagger.annotations.ApiModelProperty;
+
+@ApiModel("resource")
+public class Resource {
+
+    private String identifier;
+    private String name;
+
+    /**
+     * The name of the resource.
+     *
+     * @return The name of the resource
+     */
+    @ApiModelProperty(value = "The name of the resource.", readOnly = true)
+    public String getName() {
+        return name;
+    }
+
+    public void setName(String name) {
+        this.name = name;
+    }
+
+    /**
+     * The identifier of the resource.
+     *
+     * @return The identifier of the resource
+     */
+    @ApiModelProperty(value = "The identifier of the resource.", readOnly = true)
+    public String getIdentifier() {
+        return identifier;
+    }
+
+    public void setIdentifier(String identifier) {
+        this.identifier = identifier;
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/6f26290d/nifi-registry-core/nifi-registry-data-model/src/main/java/org/apache/nifi/registry/authorization/ResourcePermissions.java
----------------------------------------------------------------------
diff --git a/nifi-registry-core/nifi-registry-data-model/src/main/java/org/apache/nifi/registry/authorization/ResourcePermissions.java b/nifi-registry-core/nifi-registry-data-model/src/main/java/org/apache/nifi/registry/authorization/ResourcePermissions.java
new file mode 100644
index 0000000..80e95c0
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-data-model/src/main/java/org/apache/nifi/registry/authorization/ResourcePermissions.java
@@ -0,0 +1,127 @@
+/*
+ * 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.authorization;
+
+import io.swagger.annotations.ApiModel;
+import io.swagger.annotations.ApiModelProperty;
+
+@ApiModel("resourcePermissions")
+public class ResourcePermissions {
+
+    private Permissions buckets = new Permissions();
+    private Permissions tenants = new Permissions();
+    private Permissions policies = new Permissions();
+    private Permissions proxy = new Permissions();
+
+    @ApiModelProperty(
+            value = "The access that the current user has to any top level resources (a logical 'OR' of all other values)",
+            readOnly = true)
+    public Permissions getAnyTopLevelResource() {
+        return new Permissions()
+                .withCanRead(buckets.getCanRead()
+                        || tenants.getCanRead()
+                        || policies.getCanRead()
+                        || proxy.getCanRead())
+                .withCanWrite(buckets.getCanWrite()
+                        || tenants.getCanWrite()
+                        || policies.getCanWrite()
+                        || proxy.getCanWrite())
+                .withCanDelete(buckets.getCanDelete()
+                        || tenants.getCanDelete()
+                        || policies.getCanDelete()
+                        || proxy.getCanDelete());
+    }
+
+    @ApiModelProperty(
+            value = "The access that the current user has to the top level /buckets resource of this NiFi Registry (i.e., access to all buckets)",
+            readOnly = true)
+    public Permissions getBuckets() {
+        return buckets;
+    }
+
+    public void setBuckets(Permissions buckets) {
+        this.buckets = buckets;
+    }
+
+    @ApiModelProperty(
+            value = "The access that the current user has to the top level /tenants resource of this NiFi Registry",
+            readOnly = true)
+    public Permissions getTenants() {
+        return tenants;
+    }
+
+    public void setTenants(Permissions tenants) {
+        this.tenants = tenants;
+    }
+
+    @ApiModelProperty(
+            value = "The access that the current user has to the top level /policies resource of this NiFi Registry",
+            readOnly = true)
+    public Permissions getPolicies() {
+        return policies;
+    }
+
+    public void setPolicies(Permissions policies) {
+        this.policies = policies;
+    }
+
+    @ApiModelProperty(
+            value = "The access that the current user has to the top level /proxy resource of this NiFi Registry",
+            readOnly = true)
+    public Permissions getProxy() {
+        return proxy;
+    }
+
+    public void setProxy(Permissions proxy) {
+        this.proxy = proxy;
+    }
+
+    @Override
+    public boolean equals(Object o) {
+        if (this == o) return true;
+        if (o == null || getClass() != o.getClass()) return false;
+
+        ResourcePermissions that = (ResourcePermissions) o;
+
+        if (buckets != null ? !buckets.equals(that.buckets) : that.buckets != null)
+            return false;
+        if (tenants != null ? !tenants.equals(that.tenants) : that.tenants != null)
+            return false;
+        if (policies != null ? !policies.equals(that.policies) : that.policies != null)
+            return false;
+        return proxy != null ? proxy.equals(that.proxy) : that.proxy == null;
+    }
+
+    @Override
+    public int hashCode() {
+        int result = buckets != null ? buckets.hashCode() : 0;
+        result = 31 * result + (tenants != null ? tenants.hashCode() : 0);
+        result = 31 * result + (policies != null ? policies.hashCode() : 0);
+        result = 31 * result + (proxy != null ? proxy.hashCode() : 0);
+        return result;
+    }
+
+    @Override
+    public String toString() {
+        return "ResourcePermissions{" +
+                "buckets=" + buckets +
+                ", tenants=" + tenants +
+                ", policies=" + policies +
+                ", proxy=" + proxy +
+                '}';
+    }
+}

http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/6f26290d/nifi-registry-core/nifi-registry-data-model/src/main/java/org/apache/nifi/registry/authorization/Tenant.java
----------------------------------------------------------------------
diff --git a/nifi-registry-core/nifi-registry-data-model/src/main/java/org/apache/nifi/registry/authorization/Tenant.java b/nifi-registry-core/nifi-registry-data-model/src/main/java/org/apache/nifi/registry/authorization/Tenant.java
new file mode 100644
index 0000000..19eee90
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-data-model/src/main/java/org/apache/nifi/registry/authorization/Tenant.java
@@ -0,0 +1,117 @@
+/*
+ * 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.authorization;
+
+import io.swagger.annotations.ApiModel;
+import io.swagger.annotations.ApiModelProperty;
+
+import java.util.Collection;
+import java.util.HashSet;
+import java.util.Set;
+
+/**
+ * A tenant of this NiFi Registry
+ */
+@ApiModel("tenant")
+public class Tenant {
+
+    private String identifier;
+    private String identity;
+    private Boolean configurable;
+    private ResourcePermissions resourcePermissions;
+    private Set<AccessPolicySummary> accessPolicies;
+
+    public Tenant() {}
+
+    public Tenant(String identifier, String identity) {
+        this.identifier = identifier;
+        this.identity = identity;
+    }
+
+    /**
+     * @return tenant's unique identifier
+     */
+    @ApiModelProperty(
+            value = "The computer-generated identifier of the tenant.",
+            readOnly = true)
+    public String getIdentifier() {
+        return identifier;
+    }
+
+    public void setIdentifier(String identifier) {
+        this.identifier = identifier;
+    }
+
+    /**
+     * @return tenant's identity
+     */
+    @ApiModelProperty(
+            value = "The human-facing identity of the tenant. This can only be changed if the tenant is configurable.",
+            required = true)
+    public String getIdentity() {
+        return identity;
+    }
+
+    public void setIdentity(String identity) {
+        this.identity = identity;
+    }
+
+    @ApiModelProperty(
+            value = "Indicates if this tenant is configurable, based on which UserGroupProvider has been configured to manage it.",
+            readOnly = true)
+    public Boolean getConfigurable() {
+        return configurable;
+    }
+
+    public void setConfigurable(Boolean configurable) {
+        this.configurable = configurable;
+    }
+
+    @ApiModelProperty(
+            value = "A summary top-level resource access policies granted to this tenant.",
+            readOnly = true
+    )
+    public ResourcePermissions getResourcePermissions() {
+        return resourcePermissions;
+    }
+
+    public void setResourcePermissions(ResourcePermissions resourcePermissions) {
+        this.resourcePermissions = resourcePermissions;
+    }
+
+    @ApiModelProperty(
+            value = "The access policies granted to this tenant.",
+            readOnly = true
+    )
+    public Set<AccessPolicySummary> getAccessPolicies() {
+        return accessPolicies;
+    }
+
+    public void setAccessPolicies(Set<AccessPolicySummary> accessPolicies) {
+        this.accessPolicies = accessPolicies;
+    }
+
+    public void addAccessPolicies(Collection<AccessPolicySummary> accessPolicies) {
+        if (accessPolicies != null) {
+            if (this.accessPolicies == null) {
+                this.accessPolicies = new HashSet<>();
+            }
+            this.accessPolicies.addAll(accessPolicies);
+        }
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/6f26290d/nifi-registry-core/nifi-registry-data-model/src/main/java/org/apache/nifi/registry/authorization/User.java
----------------------------------------------------------------------
diff --git a/nifi-registry-core/nifi-registry-data-model/src/main/java/org/apache/nifi/registry/authorization/User.java b/nifi-registry-core/nifi-registry-data-model/src/main/java/org/apache/nifi/registry/authorization/User.java
new file mode 100644
index 0000000..6a820ab
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-data-model/src/main/java/org/apache/nifi/registry/authorization/User.java
@@ -0,0 +1,58 @@
+/*
+ * 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.authorization;
+
+import io.swagger.annotations.ApiModel;
+import io.swagger.annotations.ApiModelProperty;
+
+import java.util.Collection;
+import java.util.HashSet;
+import java.util.Set;
+
+@ApiModel("user")
+public class User extends Tenant {
+
+    private Set<Tenant> userGroups;
+
+    public User() {}
+
+    public User(String identifier, String identity) {
+        super(identifier, identity);
+    }
+
+    @ApiModelProperty(
+            value = "The groups to which the user belongs.",
+            readOnly = true
+    )
+    public Set<Tenant> getUserGroups() {
+        return userGroups;
+    }
+
+    public void setUserGroups(Set<Tenant> userGroups) {
+        this.userGroups = userGroups;
+    }
+
+    public void addUserGroups(Collection<? extends Tenant> userGroups) {
+        if (userGroups != null) {
+            if (this.userGroups == null) {
+                this.userGroups = new HashSet<>();
+            }
+            this.userGroups.addAll(userGroups);
+        }
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/6f26290d/nifi-registry-core/nifi-registry-data-model/src/main/java/org/apache/nifi/registry/authorization/UserGroup.java
----------------------------------------------------------------------
diff --git a/nifi-registry-core/nifi-registry-data-model/src/main/java/org/apache/nifi/registry/authorization/UserGroup.java b/nifi-registry-core/nifi-registry-data-model/src/main/java/org/apache/nifi/registry/authorization/UserGroup.java
new file mode 100644
index 0000000..570502d
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-data-model/src/main/java/org/apache/nifi/registry/authorization/UserGroup.java
@@ -0,0 +1,70 @@
+/*
+ * 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.authorization;
+
+import io.swagger.annotations.ApiModel;
+import io.swagger.annotations.ApiModelProperty;
+
+import java.util.Collection;
+import java.util.HashSet;
+import java.util.Set;
+
+/**
+ * A user group, used to apply a single set of authorization policies to a group of users.
+ */
+@ApiModel("userGroup")
+public class UserGroup extends Tenant {
+
+    private Set<Tenant> users;
+
+    public UserGroup() {}
+
+    public UserGroup(String identifier, String identity) {
+        super(identifier, identity);
+    }
+
+    /**
+     * @return The users that belong to this user group.
+     */
+    @ApiModelProperty(value = "The users that belong to this user group. This can only be changed if this group is configurable.")
+    public Set<Tenant> getUsers() {
+        return users;
+    }
+
+    public void setUsers(Set<Tenant> users) {
+        this.users = users;
+    }
+
+    public void addUsers(Collection<? extends Tenant> users) {
+        if (users != null) {
+            if (this.users == null) {
+                this.users = new HashSet<>();
+            }
+            this.users.addAll(users);
+        }
+    }
+
+    public void addUser(Tenant user) {
+        if (user != null) {
+            if (this.users == null) {
+                this.users = new HashSet<>();
+            }
+            this.users.add(user);
+        }
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/6f26290d/nifi-registry-core/nifi-registry-data-model/src/main/java/org/apache/nifi/registry/bucket/Bucket.java
----------------------------------------------------------------------
diff --git a/nifi-registry-core/nifi-registry-data-model/src/main/java/org/apache/nifi/registry/bucket/Bucket.java b/nifi-registry-core/nifi-registry-data-model/src/main/java/org/apache/nifi/registry/bucket/Bucket.java
new file mode 100644
index 0000000..94402a9
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-data-model/src/main/java/org/apache/nifi/registry/bucket/Bucket.java
@@ -0,0 +1,109 @@
+/*
+ * 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.bucket;
+
+import io.swagger.annotations.ApiModel;
+import io.swagger.annotations.ApiModelProperty;
+import org.apache.nifi.registry.authorization.Permissions;
+import org.apache.nifi.registry.link.LinkableEntity;
+
+import javax.validation.constraints.Min;
+import javax.validation.constraints.NotBlank;
+import javax.xml.bind.annotation.XmlRootElement;
+import java.util.Objects;
+
+@XmlRootElement
+@ApiModel(value = "bucket")
+public class Bucket extends LinkableEntity {
+
+    @NotBlank
+    private String identifier;
+
+    @NotBlank
+    private String name;
+
+    @Min(1)
+    private long createdTimestamp;
+
+    private String description;
+
+    private Permissions permissions;
+
+    @ApiModelProperty(value = "An ID to uniquely identify this object.", readOnly = true)
+    public String getIdentifier() {
+        return identifier;
+    }
+
+    public void setIdentifier(String identifier) {
+        this.identifier = identifier;
+    }
+
+    @ApiModelProperty(value = "The name of the bucket.", required = true)
+    public String getName() {
+        return name;
+    }
+
+    public void setName(String name) {
+        this.name = name;
+    }
+
+    @ApiModelProperty(value = "The timestamp of when the bucket was first created. This is set by the server at creation time.", readOnly = true)
+    public long getCreatedTimestamp() {
+        return createdTimestamp;
+    }
+
+    public void setCreatedTimestamp(long createdTimestamp) {
+        this.createdTimestamp = createdTimestamp;
+    }
+
+    @ApiModelProperty("A description of the bucket.")
+    public String getDescription() {
+        return description;
+    }
+
+    public void setDescription(String description) {
+        this.description = description;
+    }
+
+    @ApiModelProperty(value = "The access that the current user has to this bucket.", readOnly = true)
+    public Permissions getPermissions() {
+        return permissions;
+    }
+
+    public void setPermissions(Permissions permissions) {
+        this.permissions = permissions;
+    }
+
+    @Override
+    public int hashCode() {
+        return Objects.hashCode(this.getIdentifier());
+    }
+
+    @Override
+    public boolean equals(final Object obj) {
+        if (obj == null) {
+            return false;
+        }
+        if (getClass() != obj.getClass()) {
+            return false;
+        }
+
+        final Bucket other = (Bucket) obj;
+        return Objects.equals(this.getIdentifier(), other.getIdentifier());
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/6f26290d/nifi-registry-core/nifi-registry-data-model/src/main/java/org/apache/nifi/registry/bucket/BucketItem.java
----------------------------------------------------------------------
diff --git a/nifi-registry-core/nifi-registry-data-model/src/main/java/org/apache/nifi/registry/bucket/BucketItem.java b/nifi-registry-core/nifi-registry-data-model/src/main/java/org/apache/nifi/registry/bucket/BucketItem.java
new file mode 100644
index 0000000..ce9faa7
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-data-model/src/main/java/org/apache/nifi/registry/bucket/BucketItem.java
@@ -0,0 +1,155 @@
+/*
+ * 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.bucket;
+
+import io.swagger.annotations.ApiModel;
+import io.swagger.annotations.ApiModelProperty;
+import org.apache.nifi.registry.authorization.Permissions;
+import org.apache.nifi.registry.link.LinkableEntity;
+
+import javax.validation.constraints.Min;
+import javax.validation.constraints.NotBlank;
+import javax.validation.constraints.NotNull;
+import java.util.Objects;
+
+@ApiModel("bucketItem")
+public abstract class BucketItem extends LinkableEntity {
+
+    @NotBlank
+    private String identifier;
+
+    @NotBlank
+    private String name;
+
+    private String description;
+
+    @NotBlank
+    private String bucketIdentifier;
+
+    // read-only
+    private String bucketName;
+
+    @Min(1)
+    private long createdTimestamp;
+
+    @Min(1)
+    private long modifiedTimestamp;
+
+    @NotNull
+    private final BucketItemType type;
+
+    private Permissions permissions;
+
+    public BucketItem(final BucketItemType type) {
+        this.type = type;
+    }
+
+    @ApiModelProperty(value = "An ID to uniquely identify this object.", readOnly = true)
+    public String getIdentifier() {
+        return identifier;
+    }
+
+    public void setIdentifier(String identifier) {
+        this.identifier = identifier;
+    }
+
+    @ApiModelProperty(value = "The name of the item.", required = true)
+    public String getName() {
+        return name;
+    }
+
+    public void setName(String name) {
+        this.name = name;
+    }
+
+    @ApiModelProperty("A description of the item.")
+    public String getDescription() {
+        return description;
+    }
+
+    public void setDescription(String description) {
+        this.description = description;
+    }
+
+    @ApiModelProperty(value = "The identifier of the bucket this items belongs to. This cannot be changed after the item is created.", required = true)
+    public String getBucketIdentifier() {
+        return bucketIdentifier;
+    }
+
+    public void setBucketIdentifier(String bucketIdentifier) {
+        this.bucketIdentifier = bucketIdentifier;
+    }
+
+    @ApiModelProperty(value = "The name of the bucket this items belongs to.", readOnly = true)
+    public String getBucketName() {
+        return bucketName;
+    }
+
+    public void setBucketName(String bucketName) {
+        this.bucketName = bucketName;
+    }
+
+    @ApiModelProperty(value = "The timestamp of when the item was created, as milliseconds since epoch.", readOnly = true)
+    public long getCreatedTimestamp() {
+        return createdTimestamp;
+    }
+
+    public void setCreatedTimestamp(long createdTimestamp) {
+        this.createdTimestamp = createdTimestamp;
+    }
+
+    @ApiModelProperty(value = "The timestamp of when the item was last modified, as milliseconds since epoch.", readOnly = true)
+    public long getModifiedTimestamp() {
+        return modifiedTimestamp;
+    }
+
+    public void setModifiedTimestamp(long modifiedTimestamp) {
+        this.modifiedTimestamp = modifiedTimestamp;
+    }
+
+    @ApiModelProperty(value = "The type of item.", required = true)
+    public BucketItemType getType() {
+        return type;
+    }
+
+    @ApiModelProperty(value = "The access that the current user has to the bucket containing this item.", readOnly = true)
+    public Permissions getPermissions() {
+        return permissions;
+    }
+
+    public void setPermissions(Permissions permissions) {
+        this.permissions = permissions;
+    }
+
+    @Override
+    public int hashCode() {
+        return Objects.hashCode(this.getIdentifier());
+    }
+
+    @Override
+    public boolean equals(final Object obj) {
+        if (obj == null) {
+            return false;
+        }
+        if (getClass() != obj.getClass()) {
+            return false;
+        }
+
+        final BucketItem other = (BucketItem) obj;
+        return Objects.equals(this.getIdentifier(), other.getIdentifier());
+    }
+}

http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/6f26290d/nifi-registry-core/nifi-registry-data-model/src/main/java/org/apache/nifi/registry/bucket/BucketItemType.java
----------------------------------------------------------------------
diff --git a/nifi-registry-core/nifi-registry-data-model/src/main/java/org/apache/nifi/registry/bucket/BucketItemType.java b/nifi-registry-core/nifi-registry-data-model/src/main/java/org/apache/nifi/registry/bucket/BucketItemType.java
new file mode 100644
index 0000000..e119c02
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-data-model/src/main/java/org/apache/nifi/registry/bucket/BucketItemType.java
@@ -0,0 +1,27 @@
+/*
+ * 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.bucket;
+
+/**
+ * Type of item in a bucket.
+ */
+public enum BucketItemType {
+
+    // The case of these enum names matches what we want to return in
+    // the BucketItem.type field when serialized in an API response.
+    Flow;
+}

http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/6f26290d/nifi-registry-core/nifi-registry-data-model/src/main/java/org/apache/nifi/registry/diff/ComponentDifference.java
----------------------------------------------------------------------
diff --git a/nifi-registry-core/nifi-registry-data-model/src/main/java/org/apache/nifi/registry/diff/ComponentDifference.java b/nifi-registry-core/nifi-registry-data-model/src/main/java/org/apache/nifi/registry/diff/ComponentDifference.java
new file mode 100644
index 0000000..d4fb5d0
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-data-model/src/main/java/org/apache/nifi/registry/diff/ComponentDifference.java
@@ -0,0 +1,77 @@
+/*
+ * 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.diff;
+
+import io.swagger.annotations.ApiModelProperty;
+
+/**
+ * Represents a specific, individual difference that has changed between 2 versions.
+ * The change data and textual descriptions of the change are included for client consumption.
+ */
+public class ComponentDifference {
+    private String valueA;
+    private String valueB;
+    private String changeDescription;
+    private String differenceType;
+    private String differenceTypeDescription;
+
+    @ApiModelProperty("The earlier value from the difference.")
+    public String getValueA() {
+        return valueA;
+    }
+
+    public void setValueA(String valueA) {
+        this.valueA = valueA;
+    }
+
+    @ApiModelProperty("The newer value from the difference.")
+    public String getValueB() {
+        return valueB;
+    }
+
+    public void setValueB(String valueB) {
+        this.valueB = valueB;
+    }
+
+    @ApiModelProperty("The description of the change.")
+    public String getChangeDescription() {
+        return changeDescription;
+    }
+
+    public void setChangeDescription(String changeDescription) {
+        this.changeDescription = changeDescription;
+    }
+
+    @ApiModelProperty("The key to the difference.")
+    public String getDifferenceType() {
+        return differenceType;
+    }
+
+    public void setDifferenceType(String differenceType) {
+        this.differenceType = differenceType;
+    }
+
+    @ApiModelProperty("The description of the change type.")
+    public String getDifferenceTypeDescription() {
+        return differenceTypeDescription;
+    }
+
+    public void setDifferenceTypeDescription(String differenceTypeDescription) {
+        this.differenceTypeDescription = differenceTypeDescription;
+    }
+}

http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/6f26290d/nifi-registry-core/nifi-registry-data-model/src/main/java/org/apache/nifi/registry/diff/ComponentDifferenceGroup.java
----------------------------------------------------------------------
diff --git a/nifi-registry-core/nifi-registry-data-model/src/main/java/org/apache/nifi/registry/diff/ComponentDifferenceGroup.java b/nifi-registry-core/nifi-registry-data-model/src/main/java/org/apache/nifi/registry/diff/ComponentDifferenceGroup.java
new file mode 100644
index 0000000..a7b1d58
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-data-model/src/main/java/org/apache/nifi/registry/diff/ComponentDifferenceGroup.java
@@ -0,0 +1,96 @@
+/*
+ * 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.diff;
+
+import io.swagger.annotations.ApiModelProperty;
+
+import java.util.HashSet;
+import java.util.Objects;
+import java.util.Set;
+
+/**
+ * Represents a group of differences related to a specific component in a flow.
+ */
+public class ComponentDifferenceGroup {
+    private String componentId;
+    private String componentName;
+    private String componentType;
+    private String processGroupId;
+    private Set<ComponentDifference> differences = new HashSet<>();
+
+    @ApiModelProperty("The id of the component whose changes are grouped together.")
+    public String getComponentId() {
+        return componentId;
+    }
+
+    public void setComponentId(String componentId) {
+        this.componentId = componentId;
+    }
+
+    @ApiModelProperty("The name of the component whose changes are grouped together.")
+    public String getComponentName() {
+        return componentName;
+    }
+
+    public void setComponentName(String componentName) {
+        this.componentName = componentName;
+    }
+
+    @ApiModelProperty("The type of component these changes relate to.")
+    public String getComponentType() {
+        return componentType;
+    }
+
+    public void setComponentType(String componentType) {
+        this.componentType = componentType;
+    }
+
+    @ApiModelProperty("The process group id for this component.")
+    public String getProcessGroupId() {
+        return processGroupId;
+    }
+
+    public void setProcessGroupId(String processGroupId) {
+        this.processGroupId = processGroupId;
+    }
+
+    @ApiModelProperty("The list of changes related to this component between the 2 versions.")
+    public Set<ComponentDifference> getDifferences() {
+        return differences;
+    }
+
+    public void setDifferences(Set<ComponentDifference> differences) {
+        this.differences = differences;
+    }
+
+    @Override
+    public boolean equals(Object o) {
+        if (this == o) return true;
+        if (o == null || getClass() != o.getClass()) return false;
+        ComponentDifferenceGroup that = (ComponentDifferenceGroup) o;
+        return Objects.equals(componentId, that.componentId)
+                && Objects.equals(componentName, that.componentName)
+                && Objects.equals(componentType, that.componentType)
+                && Objects.equals(processGroupId, that.processGroupId);
+    }
+
+    @Override
+    public int hashCode() {
+        return Objects.hash(componentId, componentName, componentType, processGroupId);
+    }
+}

http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/6f26290d/nifi-registry-core/nifi-registry-data-model/src/main/java/org/apache/nifi/registry/diff/VersionedFlowDifference.java
----------------------------------------------------------------------
diff --git a/nifi-registry-core/nifi-registry-data-model/src/main/java/org/apache/nifi/registry/diff/VersionedFlowDifference.java b/nifi-registry-core/nifi-registry-data-model/src/main/java/org/apache/nifi/registry/diff/VersionedFlowDifference.java
new file mode 100644
index 0000000..ccc6a05
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-data-model/src/main/java/org/apache/nifi/registry/diff/VersionedFlowDifference.java
@@ -0,0 +1,79 @@
+/*
+ * 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.diff;
+
+import io.swagger.annotations.ApiModelProperty;
+
+import java.util.Set;
+
+/**
+ * Represents the result of a diff between 2 versions of the same flow.
+ * A subset of the model classes in registry.flow.diff for exposing on the API
+ * The differences are grouped by component
+ */
+public class VersionedFlowDifference {
+    private String bucketId;
+    private String flowId;
+    private int versionA;
+    private int versionB;
+    private Set<ComponentDifferenceGroup> componentDifferenceGroups;
+
+    public Set<ComponentDifferenceGroup> getComponentDifferenceGroups() {
+        return componentDifferenceGroups;
+    }
+
+    public void setComponentDifferenceGroups(Set<ComponentDifferenceGroup> componentDifferenceGroups) {
+        this.componentDifferenceGroups = componentDifferenceGroups;
+    }
+
+    @ApiModelProperty("The id of the bucket that the flow is stored in.")
+    public String getBucketId() {
+        return bucketId;
+    }
+
+    public void setBucketId(String bucketId) {
+        this.bucketId = bucketId;
+    }
+
+    @ApiModelProperty("The id of the flow that is being examined.")
+    public String getFlowId() {
+        return flowId;
+    }
+
+    public void setFlowId(String flowId) {
+        this.flowId = flowId;
+    }
+
+    @ApiModelProperty("The earlier version from the diff operation.")
+    public int getVersionA() {
+        return versionA;
+    }
+
+    public void setVersionA(int versionA) {
+        this.versionA = versionA;
+    }
+
+    @ApiModelProperty("The latter version from the diff operation.")
+    public int getVersionB() {
+        return versionB;
+    }
+
+    public void setVersionB(int versionB) {
+        this.versionB = versionB;
+    }
+}

http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/6f26290d/nifi-registry-core/nifi-registry-data-model/src/main/java/org/apache/nifi/registry/field/Fields.java
----------------------------------------------------------------------
diff --git a/nifi-registry-core/nifi-registry-data-model/src/main/java/org/apache/nifi/registry/field/Fields.java b/nifi-registry-core/nifi-registry-data-model/src/main/java/org/apache/nifi/registry/field/Fields.java
new file mode 100644
index 0000000..d1aac6d
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-data-model/src/main/java/org/apache/nifi/registry/field/Fields.java
@@ -0,0 +1,41 @@
+/*
+ * 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.field;
+
+import java.util.Set;
+
+public class Fields {
+
+    private Set<String> fields;
+
+    public Fields() {
+
+    }
+
+    public Fields(Set<String> fields) {
+        this.fields = fields;
+    }
+
+    public Set<String> getFields() {
+        return fields;
+    }
+
+    public void setFields(Set<String> fields) {
+        this.fields = fields;
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/6f26290d/nifi-registry-core/nifi-registry-data-model/src/main/java/org/apache/nifi/registry/flow/BatchSize.java
----------------------------------------------------------------------
diff --git a/nifi-registry-core/nifi-registry-data-model/src/main/java/org/apache/nifi/registry/flow/BatchSize.java b/nifi-registry-core/nifi-registry-data-model/src/main/java/org/apache/nifi/registry/flow/BatchSize.java
new file mode 100644
index 0000000..5a51240
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-data-model/src/main/java/org/apache/nifi/registry/flow/BatchSize.java
@@ -0,0 +1,76 @@
+/*
+ * 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.flow;
+
+import io.swagger.annotations.ApiModelProperty;
+import java.util.Objects;
+
+
+public class BatchSize {
+    private Integer count;
+    private String size;
+    private String duration;
+
+    @ApiModelProperty("Preferred number of flow files to include in a transaction.")
+    public Integer getCount() {
+        return count;
+    }
+
+    public void setCount(Integer count) {
+        this.count = count;
+    }
+
+    @ApiModelProperty("Preferred number of bytes to include in a transaction.")
+    public String getSize() {
+        return size;
+    }
+
+    public void setSize(String size) {
+        this.size = size;
+    }
+
+    @ApiModelProperty("Preferred amount of time that a transaction should span.")
+    public String getDuration() {
+        return duration;
+    }
+
+    public void setDuration(String duration) {
+        this.duration = duration;
+    }
+
+    @Override
+    public int hashCode() {
+        return Objects.hash(count, duration, size);
+    }
+
+    @Override
+    public boolean equals(Object obj) {
+        if (this == obj) {
+            return true;
+        }
+        if (obj == null) {
+            return false;
+        }
+        if (getClass() != obj.getClass()) {
+            return false;
+        }
+
+        final BatchSize other = (BatchSize) obj;
+        return Objects.equals(count, other.count) && Objects.equals(size, other.size) && Objects.equals(duration, other.duration);
+    }
+}

http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/6f26290d/nifi-registry-core/nifi-registry-data-model/src/main/java/org/apache/nifi/registry/flow/Bundle.java
----------------------------------------------------------------------
diff --git a/nifi-registry-core/nifi-registry-data-model/src/main/java/org/apache/nifi/registry/flow/Bundle.java b/nifi-registry-core/nifi-registry-data-model/src/main/java/org/apache/nifi/registry/flow/Bundle.java
new file mode 100644
index 0000000..1050ac9
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-data-model/src/main/java/org/apache/nifi/registry/flow/Bundle.java
@@ -0,0 +1,83 @@
+/*
+ * 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.flow;
+
+import java.util.Objects;
+
+import io.swagger.annotations.ApiModelProperty;
+
+public class Bundle {
+    private String group;
+    private String artifact;
+    private String version;
+
+    public Bundle() {
+    }
+
+    public Bundle(final String group, final String artifact, final String version) {
+        this.group = group;
+        this.artifact = artifact;
+        this.version = version;
+    }
+
+    @ApiModelProperty("The group of the bundle")
+    public String getGroup() {
+        return group;
+    }
+
+    public void setGroup(String group) {
+        this.group = group;
+    }
+
+    @ApiModelProperty("The artifact of the bundle")
+    public String getArtifact() {
+        return artifact;
+    }
+
+    public void setArtifact(String artifact) {
+        this.artifact = artifact;
+    }
+
+    @ApiModelProperty("The version of the bundle")
+    public String getVersion() {
+        return version;
+    }
+
+    public void setVersion(String version) {
+        this.version = version;
+    }
+
+    @Override
+    public boolean equals(Object obj) {
+        if (this == obj) {
+            return true;
+        }
+
+        if (obj == null || getClass() != obj.getClass()) {
+            return false;
+        }
+
+        final Bundle other = (Bundle) obj;
+        return Objects.equals(group, other.group) && Objects.equals(artifact, other.artifact) && Objects.equals(version, other.version);
+    }
+
+    @Override
+    public int hashCode() {
+        return Objects.hash(group, artifact, version);
+    }
+}

http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/6f26290d/nifi-registry-core/nifi-registry-data-model/src/main/java/org/apache/nifi/registry/flow/ComponentType.java
----------------------------------------------------------------------
diff --git a/nifi-registry-core/nifi-registry-data-model/src/main/java/org/apache/nifi/registry/flow/ComponentType.java b/nifi-registry-core/nifi-registry-data-model/src/main/java/org/apache/nifi/registry/flow/ComponentType.java
new file mode 100644
index 0000000..300c146
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-data-model/src/main/java/org/apache/nifi/registry/flow/ComponentType.java
@@ -0,0 +1,49 @@
+/*
+ * 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.flow;
+
+public enum ComponentType {
+
+    CONNECTION("Connection"),
+    PROCESSOR("Processor"),
+    PROCESS_GROUP("Process Group"),
+    REMOTE_PROCESS_GROUP("Remote Process Group"),
+    INPUT_PORT("Input Port"),
+    OUTPUT_PORT("Output Port"),
+    REMOTE_INPUT_PORT("Remote Input Port"),
+    REMOTE_OUTPUT_PORT("Remote Output Port"),
+    FUNNEL("Funnel"),
+    LABEL("Label"),
+    CONTROLLER_SERVICE("Controller Service");
+
+
+    private final String typeName;
+
+    private ComponentType(final String typeName) {
+        this.typeName = typeName;
+    }
+
+    public String getTypeName() {
+        return typeName;
+    }
+
+    @Override
+    public String toString() {
+        return typeName;
+    }
+}

http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/6f26290d/nifi-registry-core/nifi-registry-data-model/src/main/java/org/apache/nifi/registry/flow/ConnectableComponent.java
----------------------------------------------------------------------
diff --git a/nifi-registry-core/nifi-registry-data-model/src/main/java/org/apache/nifi/registry/flow/ConnectableComponent.java b/nifi-registry-core/nifi-registry-data-model/src/main/java/org/apache/nifi/registry/flow/ConnectableComponent.java
new file mode 100644
index 0000000..de144f2
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-data-model/src/main/java/org/apache/nifi/registry/flow/ConnectableComponent.java
@@ -0,0 +1,95 @@
+/*
+ * 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.flow;
+
+import io.swagger.annotations.ApiModelProperty;
+import java.util.Objects;
+
+
+public class ConnectableComponent {
+    private String id;
+    private ConnectableComponentType type;
+    private String groupId;
+    private String name;
+    private String comments;
+
+    @ApiModelProperty(value = "The id of the connectable component.", required = true)
+    public String getId() {
+        return id;
+    }
+
+    public void setId(String id) {
+        this.id = id;
+    }
+
+    @ApiModelProperty(value = "The type of component the connectable is.", required = true)
+    public ConnectableComponentType getType() {
+        return type;
+    }
+
+    public void setType(ConnectableComponentType type) {
+        this.type = type;
+    }
+
+    @ApiModelProperty(value = "The id of the group that the connectable component resides in", required = true)
+    public String getGroupId() {
+        return groupId;
+    }
+
+    public void setGroupId(String groupId) {
+        this.groupId = groupId;
+    }
+
+    @ApiModelProperty("The name of the connectable component")
+    public String getName() {
+        return name;
+    }
+
+    public void setName(String name) {
+        this.name = name;
+    }
+
+    @ApiModelProperty("The comments for the connectable component.")
+    public String getComments() {
+        return comments;
+    }
+
+    public void setComments(String comments) {
+        this.comments = comments;
+    }
+
+    @Override
+    public int hashCode() {
+        return Objects.hash(id, groupId, name, type, comments);
+    }
+
+    @Override
+    public boolean equals(Object obj) {
+        if (obj == null) {
+            return false;
+        }
+        if (obj == this) {
+            return true;
+        }
+        if (!(obj instanceof ConnectableComponent)) {
+            return false;
+        }
+        final ConnectableComponent other = (ConnectableComponent) obj;
+        return Objects.equals(id, other.id);
+    }
+}

http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/6f26290d/nifi-registry-core/nifi-registry-data-model/src/main/java/org/apache/nifi/registry/flow/ConnectableComponentType.java
----------------------------------------------------------------------
diff --git a/nifi-registry-core/nifi-registry-data-model/src/main/java/org/apache/nifi/registry/flow/ConnectableComponentType.java b/nifi-registry-core/nifi-registry-data-model/src/main/java/org/apache/nifi/registry/flow/ConnectableComponentType.java
new file mode 100644
index 0000000..1b73cac
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-data-model/src/main/java/org/apache/nifi/registry/flow/ConnectableComponentType.java
@@ -0,0 +1,27 @@
+/*
+ * 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.flow;
+
+public enum ConnectableComponentType {
+    PROCESSOR,
+    REMOTE_INPUT_PORT,
+    REMOTE_OUTPUT_PORT,
+    INPUT_PORT,
+    OUTPUT_PORT,
+    FUNNEL;
+}

http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/6f26290d/nifi-registry-core/nifi-registry-data-model/src/main/java/org/apache/nifi/registry/flow/ControllerServiceAPI.java
----------------------------------------------------------------------
diff --git a/nifi-registry-core/nifi-registry-data-model/src/main/java/org/apache/nifi/registry/flow/ControllerServiceAPI.java b/nifi-registry-core/nifi-registry-data-model/src/main/java/org/apache/nifi/registry/flow/ControllerServiceAPI.java
new file mode 100644
index 0000000..b46e87a
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-data-model/src/main/java/org/apache/nifi/registry/flow/ControllerServiceAPI.java
@@ -0,0 +1,65 @@
+/*
+ * 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.flow;
+
+import java.util.Objects;
+
+import io.swagger.annotations.ApiModelProperty;
+
+public class ControllerServiceAPI {
+    private String type;
+    private Bundle bundle;
+
+    @ApiModelProperty("The fully qualified name of the service interface.")
+    public String getType() {
+        return type;
+    }
+
+    public void setType(String type) {
+        this.type = type;
+    }
+
+    @ApiModelProperty("The details of the artifact that bundled this service interface.")
+    public Bundle getBundle() {
+        return bundle;
+    }
+
+    public void setBundle(Bundle bundle) {
+        this.bundle = bundle;
+    }
+
+    @Override
+    public boolean equals(Object o) {
+        if (this == o) {
+            return true;
+        }
+
+        if (o == null || getClass() != o.getClass()) {
+            return false;
+        }
+
+        final ControllerServiceAPI other = (ControllerServiceAPI) o;
+        return Objects.equals(type, other.type) && Objects.equals(bundle, other.bundle);
+    }
+
+    @Override
+    public int hashCode() {
+        return Objects.hash(type, bundle);
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/6f26290d/nifi-registry-core/nifi-registry-data-model/src/main/java/org/apache/nifi/registry/flow/PortType.java
----------------------------------------------------------------------
diff --git a/nifi-registry-core/nifi-registry-data-model/src/main/java/org/apache/nifi/registry/flow/PortType.java b/nifi-registry-core/nifi-registry-data-model/src/main/java/org/apache/nifi/registry/flow/PortType.java
new file mode 100644
index 0000000..6a32c11
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-data-model/src/main/java/org/apache/nifi/registry/flow/PortType.java
@@ -0,0 +1,23 @@
+/*
+ * 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.flow;
+
+public enum PortType {
+    INPUT_PORT,
+    OUTPUT_PORT;
+}

http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/6f26290d/nifi-registry-core/nifi-registry-data-model/src/main/java/org/apache/nifi/registry/flow/Position.java
----------------------------------------------------------------------
diff --git a/nifi-registry-core/nifi-registry-data-model/src/main/java/org/apache/nifi/registry/flow/Position.java b/nifi-registry-core/nifi-registry-data-model/src/main/java/org/apache/nifi/registry/flow/Position.java
new file mode 100644
index 0000000..bee14d2
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-data-model/src/main/java/org/apache/nifi/registry/flow/Position.java
@@ -0,0 +1,87 @@
+/*
+ * 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.flow;
+
+import org.apache.commons.lang3.builder.EqualsBuilder;
+import org.apache.commons.lang3.builder.HashCodeBuilder;
+
+import io.swagger.annotations.ApiModel;
+import io.swagger.annotations.ApiModelProperty;
+
+@ApiModel(description = "The position of a component on the graph")
+public class Position {
+    private double x;
+    private double y;
+
+    public Position() {
+    }
+
+    public Position(double x, double y) {
+        this.x = x;
+        this.y = y;
+    }
+
+    @ApiModelProperty("The x coordinate.")
+    public double getX() {
+        return x;
+    }
+
+    public void setX(double x) {
+        this.x = x;
+    }
+
+    @ApiModelProperty("The y coordinate.")
+    public double getY() {
+        return y;
+    }
+
+    public void setY(double y) {
+        this.y = y;
+    }
+
+    @Override
+    public String toString() {
+        return "[x=" + x + ", y=" + y + "]";
+    }
+
+    @Override
+    public boolean equals(Object o) {
+        if (this == o) {
+            return true;
+        }
+
+        if (o == null || getClass() != o.getClass()) {
+            return false;
+        }
+
+        Position position = (Position) o;
+
+        return new EqualsBuilder()
+                .append(x, position.x)
+                .append(y, position.y)
+                .isEquals();
+    }
+
+    @Override
+    public int hashCode() {
+        return new HashCodeBuilder(17, 37)
+                .append(x)
+                .append(y)
+                .toHashCode();
+    }
+}

http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/6f26290d/nifi-registry-core/nifi-registry-data-model/src/main/java/org/apache/nifi/registry/flow/SiteToSiteTransportProtocol.java
----------------------------------------------------------------------
diff --git a/nifi-registry-core/nifi-registry-data-model/src/main/java/org/apache/nifi/registry/flow/SiteToSiteTransportProtocol.java b/nifi-registry-core/nifi-registry-data-model/src/main/java/org/apache/nifi/registry/flow/SiteToSiteTransportProtocol.java
new file mode 100644
index 0000000..9f94c1a
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-data-model/src/main/java/org/apache/nifi/registry/flow/SiteToSiteTransportProtocol.java
@@ -0,0 +1,23 @@
+/*
+ * 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.flow;
+
+public enum SiteToSiteTransportProtocol {
+    RAW,
+    HTTP;
+}

http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/6f26290d/nifi-registry-core/nifi-registry-data-model/src/main/java/org/apache/nifi/registry/flow/VersionedComponent.java
----------------------------------------------------------------------
diff --git a/nifi-registry-core/nifi-registry-data-model/src/main/java/org/apache/nifi/registry/flow/VersionedComponent.java b/nifi-registry-core/nifi-registry-data-model/src/main/java/org/apache/nifi/registry/flow/VersionedComponent.java
new file mode 100644
index 0000000..b3a27aa
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-data-model/src/main/java/org/apache/nifi/registry/flow/VersionedComponent.java
@@ -0,0 +1,103 @@
+/*
+ * 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.flow;
+
+import java.util.Objects;
+
+import io.swagger.annotations.ApiModelProperty;
+
+
+public abstract class VersionedComponent {
+
+    private String identifier;
+    private String groupId;
+    private String name;
+    private String comments;
+    private Position position;
+
+    @ApiModelProperty("The component's unique identifier")
+    public String getIdentifier() {
+        return identifier;
+    }
+
+    public void setIdentifier(String identifier) {
+        this.identifier = identifier;
+    }
+
+    @ApiModelProperty("The ID of the Process Group that this component belongs to")
+    public String getGroupIdentifier() {
+        return groupId;
+    }
+
+    public void setGroupIdentifier(String groupId) {
+        this.groupId = groupId;
+    }
+
+    @ApiModelProperty("The component's name")
+    public String getName() {
+        return name;
+    }
+
+    public void setName(String name) {
+        this.name = name;
+    }
+
+    @ApiModelProperty("The component's position on the graph")
+    public Position getPosition() {
+        return position;
+    }
+
+    public void setPosition(Position position) {
+        this.position = position;
+    }
+
+    @ApiModelProperty("The user-supplied comments for the component")
+    public String getComments() {
+        return comments;
+    }
+
+    public void setComments(String comments) {
+        this.comments = comments;
+    }
+
+    public abstract ComponentType getComponentType();
+
+    public void setComponentType(ComponentType type) {
+        // purposely do nothing here, this just to allow unmarshalling
+    }
+
+    @Override
+    public int hashCode() {
+        return Objects.hash(identifier);
+    }
+
+    @Override
+    public boolean equals(final Object obj) {
+        if (obj == null) {
+            return false;
+        }
+        if (obj == this) {
+            return true;
+        }
+        if (!(obj instanceof VersionedComponent)) {
+            return false;
+        }
+        final VersionedComponent other = (VersionedComponent) obj;
+        return Objects.equals(identifier, other.identifier);
+    }
+}

http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/6f26290d/nifi-registry-core/nifi-registry-data-model/src/main/java/org/apache/nifi/registry/flow/VersionedConfigurableComponent.java
----------------------------------------------------------------------
diff --git a/nifi-registry-core/nifi-registry-data-model/src/main/java/org/apache/nifi/registry/flow/VersionedConfigurableComponent.java b/nifi-registry-core/nifi-registry-data-model/src/main/java/org/apache/nifi/registry/flow/VersionedConfigurableComponent.java
new file mode 100644
index 0000000..3201c5f
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-data-model/src/main/java/org/apache/nifi/registry/flow/VersionedConfigurableComponent.java
@@ -0,0 +1,34 @@
+/*
+ * 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.flow;
+
+import java.util.Map;
+
+/**
+ * A component that has property descriptors and can be configured with values for those properties.
+ */
+public interface VersionedConfigurableComponent {
+
+    Map<String,VersionedPropertyDescriptor> getPropertyDescriptors();
+
+    void setPropertyDescriptors(Map<String,VersionedPropertyDescriptor> propertyDescriptors);
+
+    Map<String,String> getProperties();
+
+    void setProperties(Map<String,String> properties);
+
+}


[41/51] [partial] nifi-registry git commit: NIFIREG-201 Refactoring project structure to better isolate extensions

Posted by kd...@apache.org.
http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/6f26290d/nifi-registry-core/nifi-registry-docs/src/main/asciidoc/user-guide.adoc
----------------------------------------------------------------------
diff --git a/nifi-registry-core/nifi-registry-docs/src/main/asciidoc/user-guide.adoc b/nifi-registry-core/nifi-registry-docs/src/main/asciidoc/user-guide.adoc
new file mode 100644
index 0000000..e746e12
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-docs/src/main/asciidoc/user-guide.adoc
@@ -0,0 +1,365 @@
+//
+// 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.
+//
+= Apache NiFi Registry User Guide
+Apache NiFi Team <de...@nifi.apache.org>
+:homepage: http://nifi.apache.org
+
+
+== Introduction
+Apache NiFi Registry—a subproject of Apache NiFi—is a complementary application that provides a central location for storage and management of shared resources across one or more instances of NiFi and/or MiNiFi.
+
+The first implementation of the Registry supports versioned flows.  Process group level dataflows created in NiFi can be placed under version control and stored in a registry. The registry organizes where flows are stored and manages the permissions to access, create, modify or delete them.
+
+See the link:administration-guide.html[System Administrator’s Guide] for information about Registry system requirements, installation, and configuration. Once NiFi Registry is installed, use a supported web browser to view the UI.
+
+
+== Browser Support
+[options="header"]
+|======================
+|Browser  |Version
+|Chrome   |Current and Current - 1
+|FireFox  |Current and Current - 1
+|Safari   |Current and Current - 1
+|======================
+
+Current and Current - 1 indicates that the UI is supported in the current stable release of that browser and the preceding one. For instance, if the current stable release is 62.X then the officially supported versions will be 62.X and 61.X.
+
+For Safari, which releases major versions much less frequently, Current and Current - 1 simply represent the two latest releases.
+
+The supported browser versions are driven by the capabilities the UI employs and the dependencies it uses. UI features will be developed and tested against the supported browsers. Any problem using a supported browser should be reported to Apache NiFi.
+
+=== Unsupported Browsers
+
+While the UI may run successfully in unsupported browsers, it is not actively tested against them. Additionally, the UI is designed as a desktop experience and is not currently supported in mobile browsers.
+
+=== Viewing the UI in Variably Sized Browsers
+In most environments, all of the UI is visible in your browser. However, the UI has a responsive design that allows you to scroll through screens as needed, in smaller sized browsers or tablet environments.
+
+NOTE: The minimum recommended screen size is 1080px X 445px.
+
+== Terminology
+
+*Flow*: A process group level NiFi dataflow that has been placed under version control and saved to the Registry.
+
+*Bucket*: A container that stores and organizes flows.
+
+*Policy*: Defines a user or group's ability to import, view, commit changes and/or delete flows.
+
+
+[[User_Interface]]
+== NiFi Registry User Interface
+
+The NiFi Registry UI displays the shared resources available and provides mechanisms for creating and administering users/groups, buckets and policies.
+
+When the application is started, the user is able to navigate to the UI by going to the default address of `http://<hostname>:18080/nifi-registry` in a web browser. There are no permissions configured by default, so anyone is able to view and modify the flows and buckets. For information on securing the system, see the link:administration-guide.html[System Administrator’s Guide].
+
+When an administrator navigates to the UI for the first time, the registry is empty as there are no flow resources available to share yet:
+
+image::nifi-registry-components.png["NiFi Registry Components"]
+
+The Buckets menu is available at the top left of the screen.  It allows the user to display flows based on which bucket they are contained in.  On the top right of the screen is the Settings button (image:iconSettings.png["Settings Icon"]) which accesses functionality for managing users, groups, buckets and policies.  Next to the Settings button is the Help button (image:iconHelp.png["Help Icon"]) which accesses the NiFi Registry Documentation.
+
+[[logging-in]]
+== Logging In
+
+If NiFi Registry is configured to run securely, users will have to be granted permissions to buckets by an administrator. For information on configuring NiFi Registry to run securely, see the link:administration-guide.html[System Administrator’s Guide].
+
+If the user is logging in with their username/password they will be presented with a screen to do so.
+
+image::loginRegistry.png["NiFi Registry Login"]
+
+
+== Manage Flows
+
+=== View a Flow
+Flows in all buckets are listed in the main window of the UI by default.  If the registry is secured, only the flows in the buckets that the user has access to are listed.
+
+image::flows_all.png["All Flows"]
+
+To see the flows in a particular bucket, select that bucket from the drop-down menu at the top left of the UI.
+
+image::bucket_menu.png["Bucket Menu"]
+
+Click on a flow to see its Description and Change Log:
+
+image::flow_change_log.png["Flow Change Log"]
+
+The Change Log includes all versions that were saved for a flow.  Clicking on the version reveals details about when the version was saved, which user committed the save, and any comments entered by the user.
+
+==== Sorting & Filtering Flows
+Flows can be sorted alphabetically by Name (ascending or descending) or by Update (newest or oldest) using the drop-down at the top right of the UI.
+
+image::flows_sort_menu.png["Flows Sort Menu"]
+
+The flow list can be filtered by:
+
+* flow name
+* flow description
+* flow ID
+* bucket name
+* bucket ID
+
+Here is an example filtering by flow name:
+
+image::flows_filter_by_name.png["Flows Filter By Name"]
+
+=== Delete a Flow
+To delete a flow from the registry:
+
+1. Click on the flow to see its details.
+2. Select the "Actions" drop-down and click the "Delete" menu option.
++
+image::flow_delete_action.png["Flow Delete Action"]
+3. Select "Delete" to confirm.
++
+image::flow_delete_confirm.png["Flow Delete Confirm"]
+
+WARNING:  It is possible to delete a flow that is actively being used in NiFi.
+
+
+== Manage Buckets
+
+To manage buckets, enter the Administration section of the Registry by clicking the Settings button (image:iconSettings.png["Settings Icon"]) on the top right of the UI.  The Buckets window appears by default.
+
+=== Sorting & Filtering Buckets
+Buckets can be sorted alphabetically by Name (ascending or descending) using the up/down arrows.
+
+image::buckets_sort_by_name.png["Buckets Sort By Name"]
+
+The buckets listed can be filtered by:
+
+* bucket name
+* bucket description
+* bucket ID
+
+Here is an example filtering by bucket name:
+
+image::buckets_filter_by_name.png["Buckets Filter By Name"]
+
+=== Create a Bucket
+1. Select the "New Bucket" button.
++
+image::new_bucket_button.png["New Bucket Button"]
+2. Enter the desired bucket name and select the "Create" button.
++
+image::new_bucket_dialog.png["New Bucket Dialog"]
+
+NOTE: To quickly create multiple buckets, check "Keep this dialog open after creating bucket".
+
+
+=== Delete a Bucket
+1. Select the Delete button (image:iconDelete.png["Delete Icon"]) in the row of the bucket.
++
+image::delete_bucket_single.png["Delete Single Bucket"]
+2. From the Delete Bucket dialog, select "Delete".
++
+image::delete_bucket_dialog.png["Delete Bucket Dialog"]
+
+=== Delete Multiple Buckets
+1. Select the checkboxes in the rows of the desired buckets to delete.
++
+image::check_multiple_buckets.png["Check Multiple Buckets"]
+2. Select the "Actions" drop-down and click the "Delete" option.
++
+image::delete_multiple_buckets.png["Delete Multiple Buckets"]
+3. From the Delete Buckets dialog, select "Delete".
++
+image::delete_buckets_dialog.png["Delete Buckets Dialog"]
+
+=== Edit a Bucket Name
+1. Select the Manage button (image:iconManage.png["Manage Icon"]) in the row of the bucket.
++
+image::manage_bucket.png["Manage Bucket"]
+2. Enter a new name for the bucket and select the "Save" button.
++
+image::bucket_nav_name_edit.png["Edit Bucket Name"]
+
+=== Bucket Policies
+Bucket policies define user privileges on buckets/flows in the Registry and in NiFi.  The available permissions are:
+
+* *All* - In the Registry, the assigned user is able to view and delete flows in the bucket. In NiFi, the selected user is able to import flows from the bucket and commit changes to flows in the bucket.
+
+* *Read* - In the Registry, the assigned user is able to view flows in the bucket. In NiFi, the selected user is able to import flows from the bucket.
+
+* *Write* - In NiFi, the assigned user is able to commit changes to flows in the bucket.
+
+* *Delete* - In the Registry, the assigned user is able to delete flows in the bucket.
+
+NOTE: Users would typically have Read permissions at a minimum.  A user with Write permission would not commit changes to a flow if they were not able to import it initially.  A user with Delete permission would not delete a flow if they could not view it.
+
+NOTE: If a user has a bucket policy and the group that the user is in also has a policy, all policies are used to determine access.  For example, assume User1 is in Group1, User1 has READ privileges on Bucket1 and Group1 has READ privileges on Bucket2. In this scenario, User1 will have READ privileges on both Bucket1 and Bucket2.
+
+==== Create a Bucket Policy
+1. Select the Manage button (image:iconManage.png["Manage Icon"]) in the row of the bucket.
+2. Select the "New Policy" button.
++
+image::new_bucket_policy_create.png["Create New Bucket Policy"]
+3. Select a user, check the desired permissions and select the "Apply" button:
++
+image::new_bucket_policy_user_permission.png["New Bucket Policy User and Permissions"]
+4. The policy is added to the bucket:
++
+image::new_bucket_policy_added.png["New Bucket Policy Added"]
+
+==== Delete a Bucket Policy
+1. Select the Manage button (image:iconManage.png["Manage Icon"]) in the row of the bucket.
+2. Select the Delete button (image:iconDelete.png["Delete Icon"]) in the row of the policy.
++
+image::delete_bucket_policy.png["Delete Policy"]
+3. From the Delete Policy dialog, select "Delete".
++
+image::delete_bucket_policy_dialog.png["Delete Policy Dialog"]
+
+
+== Manage Users & Groups
+
+To manage users/groups, enter the Administration section of the Registry by clicking the Settings button (image:iconSettings.png["Settings Icon"]) on the top right of the UI.  Select Users from the top menu to open the Users window.
+
+=== Sorting & Filtering Users/Groups
+Users/groups can be sorted alphabetically by Name (ascending or descending) using the up/down arrows.
+
+image::users_sort_by_name.png["Users Sort By Name"]
+
+The Users/groups listed can be filtered by:
+
+* user name
+* user ID
+* group name
+* group ID
+
+Here is an example of filtering by user name:
+
+image::users_filter_by_name.png["Users Filter By Name"]
+
+=== Add a User
+1. Select the "Add User" button.
++
+image::add_user_button.png["Add User"]
+2. Enter the desired username or appropriate Identity information. Select the "Add" button.
++
+image::add_user_dialog.png["New User Dialog"]
+
+NOTE: To quickly create multiple users, check "Keep this dialog open after adding user".
+
+=== Delete a User
+1. Select the Delete button (image:iconDelete.png["Delete Icon"]) in the row of the user.
++
+image::delete_user_single.png["Delete Single User"]
+2. From the Delete User dialog, select "Delete".
++
+image::delete_user_dialog.png["Delete User Dialog"]
+
+=== Delete Multiple Users
+1. Select the checkboxes in the rows of the desired users to delete.
++
+image::check_multiple_users.png["Check Multiple Users"]
+2. Select the "Actions" drop-down and click the "Delete" option.
++
+image::delete_multiple_users.png["Delete Multiple Users"]
+3. From the Delete Users dialog, select "Delete".
++
+image::delete_users_groups_dialog.png["Delete Users Dialog"]
+
+
+=== Edit a User Name
+1. Select the Manage button (image:iconManage.png["Manage Icon"]) in the row of the user.
++
+image::manage_user.png["Manage User"]
+2. Enter a new user name and select the "Save" button.
++
+image::user_nav_name_edit.png["Edit User Name"]
+
+WARNING: Some users cannot have their names edited.  For example, those defined by LDAP.  These users will be specially highlighted in the list.
+
+image::users_non_configurable.png["Non-configurable Users"]
+
+=== Special Privileges
+Special privileges are additional permissions that allow a user to manage or access certain aspects of the Registry.  The special privileges are:
+
+* *Can manage buckets* - Allow a user to manage all buckets in the registry, as well as provide the user access to all buckets from a connected system (e.g., NiFi).
+
+* *Can manage users* - Allow a user to manage all registry users and groups.
+
+* *Can manage policies* - Allow a user to grant all registry users read, write, and delete permission to a bucket.
+
+* *Can proxy user requests* - Allow a connected system (e.g., NiFi) to process requests of authorized users of that system.
+
+==== Grant Special Privileges to a User
+1. Select the Manage button (image:iconManage.png["Manage Icon"]) in the row of the user.
++
+image::manage_user.png["Manage User"]
+2. Check the desired privileges:
++
+image::user_special_privileges.png["User Special Privileges"]
+3. Changes made to special privileges are automatically saved.
+
+== Manage Groups
+
+=== Add an Empty Group
+1. With no users checked, select the "Actions" drop-down and click the "Create new group" option.
++
+image::create_new_group.png["Create New Group"]
+2. Enter a name for the Group and select the "Create" button.
++
+image::create_new_group_dialog.png["Create New Group Dialog"]
+
+NOTE: To quickly create multiple empty groups, check "Keep this dialog open after creating group".
+
+
+=== Add User to a Group
+1. Select the Manage button (image:iconManage.png["Manage Icon"]) in the row of the user.
+2. Select the "Add To Group" button.
++
+image::user_nav_add_to_group.png["Add User to Group"]
+3. In the "Add User to Groups" dialog, select the group(s) to add the user to.  Select the "Add" button when all desired groups have been selected.
++
+image::add_user_to_groups_dialog.png["Add User to Groups Dialog"]
+4.  The user is added to the group:
++
+image::group_added.png["Group Added"]
+
+NOTE:  Groups cannot contain other groups.
+
+=== Create a New Group with Selected Users
+1. Select the checkboxes in the rows of the desired users. From the "Actions" drop-down, click the "Create new group" option.
++
+image::select_users_create_new_group.png["Select Users for New Group"]
+2. Enter a name for the Group and select the "Create" button.
++
+image::select_users_create_new_group_dialog.png["Create New Group Dialog"]
+3. The new group is created with the selected users as members:
++
+image::select_users_new_group_added.png["New Group Added with Selected Users"]
+
+=== Remove a User from a Group
+There are two ways to remove a user from a group.
+
+==== User Window
+1. Select the Manage button (image:iconManage.png["Manage Icon"]) in the row of the user.
+2. In the Membership section of the window, select the Remove button (image:iconDelete.png["Delete Icon"]) in the row of the group.
++
+image::remove_group_from_user.png["Remove Group From User"]
+
+==== Group Window
+1. Select the Manage button (image:iconManage.png["Manage Icon"]) in the row of the group. The Members tab is selected by default.
+2. In the Membership section of the window, select the Remove button (image:iconDelete.png["Delete Icon"]) in the row of the user.
++
+image::remove_user_from_group.png["Remove User From Group"]
+
+=== Other Group Level Actions
+
+Editing group names, deleting groups, adding policies to/deleting policies from groups and granting special privileges to groups follow similar procedures described earlier for corresponding user level actions.

http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/6f26290d/nifi-registry-core/nifi-registry-docs/src/main/assembly/dependencies.xml
----------------------------------------------------------------------
diff --git a/nifi-registry-core/nifi-registry-docs/src/main/assembly/dependencies.xml b/nifi-registry-core/nifi-registry-docs/src/main/assembly/dependencies.xml
new file mode 100644
index 0000000..6f6279b
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-docs/src/main/assembly/dependencies.xml
@@ -0,0 +1,44 @@
+<?xml version="1.0"?>
+<!--
+  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.
+-->
+<assembly>
+    <id>resources</id>
+    <formats>
+        <format>zip</format>
+    </formats>
+    <includeBaseDirectory>false</includeBaseDirectory>
+    <fileSets>
+        <fileSet>
+            <directory>${project.build.directory}/generated-docs/</directory>
+            <outputDirectory>/html/</outputDirectory>
+        </fileSet>        
+    </fileSets>
+    <files>
+        <file>
+            <source>./LICENSE</source>
+            <outputDirectory>./</outputDirectory>
+            <destName>LICENSE</destName>
+            <fileMode>0644</fileMode>
+            <filtered>true</filtered>
+        </file>       
+        <file>
+            <source>./NOTICE</source>
+            <outputDirectory>./</outputDirectory>
+            <destName>NOTICE</destName>
+            <fileMode>0644</fileMode>
+            <filtered>true</filtered>
+        </file>
+    </files>
+</assembly>

http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/6f26290d/nifi-registry-core/nifi-registry-flow-diff/pom.xml
----------------------------------------------------------------------
diff --git a/nifi-registry-core/nifi-registry-flow-diff/pom.xml b/nifi-registry-core/nifi-registry-flow-diff/pom.xml
new file mode 100644
index 0000000..d6af7c6
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-flow-diff/pom.xml
@@ -0,0 +1,30 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 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. -->
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+    <modelVersion>4.0.0</modelVersion>
+    <parent>
+        <groupId>org.apache.nifi.registry</groupId>
+        <artifactId>nifi-registry-core</artifactId>
+        <version>0.3.0-SNAPSHOT</version>
+    </parent>
+    
+    <artifactId>nifi-registry-flow-diff</artifactId>
+    <packaging>jar</packaging>
+    
+    <dependencies>
+        <dependency>
+            <groupId>org.apache.nifi.registry</groupId>
+            <artifactId>nifi-registry-data-model</artifactId>
+            <version>0.3.0-SNAPSHOT</version>
+        </dependency>
+    </dependencies>
+</project>

http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/6f26290d/nifi-registry-core/nifi-registry-flow-diff/src/main/java/org/apache/nifi/registry/flow/diff/ComparableDataFlow.java
----------------------------------------------------------------------
diff --git a/nifi-registry-core/nifi-registry-flow-diff/src/main/java/org/apache/nifi/registry/flow/diff/ComparableDataFlow.java b/nifi-registry-core/nifi-registry-flow-diff/src/main/java/org/apache/nifi/registry/flow/diff/ComparableDataFlow.java
new file mode 100644
index 0000000..603b7cf
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-flow-diff/src/main/java/org/apache/nifi/registry/flow/diff/ComparableDataFlow.java
@@ -0,0 +1,26 @@
+/*
+ * 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.flow.diff;
+
+import org.apache.nifi.registry.flow.VersionedProcessGroup;
+
+public interface ComparableDataFlow {
+    String getName();
+
+    VersionedProcessGroup getContents();
+}

http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/6f26290d/nifi-registry-core/nifi-registry-flow-diff/src/main/java/org/apache/nifi/registry/flow/diff/ConciseEvolvingDifferenceDescriptor.java
----------------------------------------------------------------------
diff --git a/nifi-registry-core/nifi-registry-flow-diff/src/main/java/org/apache/nifi/registry/flow/diff/ConciseEvolvingDifferenceDescriptor.java b/nifi-registry-core/nifi-registry-flow-diff/src/main/java/org/apache/nifi/registry/flow/diff/ConciseEvolvingDifferenceDescriptor.java
new file mode 100644
index 0000000..76fdfc5
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-flow-diff/src/main/java/org/apache/nifi/registry/flow/diff/ConciseEvolvingDifferenceDescriptor.java
@@ -0,0 +1,85 @@
+/*
+ * 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.flow.diff;
+
+import java.util.Objects;
+
+import org.apache.nifi.registry.flow.VersionedComponent;
+import org.apache.nifi.registry.flow.VersionedFlowCoordinates;
+
+/**
+ * Describes differences between flows as if Flow A is an 'earlier version' of the same flow than Flow B.
+ * This provides verbiage such as "Processor with ID 123 was added to flow."
+ */
+public class ConciseEvolvingDifferenceDescriptor implements DifferenceDescriptor {
+
+    @Override
+    public String describeDifference(final DifferenceType type, final String flowAName, final String flowBName, final VersionedComponent componentA,
+        final VersionedComponent componentB, final String fieldName, final Object valueA, final Object valueB) {
+
+        final String description;
+        switch (type) {
+            case COMPONENT_ADDED:
+                description = String.format("%s was added", componentB.getComponentType().getTypeName());
+                break;
+            case COMPONENT_REMOVED:
+                description = String.format("%s was removed", componentA.getComponentType().getTypeName());
+                break;
+            case PROPERTY_ADDED:
+                description = String.format("Property '%s' was added", fieldName);
+                break;
+            case PROPERTY_REMOVED:
+                description = String.format("Property '%s' was removed", fieldName);
+                break;
+            case VARIABLE_ADDED:
+                description = String.format("Variable '%s' was added", fieldName);
+                break;
+            case VARIABLE_REMOVED:
+                description = String.format("Variable '%s' was removed", fieldName);
+                break;
+            case POSITION_CHANGED:
+                description = "Position was changed";
+                break;
+            case BENDPOINTS_CHANGED:
+                description = "Connection Bendpoints changed";
+                break;
+            case VERSIONED_FLOW_COORDINATES_CHANGED:
+                if (valueA instanceof VersionedFlowCoordinates && valueB instanceof VersionedFlowCoordinates) {
+                    final VersionedFlowCoordinates coordinatesA = (VersionedFlowCoordinates) valueA;
+                    final VersionedFlowCoordinates coordinatesB = (VersionedFlowCoordinates) valueB;
+
+                    // If the two vary only by version, then use a more concise message. If anything else is different, then use a fully explanation.
+                    if (Objects.equals(coordinatesA.getRegistryUrl(), coordinatesB.getRegistryUrl()) && Objects.equals(coordinatesA.getBucketId(), coordinatesB.getBucketId())
+                            && Objects.equals(coordinatesA.getFlowId(), coordinatesB.getFlowId()) && coordinatesA.getVersion() != coordinatesB.getVersion()) {
+
+                        description = String.format("Flow Version changed from %s to %s", coordinatesA.getVersion(), coordinatesB.getVersion());
+                        break;
+                    }
+                }
+
+                description = String.format("From '%s' to '%s'", valueA, valueB);
+                break;
+            default:
+                description = String.format("From '%s' to '%s'", valueA, valueB);
+                break;
+        }
+
+        return description;
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/6f26290d/nifi-registry-core/nifi-registry-flow-diff/src/main/java/org/apache/nifi/registry/flow/diff/DifferenceDescriptor.java
----------------------------------------------------------------------
diff --git a/nifi-registry-core/nifi-registry-flow-diff/src/main/java/org/apache/nifi/registry/flow/diff/DifferenceDescriptor.java b/nifi-registry-core/nifi-registry-flow-diff/src/main/java/org/apache/nifi/registry/flow/diff/DifferenceDescriptor.java
new file mode 100644
index 0000000..56e65ef
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-flow-diff/src/main/java/org/apache/nifi/registry/flow/diff/DifferenceDescriptor.java
@@ -0,0 +1,36 @@
+/*
+ * 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.flow.diff;
+
+import org.apache.nifi.registry.flow.VersionedComponent;
+
+public interface DifferenceDescriptor {
+    /**
+     * Describes a difference between two flows
+     *
+     * @param type the difference
+     * @param componentA the component in "Flow A"
+     * @param componentB the component in "Flow B"
+     * @param fieldName the name of the field that changed, or <code>null</code> if the field name does not apply for the difference type
+     * @param valueA the value being compared from "Flow A"
+     * @param valueB the value being compared from "Flow B"
+     * @return a human-readable description of how the flows differ
+     */
+    String describeDifference(DifferenceType type, String flowAName, String flowBName, VersionedComponent componentA, VersionedComponent componentB, String fieldName,
+        Object valueA, Object valueB);
+}

http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/6f26290d/nifi-registry-core/nifi-registry-flow-diff/src/main/java/org/apache/nifi/registry/flow/diff/DifferenceType.java
----------------------------------------------------------------------
diff --git a/nifi-registry-core/nifi-registry-flow-diff/src/main/java/org/apache/nifi/registry/flow/diff/DifferenceType.java b/nifi-registry-core/nifi-registry-flow-diff/src/main/java/org/apache/nifi/registry/flow/diff/DifferenceType.java
new file mode 100644
index 0000000..047f557
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-flow-diff/src/main/java/org/apache/nifi/registry/flow/diff/DifferenceType.java
@@ -0,0 +1,255 @@
+/*
+ * 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.flow.diff;
+
+public enum DifferenceType {
+    /**
+     * The component does not exist in Flow A but exists in Flow B
+     */
+    COMPONENT_ADDED("Component Added"),
+
+    /**
+     * The component exists in Flow A but does not exist in Flow B
+     */
+    COMPONENT_REMOVED("Component Removed"),
+
+    /**
+     * The component has a different name in each of the flows
+     */
+    NAME_CHANGED("Component Name Changed"),
+
+    /**
+     * The component has a different type in each of the flows
+     */
+    TYPE_CHANGED("Component Type Changed"),
+
+    /**
+     * The component has a different bundle in each of the flows
+     */
+    BUNDLE_CHANGED("Component Bundle Changed"),
+
+    /**
+     * The component has a different penalty duration in each of the flows
+     */
+    PENALTY_DURATION_CHANGED("Penalty Duration Changed"),
+
+    /**
+     * The component has a different yield duration in each of the flows
+     */
+    YIELD_DURATION_CHANGED("Yield Duration Changed"),
+
+    /**
+     * The component has a different bulletin level in each of the flows
+     */
+    BULLETIN_LEVEL_CHANGED("Bulletin Level Changed"),
+
+    /**
+     * The component has a different set of Auto-Terminated Relationships in each of the flows
+     */
+    AUTO_TERMINATED_RELATIONSHIPS_CHANGED("Auto-Terminated Relationships Changed"),
+
+    /**
+     * The component has a different scheduling strategy in each of the flows
+     */
+    SCHEDULING_STRATEGY_CHANGED("Scheduling Strategy Changed"),
+
+    /**
+     * The component has a different maximum number of concurrent tasks in each of the flows
+     */
+    CONCURRENT_TASKS_CHANGED("Concurrent Tasks Changed"),
+
+    /**
+     * The component has a different run schedule in each of the flows
+     */
+    RUN_SCHEDULE_CHANGED("Run Schedule Changed"),
+
+    /**
+     * The component has a different execution mode in each of the flows
+     */
+    EXECUTION_MODE_CHANGED("Execution Mode Changed"),
+
+    /**
+     * The component has a different run duration in each of the flows
+     */
+    RUN_DURATION_CHANGED("Run Duration Changed"),
+
+    /**
+     * The component has a different value in each of the flows for a specific property
+     */
+    PROPERTY_CHANGED("Property Value Changed"),
+
+    /**
+     * Property does not exist in Flow A but does exist in Flow B
+     */
+    PROPERTY_ADDED("Property Added"),
+
+    /**
+     * Property exists in Flow A but does not exist in Flow B
+     */
+    PROPERTY_REMOVED("Property Removed"),
+
+    /**
+     * The component has a different value for the Annotation Data in each of the flows
+     */
+    ANNOTATION_DATA_CHANGED("Annotation Data (Advanced UI Configuration) Changed"),
+
+    /**
+     * The component has a different comment in each of the flows
+     */
+    COMMENTS_CHANGED("Comments Changed"),
+
+    /**
+     * The position of the component on the graph is different in each of the flows
+     */
+    POSITION_CHANGED("Position Changed"),
+
+    /**
+     * The stylistic configuration of the component is different in each of the flows
+     */
+    STYLE_CHANGED("Style Changed"),
+
+    /**
+     * The Relationships included in a connection is different in each of the flows
+     */
+    SELECTED_RELATIONSHIPS_CHANGED("Selected Relationships Changed"),
+
+    /**
+     * The Connection has a different set of Prioritizers in each of the flows
+     */
+    PRIORITIZERS_CHANGED("Prioritizers Changed"),
+
+    /**
+     * The Connection has a different value for the FlowFile Expiration in each of the flows
+     */
+    FLOWFILE_EXPIRATION_CHANGED("FlowFile Expiration Changed"),
+
+    /**
+     * The Connection has a different value for the Object Backpressure Threshold in each of the flows
+     */
+    BACKPRESSURE_OBJECT_THRESHOLD_CHANGED("Backpressure Object Threshold Changed"),
+
+    /**
+     * The Connection has a different value for the Data Size Backpressure Threshold in each of the flows
+     */
+    BACKPRESSURE_DATA_SIZE_THRESHOLD_CHANGED("Backpressure Data Size Threshold Changed"),
+
+    /**
+     * The Connection has a different value for the Load Balance Strategy in each of the flows
+     */
+    LOAD_BALANCE_STRATEGY_CHANGED("Load-Balance Strategy Changed"),
+
+    /**
+     * The Connection has a different value for the Partitioning Attribute in each of the flows
+     */
+    PARTITIONING_ATTRIBUTE_CHANGED("Partitioning Attribute Changed"),
+
+    /**
+     * The Connection has a different value for the Load Balancing Compression in each of the flows
+     */
+    LOAD_BALANCE_COMPRESSION_CHANGED("Load-Balance Compression Changed"),
+
+    /**
+     * The Connection has a different set of Bend Points in each of the flows
+     */
+    BENDPOINTS_CHANGED("Connection Bend Points Changed"),
+
+    /**
+     * The Connection has a difference Source in each of the flows
+     */
+    SOURCE_CHANGED("Connection Source Changed"),
+
+    /**
+     * The Connection has a difference Destination in each of the flows
+     */
+    DESTINATION_CHANGED("Connection Destination Changed"),
+
+    /**
+     * The value in the Label is different in each of the flows
+     */
+    LABEL_VALUE_CHANGED("Label Text Changed"),
+
+    /**
+     * The variable does not exist in Flow A but exists in Flow B
+     */
+    VARIABLE_ADDED("Variable Added to Process Group"),
+
+    /**
+     * The variable does not exist in Flow B but exists in Flow A
+     */
+    VARIABLE_REMOVED("Variable Removed from Process Group"),
+
+    /**
+     * The API of the Controller Service is different in each of the flows
+     */
+    SERVICE_API_CHANGED("Controller Service API Changed"),
+
+    /**
+     * The Remote Process Group has a different Transport Protocol in each of the flows
+     */
+    RPG_TRANSPORT_PROTOCOL_CHANGED("Remote Process Group Transport Protocol Changed"),
+
+    /**
+     * The Remote Process Group has a different Proxy Host in each of the flows
+     */
+    RPG_PROXY_HOST_CHANGED("Remote Process Group Proxy Host Changed"),
+
+    /**
+     * The Remote Process Group has a different Proxy Port in each of the flows
+     */
+    RPG_PROXY_PORT_CHANGED("Remote Process Group Proxy Port Changed"),
+
+    /**
+     * The Remote Process Group has a different Proxy User in each of the flows
+     */
+    RPG_PROXY_USER_CHANGED("Remote Process Group Proxy User Changed"),
+
+    /**
+     * The Remote Process Group has a different Network Interface chosen in each of the flows
+     */
+    RPG_NETWORK_INTERFACE_CHANGED("Remote Process Group Network Interface Changed"),
+
+    /**
+     * The Remote Process Group has a different Communications Timeout in each of the flows
+     */
+    RPG_COMMS_TIMEOUT_CHANGED("Remote Process Group Communications Timeout Changed"),
+
+    /**
+     * The Remote Input Port or Remote Output Port has a different Batch Size in each of the flows
+     */
+    REMOTE_PORT_BATCH_SIZE_CHANGED("Remote Process Group Port's Batch Size Changed"),
+
+    /**
+     * The Remote Input Port or Remote Output Port has a different value for the Compression flag in each of the flows
+     */
+    REMOTE_PORT_COMPRESSION_CHANGED("Remote Process Group Port's Compression Flag Changed"),
+
+    /**
+     * The Process Group points to a different Versioned Flow in each of the flows
+     */
+    VERSIONED_FLOW_COORDINATES_CHANGED("Versioned Flow Coordinates Changed");
+
+    private final String description;
+
+    private DifferenceType(final String description) {
+        this.description = description;
+    }
+
+    public String getDescription() {
+        return description;
+    }
+}

http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/6f26290d/nifi-registry-core/nifi-registry-flow-diff/src/main/java/org/apache/nifi/registry/flow/diff/EvolvingDifferenceDescriptor.java
----------------------------------------------------------------------
diff --git a/nifi-registry-core/nifi-registry-flow-diff/src/main/java/org/apache/nifi/registry/flow/diff/EvolvingDifferenceDescriptor.java b/nifi-registry-core/nifi-registry-flow-diff/src/main/java/org/apache/nifi/registry/flow/diff/EvolvingDifferenceDescriptor.java
new file mode 100644
index 0000000..c3300e6
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-flow-diff/src/main/java/org/apache/nifi/registry/flow/diff/EvolvingDifferenceDescriptor.java
@@ -0,0 +1,61 @@
+/*
+ * 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.flow.diff;
+
+import org.apache.nifi.registry.flow.VersionedComponent;
+
+/**
+ * Describes differences between flows as if Flow A is an 'earlier version' of the same flow than Flow B.
+ * This provides verbiage such as "Processor with ID 123 was added to flow."
+ */
+public class EvolvingDifferenceDescriptor implements DifferenceDescriptor {
+
+    @Override
+    public String describeDifference(final DifferenceType type, final String flowAName, final String flowBName, final VersionedComponent componentA,
+        final VersionedComponent componentB, final String fieldName, final Object valueA, final Object valueB) {
+
+        final String description;
+        switch (type) {
+            case COMPONENT_ADDED:
+                description = String.format("%s with ID %s was added to flow", componentB.getComponentType().getTypeName(), componentB.getIdentifier());
+                break;
+            case COMPONENT_REMOVED:
+                description = String.format("%s with ID %s was removed from flow", componentA.getComponentType().getTypeName(), componentA.getIdentifier());
+                break;
+            case PROPERTY_ADDED:
+                description = String.format("Property '%s' was added to %s with ID %s", fieldName, componentB.getComponentType().getTypeName(), componentB.getIdentifier());
+                break;
+            case PROPERTY_REMOVED:
+                description = String.format("Property '%s' was removed from %s with ID %s", fieldName, componentA.getComponentType().getTypeName(), componentA.getIdentifier());
+                break;
+            case VARIABLE_ADDED:
+                description = String.format("Variable '%s' was added to Process Group with ID %s", fieldName, componentB.getIdentifier());
+                break;
+            case VARIABLE_REMOVED:
+                description = String.format("Variable '%s' was removed from Process Group with ID %s", fieldName, componentA.getIdentifier());
+                break;
+            default:
+                description = String.format("%s for %s with ID %s from '%s' to '%s'",
+                    type.getDescription(), componentA.getComponentType().getTypeName(), componentA.getIdentifier(), valueA, valueB);
+                break;
+        }
+
+        return description;
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/6f26290d/nifi-registry-core/nifi-registry-flow-diff/src/main/java/org/apache/nifi/registry/flow/diff/FlowComparator.java
----------------------------------------------------------------------
diff --git a/nifi-registry-core/nifi-registry-flow-diff/src/main/java/org/apache/nifi/registry/flow/diff/FlowComparator.java b/nifi-registry-core/nifi-registry-flow-diff/src/main/java/org/apache/nifi/registry/flow/diff/FlowComparator.java
new file mode 100644
index 0000000..3835fec
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-flow-diff/src/main/java/org/apache/nifi/registry/flow/diff/FlowComparator.java
@@ -0,0 +1,35 @@
+/*
+ * 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.flow.diff;
+
+import java.util.Set;
+
+import org.apache.nifi.registry.flow.VersionedControllerService;
+
+public interface FlowComparator {
+    FlowComparison compare();
+
+    /**
+     * Compares to versions of a Controller Service and returns the differences between them
+     *
+     * @param serviceA the first Controller Service
+     * @param serviceB the second Controller Service
+     * @return the differences between them
+     */
+    Set<FlowDifference> compareControllerServices(VersionedControllerService serviceA, VersionedControllerService serviceB);
+}

http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/6f26290d/nifi-registry-core/nifi-registry-flow-diff/src/main/java/org/apache/nifi/registry/flow/diff/FlowComparison.java
----------------------------------------------------------------------
diff --git a/nifi-registry-core/nifi-registry-flow-diff/src/main/java/org/apache/nifi/registry/flow/diff/FlowComparison.java b/nifi-registry-core/nifi-registry-flow-diff/src/main/java/org/apache/nifi/registry/flow/diff/FlowComparison.java
new file mode 100644
index 0000000..52b3f26
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-flow-diff/src/main/java/org/apache/nifi/registry/flow/diff/FlowComparison.java
@@ -0,0 +1,28 @@
+/*
+ * 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.flow.diff;
+
+import java.util.Set;
+
+public interface FlowComparison {
+    ComparableDataFlow getFlowA();
+
+    ComparableDataFlow getFlowB();
+
+    Set<FlowDifference> getDifferences();
+}

http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/6f26290d/nifi-registry-core/nifi-registry-flow-diff/src/main/java/org/apache/nifi/registry/flow/diff/FlowDifference.java
----------------------------------------------------------------------
diff --git a/nifi-registry-core/nifi-registry-flow-diff/src/main/java/org/apache/nifi/registry/flow/diff/FlowDifference.java b/nifi-registry-core/nifi-registry-flow-diff/src/main/java/org/apache/nifi/registry/flow/diff/FlowDifference.java
new file mode 100644
index 0000000..249bf42
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-flow-diff/src/main/java/org/apache/nifi/registry/flow/diff/FlowDifference.java
@@ -0,0 +1,38 @@
+/*
+ * 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.flow.diff;
+
+import java.util.Optional;
+
+import org.apache.nifi.registry.flow.VersionedComponent;
+
+public interface FlowDifference {
+    DifferenceType getDifferenceType();
+
+    VersionedComponent getComponentA();
+
+    VersionedComponent getComponentB();
+
+    Optional<String> getFieldName();
+
+    Object getValueA();
+
+    Object getValueB();
+
+    String getDescription();
+}

http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/6f26290d/nifi-registry-core/nifi-registry-flow-diff/src/main/java/org/apache/nifi/registry/flow/diff/StandardComparableDataFlow.java
----------------------------------------------------------------------
diff --git a/nifi-registry-core/nifi-registry-flow-diff/src/main/java/org/apache/nifi/registry/flow/diff/StandardComparableDataFlow.java b/nifi-registry-core/nifi-registry-flow-diff/src/main/java/org/apache/nifi/registry/flow/diff/StandardComparableDataFlow.java
new file mode 100644
index 0000000..649dbc3
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-flow-diff/src/main/java/org/apache/nifi/registry/flow/diff/StandardComparableDataFlow.java
@@ -0,0 +1,41 @@
+/*
+ * 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.flow.diff;
+
+import org.apache.nifi.registry.flow.VersionedProcessGroup;
+
+public class StandardComparableDataFlow implements ComparableDataFlow {
+    private final String name;
+    private final VersionedProcessGroup contents;
+
+    public StandardComparableDataFlow(final String name, final VersionedProcessGroup contents) {
+        this.name = name;
+        this.contents = contents;
+    }
+
+    @Override
+    public String getName() {
+        return name;
+    }
+
+    @Override
+    public VersionedProcessGroup getContents() {
+        return contents;
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/6f26290d/nifi-registry-core/nifi-registry-flow-diff/src/main/java/org/apache/nifi/registry/flow/diff/StandardFlowComparator.java
----------------------------------------------------------------------
diff --git a/nifi-registry-core/nifi-registry-flow-diff/src/main/java/org/apache/nifi/registry/flow/diff/StandardFlowComparator.java b/nifi-registry-core/nifi-registry-flow-diff/src/main/java/org/apache/nifi/registry/flow/diff/StandardFlowComparator.java
new file mode 100644
index 0000000..f98225e
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-flow-diff/src/main/java/org/apache/nifi/registry/flow/diff/StandardFlowComparator.java
@@ -0,0 +1,404 @@
+/*
+ * 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.flow.diff;
+
+import java.util.Collection;
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.Map;
+import java.util.Objects;
+import java.util.Set;
+import java.util.function.Function;
+import java.util.stream.Collectors;
+
+import org.apache.nifi.registry.flow.VersionedComponent;
+import org.apache.nifi.registry.flow.VersionedConnection;
+import org.apache.nifi.registry.flow.VersionedControllerService;
+import org.apache.nifi.registry.flow.VersionedFunnel;
+import org.apache.nifi.registry.flow.VersionedLabel;
+import org.apache.nifi.registry.flow.VersionedPort;
+import org.apache.nifi.registry.flow.VersionedProcessGroup;
+import org.apache.nifi.registry.flow.VersionedProcessor;
+import org.apache.nifi.registry.flow.VersionedPropertyDescriptor;
+import org.apache.nifi.registry.flow.VersionedRemoteGroupPort;
+import org.apache.nifi.registry.flow.VersionedRemoteProcessGroup;
+
+public class StandardFlowComparator implements FlowComparator {
+    private static final String DEFAULT_LOAD_BALANCE_STRATEGY = "DO_NOT_LOAD_BALANCE";
+    private static final String DEFAULT_PARTITIONING_ATTRIBUTE = "";
+    private static final String DEFAULT_LOAD_BALANCE_COMPRESSION = "DO_NOT_COMPRESS";
+
+    private final ComparableDataFlow flowA;
+    private final ComparableDataFlow flowB;
+    private final Set<String> externallyAccessibleServiceIds;
+    private final DifferenceDescriptor differenceDescriptor;
+
+    public StandardFlowComparator(final ComparableDataFlow flowA, final ComparableDataFlow flowB,
+        final Set<String> externallyAccessibleServiceIds, final DifferenceDescriptor differenceDescriptor) {
+        this.flowA = flowA;
+        this.flowB = flowB;
+        this.externallyAccessibleServiceIds = externallyAccessibleServiceIds;
+        this.differenceDescriptor = differenceDescriptor;
+    }
+
+    @Override
+    public FlowComparison compare() {
+        final VersionedProcessGroup groupA = flowA.getContents();
+        final VersionedProcessGroup groupB = flowB.getContents();
+        final Set<FlowDifference> differences = compare(groupA, groupB);
+
+        return new StandardFlowComparison(flowA, flowB, differences);
+    }
+
+    private Set<FlowDifference> compare(final VersionedProcessGroup groupA, final VersionedProcessGroup groupB) {
+        final Set<FlowDifference> differences = new HashSet<>();
+        // Note that we do not compare the names, because when we import a Flow into NiFi, we may well give it a new name.
+        // Child Process Groups' names will still compare but the main group that is under Version Control will not
+        compare(groupA, groupB, differences, false);
+        return differences;
+    }
+
+
+    private <T extends VersionedComponent> Set<FlowDifference> compareComponents(final Set<T> componentsA, final Set<T> componentsB, final ComponentComparator<T> comparator) {
+        final Map<String, T> componentMapA = byId(componentsA == null ? Collections.emptySet() : componentsA);
+        final Map<String, T> componentMapB = byId(componentsB == null ? Collections.emptySet() : componentsB);
+
+        final Set<FlowDifference> differences = new HashSet<>();
+
+        componentMapA.forEach((key, componentA) -> {
+            final T componentB = componentMapB.get(key);
+            comparator.compare(componentA, componentB, differences);
+        });
+
+        componentMapB.forEach((key, componentB) -> {
+            final T componentA = componentMapA.get(key);
+
+            // if component A is not null, it has already been compared above. If component A
+            // is null, then it is missing from Flow A but present in Flow B, so we will just call
+            // compare(), which will handle this for us.
+            if (componentA == null) {
+                comparator.compare(componentA, componentB, differences);
+            }
+        });
+
+        return differences;
+    }
+
+
+    private boolean compareComponents(final VersionedComponent componentA, final VersionedComponent componentB, final Set<FlowDifference> differences) {
+        return compareComponents(componentA, componentB, differences, true, true, true);
+    }
+
+    private boolean compareComponents(final VersionedComponent componentA, final VersionedComponent componentB, final Set<FlowDifference> differences,
+        final boolean compareName, final boolean comparePos, final boolean compareComments) {
+        if (componentA == null) {
+            differences.add(difference(DifferenceType.COMPONENT_ADDED, componentA, componentB, componentA, componentB));
+            return true;
+        }
+
+        if (componentB == null) {
+            differences.add(difference(DifferenceType.COMPONENT_REMOVED, componentA, componentB, componentA, componentB));
+            return true;
+        }
+
+        if (compareComments) {
+            addIfDifferent(differences, DifferenceType.COMMENTS_CHANGED, componentA, componentB, VersionedComponent::getComments, false);
+        }
+
+        if (compareName) {
+            addIfDifferent(differences, DifferenceType.NAME_CHANGED, componentA, componentB, VersionedComponent::getName);
+        }
+
+        if (comparePos) {
+            addIfDifferent(differences, DifferenceType.POSITION_CHANGED, componentA, componentB, VersionedComponent::getPosition);
+        }
+
+        return false;
+    }
+
+    private void compare(final VersionedProcessor processorA, final VersionedProcessor processorB, final Set<FlowDifference> differences) {
+        if (compareComponents(processorA, processorB, differences)) {
+            return;
+        }
+
+        addIfDifferent(differences, DifferenceType.ANNOTATION_DATA_CHANGED, processorA, processorB, VersionedProcessor::getAnnotationData);
+        addIfDifferent(differences, DifferenceType.AUTO_TERMINATED_RELATIONSHIPS_CHANGED, processorA, processorB, VersionedProcessor::getAutoTerminatedRelationships);
+        addIfDifferent(differences, DifferenceType.BULLETIN_LEVEL_CHANGED, processorA, processorB, VersionedProcessor::getBulletinLevel);
+        addIfDifferent(differences, DifferenceType.BUNDLE_CHANGED, processorA, processorB, VersionedProcessor::getBundle);
+        addIfDifferent(differences, DifferenceType.CONCURRENT_TASKS_CHANGED, processorA, processorB, VersionedProcessor::getConcurrentlySchedulableTaskCount);
+        addIfDifferent(differences, DifferenceType.EXECUTION_MODE_CHANGED, processorA, processorB, VersionedProcessor::getExecutionNode);
+        addIfDifferent(differences, DifferenceType.PENALTY_DURATION_CHANGED, processorA, processorB, VersionedProcessor::getPenaltyDuration);
+        addIfDifferent(differences, DifferenceType.RUN_DURATION_CHANGED, processorA, processorB, VersionedProcessor::getRunDurationMillis);
+        addIfDifferent(differences, DifferenceType.RUN_SCHEDULE_CHANGED, processorA, processorB, VersionedProcessor::getSchedulingPeriod);
+        addIfDifferent(differences, DifferenceType.SCHEDULING_STRATEGY_CHANGED, processorA, processorB, VersionedProcessor::getSchedulingStrategy);
+        addIfDifferent(differences, DifferenceType.STYLE_CHANGED, processorA, processorB, VersionedProcessor::getStyle);
+        addIfDifferent(differences, DifferenceType.YIELD_DURATION_CHANGED, processorA, processorB, VersionedProcessor::getYieldDuration);
+        compareProperties(processorA, processorB, processorA.getProperties(), processorB.getProperties(), processorA.getPropertyDescriptors(), processorB.getPropertyDescriptors(), differences);
+    }
+
+    @Override
+    public Set<FlowDifference> compareControllerServices(final VersionedControllerService serviceA, final VersionedControllerService serviceB) {
+        final Set<FlowDifference> differences = new HashSet<>();
+        compare(serviceA, serviceB, differences);
+        return differences;
+    }
+
+    private void compare(final VersionedControllerService serviceA, final VersionedControllerService serviceB, final Set<FlowDifference> differences) {
+        if (compareComponents(serviceA, serviceB, differences)) {
+            return;
+        }
+
+        addIfDifferent(differences, DifferenceType.ANNOTATION_DATA_CHANGED, serviceA, serviceB, VersionedControllerService::getAnnotationData);
+        addIfDifferent(differences, DifferenceType.BUNDLE_CHANGED, serviceA, serviceB, VersionedControllerService::getBundle);
+        compareProperties(serviceA, serviceB, serviceA.getProperties(), serviceB.getProperties(), serviceA.getPropertyDescriptors(), serviceB.getPropertyDescriptors(), differences);
+    }
+
+
+    private void compareProperties(final VersionedComponent componentA, final VersionedComponent componentB,
+        final Map<String, String> propertiesA, final Map<String, String> propertiesB,
+        final Map<String, VersionedPropertyDescriptor> descriptorsA, final Map<String, VersionedPropertyDescriptor> descriptorsB,
+        final Set<FlowDifference> differences) {
+
+        propertiesA.forEach((key, valueA) -> {
+            final String valueB = propertiesB.get(key);
+
+            VersionedPropertyDescriptor descriptor = descriptorsA.get(key);
+            if (descriptor == null) {
+                descriptor = descriptorsB.get(key);
+            }
+
+            final String displayName;
+            if (descriptor == null) {
+                displayName = key;
+            } else {
+                displayName = descriptor.getDisplayName() == null ? descriptor.getName() : descriptor.getDisplayName();
+            }
+
+            if (valueA == null && valueB != null) {
+                differences.add(difference(DifferenceType.PROPERTY_ADDED, componentA, componentB, displayName, valueA, valueB));
+            } else if (valueA != null && valueB == null) {
+                differences.add(difference(DifferenceType.PROPERTY_REMOVED, componentA, componentB, displayName, valueA, valueB));
+            } else if (valueA != null && !valueA.equals(valueB)) {
+                // If the property in Flow A references a Controller Service that is not available in the flow
+                // and the property in Flow B references a Controller Service that is available in its environment
+                // but not part of the Versioned Flow, then we do not want to consider this to be a Flow Difference.
+                // This is typically the case when a flow is versioned in one instance, referencing an external Controller Service,
+                // and then imported into another NiFi instance. When imported, the property does not point to any existing Controller
+                // Service, and the user must then point the property an existing Controller Service. We don't want to consider the
+                // flow as having changed, since it is an environment-specific change (similar to how we handle variables).
+                if (descriptor != null && descriptor.getIdentifiesControllerService()) {
+                    final boolean accessibleA = externallyAccessibleServiceIds.contains(valueA);
+                    final boolean accessibleB = externallyAccessibleServiceIds.contains(valueB);
+                    if (!accessibleA && accessibleB) {
+                        return;
+                    }
+                }
+
+                differences.add(difference(DifferenceType.PROPERTY_CHANGED, componentA, componentB, displayName, valueA, valueB));
+            }
+        });
+
+        propertiesB.forEach((key, valueB) -> {
+            final String valueA = propertiesA.get(key);
+
+            // If there are any properties for component B that do not exist for Component A, add those as differences as well.
+            if (valueA == null && valueB != null) {
+                final VersionedPropertyDescriptor descriptor = descriptorsB.get(key);
+
+                final String displayName;
+                if (descriptor == null) {
+                    displayName = key;
+                } else {
+                    displayName = descriptor.getDisplayName() == null ? descriptor.getName() : descriptor.getDisplayName();
+                }
+
+                differences.add(difference(DifferenceType.PROPERTY_ADDED, componentA, componentB, displayName, null, valueB));
+            }
+        });
+    }
+
+
+    private void compare(final VersionedFunnel funnelA, final VersionedFunnel funnelB, final Set<FlowDifference> differences) {
+        compareComponents(funnelA, funnelB, differences);
+    }
+
+    private void compare(final VersionedLabel labelA, final VersionedLabel labelB, final Set<FlowDifference> differences) {
+        if (compareComponents(labelA, labelB, differences)) {
+            return;
+        }
+
+        addIfDifferent(differences, DifferenceType.LABEL_VALUE_CHANGED, labelA, labelB, VersionedLabel::getLabel);
+        addIfDifferent(differences, DifferenceType.POSITION_CHANGED, labelA, labelB, VersionedLabel::getHeight);
+        addIfDifferent(differences, DifferenceType.POSITION_CHANGED, labelA, labelB, VersionedLabel::getWidth);
+        addIfDifferent(differences, DifferenceType.STYLE_CHANGED, labelA, labelB, VersionedLabel::getStyle);
+    }
+
+    private void compare(final VersionedPort portA, final VersionedPort portB, final Set<FlowDifference> differences) {
+        compareComponents(portA, portB, differences);
+    }
+
+    private void compare(final VersionedRemoteProcessGroup rpgA, final VersionedRemoteProcessGroup rpgB, final Set<FlowDifference> differences) {
+        if (compareComponents(rpgA, rpgB, differences, false, true, false)) { // do not compare comments for RPG because they come from remote system, not our local flow
+            return;
+        }
+
+        addIfDifferent(differences, DifferenceType.RPG_COMMS_TIMEOUT_CHANGED, rpgA, rpgB, VersionedRemoteProcessGroup::getCommunicationsTimeout);
+        addIfDifferent(differences, DifferenceType.RPG_NETWORK_INTERFACE_CHANGED, rpgA, rpgB, VersionedRemoteProcessGroup::getLocalNetworkInterface);
+        addIfDifferent(differences, DifferenceType.RPG_PROXY_HOST_CHANGED, rpgA, rpgB, VersionedRemoteProcessGroup::getProxyHost);
+        addIfDifferent(differences, DifferenceType.RPG_PROXY_PORT_CHANGED, rpgA, rpgB, VersionedRemoteProcessGroup::getProxyPort);
+        addIfDifferent(differences, DifferenceType.RPG_PROXY_USER_CHANGED, rpgA, rpgB, VersionedRemoteProcessGroup::getProxyUser);
+        addIfDifferent(differences, DifferenceType.RPG_TRANSPORT_PROTOCOL_CHANGED, rpgA, rpgB, VersionedRemoteProcessGroup::getTransportProtocol);
+        addIfDifferent(differences, DifferenceType.YIELD_DURATION_CHANGED, rpgA, rpgB, VersionedRemoteProcessGroup::getYieldDuration);
+
+        differences.addAll(compareComponents(rpgA.getInputPorts(), rpgB.getInputPorts(), this::compare));
+        differences.addAll(compareComponents(rpgA.getOutputPorts(), rpgB.getOutputPorts(), this::compare));
+    }
+
+    private void compare(final VersionedRemoteGroupPort portA, final VersionedRemoteGroupPort portB, final Set<FlowDifference> differences) {
+        if (compareComponents(portA, portB, differences)) {
+            return;
+        }
+
+        addIfDifferent(differences, DifferenceType.REMOTE_PORT_BATCH_SIZE_CHANGED, portA, portB, VersionedRemoteGroupPort::getBatchSize);
+        addIfDifferent(differences, DifferenceType.REMOTE_PORT_COMPRESSION_CHANGED, portA, portB, VersionedRemoteGroupPort::isUseCompression);
+        addIfDifferent(differences, DifferenceType.CONCURRENT_TASKS_CHANGED, portA, portB, VersionedRemoteGroupPort::getConcurrentlySchedulableTaskCount);
+    }
+
+
+    private void compare(final VersionedProcessGroup groupA, final VersionedProcessGroup groupB, final Set<FlowDifference> differences, final boolean compareNamePos) {
+        if (compareComponents(groupA, groupB, differences, compareNamePos, compareNamePos, true)) {
+            return;
+        }
+
+        if (groupA == null) {
+            differences.add(difference(DifferenceType.COMPONENT_ADDED, groupA, groupB, groupA, groupB));
+            return;
+        }
+
+        if (groupB == null) {
+            differences.add(difference(DifferenceType.COMPONENT_REMOVED, groupA, groupB, groupA, groupB));
+            return;
+        }
+
+        addIfDifferent(differences, DifferenceType.VERSIONED_FLOW_COORDINATES_CHANGED, groupA, groupB, VersionedProcessGroup::getVersionedFlowCoordinates);
+
+        if (groupA.getVersionedFlowCoordinates() == null && groupB.getVersionedFlowCoordinates() == null) {
+            differences.addAll(compareComponents(groupA.getConnections(), groupB.getConnections(), this::compare));
+            differences.addAll(compareComponents(groupA.getProcessors(), groupB.getProcessors(), this::compare));
+            differences.addAll(compareComponents(groupA.getControllerServices(), groupB.getControllerServices(), this::compare));
+            differences.addAll(compareComponents(groupA.getFunnels(), groupB.getFunnels(), this::compare));
+            differences.addAll(compareComponents(groupA.getInputPorts(), groupB.getInputPorts(), this::compare));
+            differences.addAll(compareComponents(groupA.getLabels(), groupB.getLabels(), this::compare));
+            differences.addAll(compareComponents(groupA.getOutputPorts(), groupB.getOutputPorts(), this::compare));
+            differences.addAll(compareComponents(groupA.getProcessGroups(), groupB.getProcessGroups(), (a, b, diffs) -> compare(a, b, diffs, true)));
+            differences.addAll(compareComponents(groupA.getRemoteProcessGroups(), groupB.getRemoteProcessGroups(), this::compare));
+        }
+    }
+
+
+    private void compare(final VersionedConnection connectionA, final VersionedConnection connectionB, final Set<FlowDifference> differences) {
+        if (compareComponents(connectionA, connectionB, differences)) {
+            return;
+        }
+
+        addIfDifferent(differences, DifferenceType.BACKPRESSURE_DATA_SIZE_THRESHOLD_CHANGED, connectionA, connectionB, VersionedConnection::getBackPressureDataSizeThreshold);
+        addIfDifferent(differences, DifferenceType.BACKPRESSURE_OBJECT_THRESHOLD_CHANGED, connectionA, connectionB, VersionedConnection::getBackPressureObjectThreshold);
+        addIfDifferent(differences, DifferenceType.BENDPOINTS_CHANGED, connectionA, connectionB, VersionedConnection::getBends);
+        addIfDifferent(differences, DifferenceType.DESTINATION_CHANGED, connectionA, connectionB, VersionedConnection::getDestination);
+        addIfDifferent(differences, DifferenceType.FLOWFILE_EXPIRATION_CHANGED, connectionA, connectionB, VersionedConnection::getFlowFileExpiration);
+        addIfDifferent(differences, DifferenceType.PRIORITIZERS_CHANGED, connectionA, connectionB, VersionedConnection::getPrioritizers);
+        addIfDifferent(differences, DifferenceType.SELECTED_RELATIONSHIPS_CHANGED, connectionA, connectionB, VersionedConnection::getSelectedRelationships);
+        addIfDifferent(differences, DifferenceType.SOURCE_CHANGED, connectionA, connectionB, c -> c.getSource().getId());
+
+        addIfDifferent(differences, DifferenceType.LOAD_BALANCE_STRATEGY_CHANGED, connectionA, connectionB,
+                conn -> conn.getLoadBalanceStrategy() == null ? DEFAULT_LOAD_BALANCE_STRATEGY : conn.getLoadBalanceStrategy());
+
+        addIfDifferent(differences, DifferenceType.PARTITIONING_ATTRIBUTE_CHANGED, connectionA, connectionB,
+                conn -> conn.getPartitioningAttribute() == null ? DEFAULT_PARTITIONING_ATTRIBUTE : conn.getPartitioningAttribute());
+
+        addIfDifferent(differences, DifferenceType.LOAD_BALANCE_COMPRESSION_CHANGED, connectionA, connectionB,
+            conn -> conn.getLoadBalanceCompression() == null ? DEFAULT_LOAD_BALANCE_COMPRESSION : conn.getLoadBalanceCompression());
+    }
+
+
+    private <T extends VersionedComponent> Map<String, T> byId(final Set<T> components) {
+        return components.stream().collect(Collectors.toMap(VersionedComponent::getIdentifier, Function.identity()));
+    }
+
+    private <T extends VersionedComponent> void addIfDifferent(final Set<FlowDifference> differences, final DifferenceType type, final T componentA, final T componentB,
+        final Function<T, Object> transform) {
+
+        addIfDifferent(differences, type, componentA, componentB, transform, true);
+    }
+
+    private <T extends VersionedComponent> void addIfDifferent(final Set<FlowDifference> differences, final DifferenceType type, final T componentA, final T componentB,
+        final Function<T, Object> transform, final boolean differentiateNullAndEmptyString) {
+
+        final Object valueA = transform.apply(componentA);
+        final Object valueB = transform.apply(componentB);
+
+        if (Objects.equals(valueA, valueB)) {
+            return;
+        }
+
+        // We don't want to disambiguate between an empty collection and null.
+        if ((valueA == null || valueA instanceof Collection) && (valueB == null || valueB instanceof Collection) && isEmpty((Collection<?>) valueA) && isEmpty((Collection<?>) valueB)) {
+            return;
+        }
+
+        if (!differentiateNullAndEmptyString && isEmptyString(valueA) && isEmptyString(valueB)) {
+            return;
+        }
+
+        differences.add(difference(type, componentA, componentB, null, valueA, valueB));
+    }
+
+    private boolean isEmpty(final Collection<?> collection) {
+        return collection == null || collection.isEmpty();
+    }
+
+    private boolean isEmptyString(final Object potentialString) {
+        if (potentialString == null) {
+            return true;
+        }
+
+        if (potentialString instanceof String) {
+            final String string = (String) potentialString;
+            return string.isEmpty();
+        } else {
+            return false;
+        }
+    }
+
+    private FlowDifference difference(final DifferenceType type, final VersionedComponent componentA, final VersionedComponent componentB,
+                                      final Object valueA, final Object valueB) {
+        return difference(type, componentA, componentB, null, valueA, valueB);
+    }
+
+    private FlowDifference difference(final DifferenceType type, final VersionedComponent componentA, final VersionedComponent componentB, final String fieldName,
+            final Object valueA, final Object valueB) {
+
+        final String description = differenceDescriptor.describeDifference(type, flowA.getName(), flowB.getName(), componentA, componentB, fieldName, valueA, valueB);
+        return new StandardFlowDifference(type, componentA, componentB, valueA, valueB, description);
+    }
+
+
+    private static interface ComponentComparator<T extends VersionedComponent> {
+        void compare(T componentA, T componentB, Set<FlowDifference> differences);
+    }
+}

http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/6f26290d/nifi-registry-core/nifi-registry-flow-diff/src/main/java/org/apache/nifi/registry/flow/diff/StandardFlowComparison.java
----------------------------------------------------------------------
diff --git a/nifi-registry-core/nifi-registry-flow-diff/src/main/java/org/apache/nifi/registry/flow/diff/StandardFlowComparison.java b/nifi-registry-core/nifi-registry-flow-diff/src/main/java/org/apache/nifi/registry/flow/diff/StandardFlowComparison.java
new file mode 100644
index 0000000..3245994
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-flow-diff/src/main/java/org/apache/nifi/registry/flow/diff/StandardFlowComparison.java
@@ -0,0 +1,60 @@
+/*
+ * 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.flow.diff;
+
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.Set;
+
+public class StandardFlowComparison implements FlowComparison {
+
+    private final ComparableDataFlow flowA;
+    private final ComparableDataFlow flowB;
+    private final Set<FlowDifference> differences;
+
+    public StandardFlowComparison(final ComparableDataFlow flowA, final ComparableDataFlow flowB) {
+        this.flowA = flowA;
+        this.flowB = flowB;
+        this.differences = new HashSet<>();
+    }
+
+    public StandardFlowComparison(final ComparableDataFlow flowA, final ComparableDataFlow flowB, final Set<FlowDifference> differences) {
+        this.flowA = flowA;
+        this.flowB = flowB;
+        this.differences = differences;
+    }
+
+    @Override
+    public ComparableDataFlow getFlowA() {
+        return flowA;
+    }
+
+    @Override
+    public ComparableDataFlow getFlowB() {
+        return flowB;
+    }
+
+    @Override
+    public Set<FlowDifference> getDifferences() {
+        return Collections.unmodifiableSet(differences);
+    }
+
+    public void addDifference(final FlowDifference difference) {
+        this.differences.add(difference);
+    }
+}

http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/6f26290d/nifi-registry-core/nifi-registry-flow-diff/src/main/java/org/apache/nifi/registry/flow/diff/StandardFlowDifference.java
----------------------------------------------------------------------
diff --git a/nifi-registry-core/nifi-registry-flow-diff/src/main/java/org/apache/nifi/registry/flow/diff/StandardFlowDifference.java b/nifi-registry-core/nifi-registry-flow-diff/src/main/java/org/apache/nifi/registry/flow/diff/StandardFlowDifference.java
new file mode 100644
index 0000000..054ee5f
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-flow-diff/src/main/java/org/apache/nifi/registry/flow/diff/StandardFlowDifference.java
@@ -0,0 +1,119 @@
+/*
+ * 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.flow.diff;
+
+import java.util.Objects;
+import java.util.Optional;
+
+import org.apache.nifi.registry.flow.VersionedComponent;
+
+public class StandardFlowDifference implements FlowDifference {
+    private final DifferenceType type;
+    private final VersionedComponent componentA;
+    private final VersionedComponent componentB;
+    private final Optional<String> fieldName;
+    private final Object valueA;
+    private final Object valueB;
+    private final String description;
+
+    public StandardFlowDifference(final DifferenceType type, final VersionedComponent componentA, final VersionedComponent componentB, final Object valueA, final Object valueB,
+            final String description) {
+        this(type, componentA, componentB, null, valueA, valueB, description);
+    }
+
+    public StandardFlowDifference(final DifferenceType type, final VersionedComponent componentA, final VersionedComponent componentB, final String fieldName,
+            final Object valueA, final Object valueB, final String description) {
+        this.type = type;
+        this.componentA = componentA;
+        this.componentB = componentB;
+        this.fieldName = Optional.ofNullable(fieldName);
+        this.valueA = valueA;
+        this.valueB = valueB;
+        this.description = description;
+    }
+
+    @Override
+    public DifferenceType getDifferenceType() {
+        return type;
+    }
+
+    @Override
+    public VersionedComponent getComponentA() {
+        return componentA;
+    }
+
+    @Override
+    public VersionedComponent getComponentB() {
+        return componentB;
+    }
+
+    @Override
+    public Optional<String> getFieldName() {
+        return fieldName;
+    }
+
+    @Override
+    public Object getValueA() {
+        return valueA;
+    }
+
+    @Override
+    public Object getValueB() {
+        return valueB;
+    }
+
+    @Override
+    public String getDescription() {
+        return description;
+    }
+
+    @Override
+    public String toString() {
+        return description;
+    }
+
+    @Override
+    public int hashCode() {
+        return 31 + 17 * (componentA == null ? 0 : componentA.getIdentifier().hashCode()) +
+            17 * (componentB == null ? 0 : componentB.getIdentifier().hashCode()) +
+            Objects.hash(description, type, valueA, valueB);
+    }
+
+    @Override
+    public boolean equals(final Object obj) {
+        if (obj == null) {
+            return false;
+        }
+        if (obj == this) {
+            return true;
+        }
+        if (!(obj instanceof StandardFlowDifference)) {
+            return false;
+        }
+        final StandardFlowDifference other = (StandardFlowDifference) obj;
+        final String componentAId = componentA == null ? null : componentA.getIdentifier();
+        final String otherComponentAId = other.componentA == null ? null : other.componentA.getIdentifier();
+
+        final String componentBId = componentB == null ? null : componentB.getIdentifier();
+        final String otherComponentBId = other.componentB == null ? null : other.componentB.getIdentifier();
+
+        return Objects.equals(componentAId, otherComponentAId) && Objects.equals(componentBId, otherComponentBId)
+            && Objects.equals(description, other.description) && Objects.equals(type, other.type)
+            && Objects.equals(valueA, other.valueA) && Objects.equals(valueB, other.valueB);
+    }
+}

http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/6f26290d/nifi-registry-core/nifi-registry-flow-diff/src/main/java/org/apache/nifi/registry/flow/diff/StaticDifferenceDescriptor.java
----------------------------------------------------------------------
diff --git a/nifi-registry-core/nifi-registry-flow-diff/src/main/java/org/apache/nifi/registry/flow/diff/StaticDifferenceDescriptor.java b/nifi-registry-core/nifi-registry-flow-diff/src/main/java/org/apache/nifi/registry/flow/diff/StaticDifferenceDescriptor.java
new file mode 100644
index 0000000..5bf82c1
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-flow-diff/src/main/java/org/apache/nifi/registry/flow/diff/StaticDifferenceDescriptor.java
@@ -0,0 +1,89 @@
+/*
+ * 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.flow.diff;
+
+import java.util.Objects;
+
+import org.apache.nifi.registry.flow.VersionedComponent;
+import org.apache.nifi.registry.flow.VersionedFlowCoordinates;
+
+/**
+ * Describes differences between flows as if the flows are two disparate flows that are being
+ * compared to one another. This provides verbiage such as "Processor with ID 123 exists in Flow A but not in Flow B."
+ */
+public class StaticDifferenceDescriptor implements DifferenceDescriptor {
+
+    @Override
+    public String describeDifference(final DifferenceType type, final String flowAName, final String flowBName, final VersionedComponent componentA,
+        final VersionedComponent componentB, final String fieldName, final Object valueA, final Object valueB) {
+
+        final String description;
+        switch (type) {
+            case COMPONENT_ADDED:
+                description = String.format("%s with ID %s exists in %s but not in %s",
+                    componentB.getComponentType().getTypeName(), componentB.getIdentifier(), flowBName, flowAName);
+                break;
+            case COMPONENT_REMOVED:
+                description = String.format("%s with ID %s exists in %s but not in %s",
+                    componentA.getComponentType().getTypeName(), componentA.getIdentifier(), flowAName, flowBName);
+                break;
+            case PROPERTY_ADDED:
+                description = String.format("Property '%s' exists for %s with ID %s in %s but not in %s",
+                    fieldName, componentB.getComponentType().getTypeName(), componentB.getIdentifier(), flowBName, flowAName);
+                break;
+            case PROPERTY_REMOVED:
+                description = String.format("Property '%s' exists for %s with ID %s in %s but not in %s",
+                    fieldName, componentA.getComponentType().getTypeName(), componentA.getIdentifier(), flowAName, flowBName);
+                break;
+            case VARIABLE_ADDED:
+                description = String.format("Variable '%s' exists for Process Group with ID %s in %s but not in %s",
+                    fieldName, componentB.getIdentifier(), flowBName, flowAName);
+                break;
+            case VARIABLE_REMOVED:
+                description = String.format("Variable '%s' exists for Process Group with ID %s in %s but not in %s",
+                    fieldName, componentA.getIdentifier(), flowAName, flowBName);
+                break;
+            case VERSIONED_FLOW_COORDINATES_CHANGED:
+                if (valueA instanceof VersionedFlowCoordinates && valueB instanceof VersionedFlowCoordinates) {
+                    final VersionedFlowCoordinates coordinatesA = (VersionedFlowCoordinates) valueA;
+                    final VersionedFlowCoordinates coordinatesB = (VersionedFlowCoordinates) valueB;
+
+                    // If the two vary only by version, then use a more concise message. If anything else is different, then use a fully explanation.
+                    if (Objects.equals(coordinatesA.getRegistryUrl(), coordinatesB.getRegistryUrl()) && Objects.equals(coordinatesA.getBucketId(), coordinatesB.getBucketId())
+                            && Objects.equals(coordinatesA.getFlowId(), coordinatesB.getFlowId()) && coordinatesA.getVersion() != coordinatesB.getVersion()) {
+
+                        description = String.format("Flow Version is %s in %s but %s in %s", coordinatesA.getVersion(), flowAName, coordinatesB.getVersion(), flowBName);
+                        break;
+                    }
+                }
+
+                description = String.format("%s for %s with ID %s; flow '%s' has value %s; flow '%s' has value %s",
+                    type.getDescription(), componentA.getComponentType().getTypeName(), componentA.getIdentifier(),
+                    flowAName, valueA, flowBName, valueB);
+                break;
+            default:
+                description = String.format("%s for %s with ID %s; flow '%s' has value %s; flow '%s' has value %s",
+                    type.getDescription(), componentA.getComponentType().getTypeName(), componentA.getIdentifier(),
+                    flowAName, valueA, flowBName, valueB);
+                break;
+        }
+
+        return description;
+    }
+
+}


[18/51] [partial] nifi-registry git commit: NIFIREG-201 Refactoring project structure to better isolate extensions

Posted by kd...@apache.org.
http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/6f26290d/nifi-registry-core/nifi-registry-utils/src/main/java/org/apache/nifi/registry/util/FileUtils.java
----------------------------------------------------------------------
diff --git a/nifi-registry-core/nifi-registry-utils/src/main/java/org/apache/nifi/registry/util/FileUtils.java b/nifi-registry-core/nifi-registry-utils/src/main/java/org/apache/nifi/registry/util/FileUtils.java
new file mode 100644
index 0000000..5abaf7e
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-utils/src/main/java/org/apache/nifi/registry/util/FileUtils.java
@@ -0,0 +1,426 @@
+/*
+ * 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.util;
+
+import java.io.Closeable;
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileNotFoundException;
+import java.io.FileOutputStream;
+import java.io.FilenameFilter;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.nio.channels.FileChannel;
+import java.nio.channels.FileLock;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.Arrays;
+import java.util.Collection;
+
+import org.slf4j.Logger;
+
+/**
+ * A utility class containing a few useful static methods to do typical IO
+ * operations.
+ *
+ */
+public class FileUtils {
+
+    public static final long TRANSFER_CHUNK_SIZE_BYTES = 1024 * 1024 * 8; //8 MB chunks
+    public static final long MILLIS_BETWEEN_ATTEMPTS = 50L;
+
+    /**
+     * Closes the given closeable quietly - no logging, no exceptions...
+     *
+     * @param closeable the thing to close
+     */
+    public static void closeQuietly(final Closeable closeable) {
+        if (null != closeable) {
+            try {
+                closeable.close();
+            } catch (final IOException io) {/*IGNORE*/
+
+            }
+        }
+    }
+
+    /**
+     * Releases the given lock quietly no logging, no exception
+     *
+     * @param lock the lock to release
+     */
+    public static void releaseQuietly(final FileLock lock) {
+        if (null != lock) {
+            try {
+                lock.release();
+            } catch (final IOException io) {
+                /*IGNORE*/
+            }
+        }
+    }
+
+    /* Superseded by renamed class bellow */
+    @Deprecated
+    public static void ensureDirectoryExistAndCanAccess(final File dir) throws IOException {
+        ensureDirectoryExistAndCanReadAndWrite(dir);
+    }
+
+    public static void ensureDirectoryExistAndCanReadAndWrite(final File dir) throws IOException {
+        if (dir.exists() && !dir.isDirectory()) {
+            throw new IOException(dir.getAbsolutePath() + " is not a directory");
+        } else if (!dir.exists()) {
+            final boolean made = dir.mkdirs();
+            if (!made) {
+                throw new IOException(dir.getAbsolutePath() + " could not be created");
+            }
+        }
+        if (!(dir.canRead() && dir.canWrite())) {
+            throw new IOException(dir.getAbsolutePath() + " directory does not have read/write privilege");
+        }
+    }
+
+    public static void ensureDirectoryExistAndCanRead(final File dir) throws IOException {
+        if (dir.exists() && !dir.isDirectory()) {
+            throw new IOException(dir.getAbsolutePath() + " is not a directory");
+        } else if (!dir.exists()) {
+            final boolean made = dir.mkdirs();
+            if (!made) {
+                throw new IOException(dir.getAbsolutePath() + " could not be created");
+            }
+        }
+        if (!dir.canRead()) {
+            throw new IOException(dir.getAbsolutePath() + " directory does not have read privilege");
+        }
+    }
+
+    /**
+     * Copies the given source file to the given destination file. The given destination will be overwritten if it already exists.
+     *
+     * @param source the file to copy
+     * @param destination the file to copy to
+     * @param lockInputFile if true will lock input file during copy; if false will not
+     * @param lockOutputFile if true will lock output file during copy; if false will not
+     * @param move if true will perform what is effectively a move operation rather than a pure copy. This allows for potentially highly efficient movement of the file but if not possible this will
+     * revert to a copy then delete behavior. If false, then the file is copied and the source file is retained. If a true rename/move occurs then no lock is held during that time.
+     * @param logger if failures occur, they will be logged to this logger if possible. If this logger is null, an IOException will instead be thrown, indicating the problem.
+     * @return long number of bytes copied
+     * @throws FileNotFoundException if the source file could not be found
+     * @throws IOException if unable to read or write the underlying streams
+     * @throws SecurityException if a security manager denies the needed file operations
+     */
+    public static long copyFile(final File source, final File destination, final boolean lockInputFile, final boolean lockOutputFile, final boolean move, final Logger logger)
+            throws FileNotFoundException, IOException {
+
+        FileInputStream fis = null;
+        FileOutputStream fos = null;
+        FileLock inLock = null;
+        FileLock outLock = null;
+        long fileSize = 0L;
+        if (!source.canRead()) {
+            throw new IOException("Must at least have read permission");
+
+        }
+        if (move && source.renameTo(destination)) {
+            fileSize = destination.length();
+        } else {
+            try {
+                fis = new FileInputStream(source);
+                fos = new FileOutputStream(destination);
+                final FileChannel in = fis.getChannel();
+                final FileChannel out = fos.getChannel();
+                if (lockInputFile) {
+                    inLock = in.tryLock(0, Long.MAX_VALUE, true);
+                    if (null == inLock) {
+                        throw new IOException("Unable to obtain shared file lock for: " + source.getAbsolutePath());
+                    }
+                }
+                if (lockOutputFile) {
+                    outLock = out.tryLock(0, Long.MAX_VALUE, false);
+                    if (null == outLock) {
+                        throw new IOException("Unable to obtain exclusive file lock for: " + destination.getAbsolutePath());
+                    }
+                }
+                long bytesWritten = 0;
+                do {
+                    bytesWritten += out.transferFrom(in, bytesWritten, TRANSFER_CHUNK_SIZE_BYTES);
+                    fileSize = in.size();
+                } while (bytesWritten < fileSize);
+                out.force(false);
+                FileUtils.closeQuietly(fos);
+                FileUtils.closeQuietly(fis);
+                fos = null;
+                fis = null;
+                if (move && !FileUtils.deleteFile(source, null, 5)) {
+                    if (logger == null) {
+                        FileUtils.deleteFile(destination, null, 5);
+                        throw new IOException("Could not remove file " + source.getAbsolutePath());
+                    } else {
+                        logger.warn("Configured to delete source file when renaming/move not successful.  However, unable to delete file at: " + source.getAbsolutePath());
+                    }
+                }
+            } finally {
+                FileUtils.releaseQuietly(inLock);
+                FileUtils.releaseQuietly(outLock);
+                FileUtils.closeQuietly(fos);
+                FileUtils.closeQuietly(fis);
+            }
+        }
+        return fileSize;
+    }
+
+    /**
+     * Copies the given source file to the given destination file. The given destination will be overwritten if it already exists.
+     *
+     * @param source the file to copy from
+     * @param destination the file to copy to
+     * @param lockInputFile if true will lock input file during copy; if false will not
+     * @param lockOutputFile if true will lock output file during copy; if false will not
+     * @param logger the logger to use
+     * @return long number of bytes copied
+     * @throws FileNotFoundException if the source file could not be found
+     * @throws IOException if unable to read or write to file
+     * @throws SecurityException if a security manager denies the needed file operations
+     */
+    public static long copyFile(final File source, final File destination, final boolean lockInputFile, final boolean lockOutputFile, final Logger logger) throws FileNotFoundException, IOException {
+        return FileUtils.copyFile(source, destination, lockInputFile, lockOutputFile, false, logger);
+    }
+
+    public static long copyFile(final File source, final OutputStream stream, final boolean closeOutputStream, final boolean lockInputFile) throws FileNotFoundException, IOException {
+        FileInputStream fis = null;
+        FileLock inLock = null;
+        long fileSize = 0L;
+        try {
+            fis = new FileInputStream(source);
+            final FileChannel in = fis.getChannel();
+            if (lockInputFile) {
+                inLock = in.tryLock(0, Long.MAX_VALUE, true);
+                if (inLock == null) {
+                    throw new IOException("Unable to obtain exclusive file lock for: " + source.getAbsolutePath());
+                }
+
+            }
+
+            byte[] buffer = new byte[1 << 18]; //256 KB
+            int bytesRead = -1;
+            while ((bytesRead = fis.read(buffer)) != -1) {
+                stream.write(buffer, 0, bytesRead);
+            }
+            in.force(false);
+            stream.flush();
+            fileSize = in.size();
+        } finally {
+            FileUtils.releaseQuietly(inLock);
+            FileUtils.closeQuietly(fis);
+            if (closeOutputStream) {
+                FileUtils.closeQuietly(stream);
+            }
+        }
+        return fileSize;
+    }
+
+    public static long copyFile(final InputStream stream, final File destination, final boolean closeInputStream, final boolean lockOutputFile) throws FileNotFoundException, IOException {
+        final Path destPath = destination.toPath();
+        final long size = Files.copy(stream, destPath);
+        if (closeInputStream) {
+            stream.close();
+        }
+        return size;
+    }
+
+    /**
+     * Deletes the given file. If the given file exists but could not be deleted
+     * this will be printed as a warning to the given logger
+     *
+     * @param file to delete
+     * @param logger to notify
+     * @return true if deleted
+     */
+    public static boolean deleteFile(final File file, final Logger logger) {
+        return FileUtils.deleteFile(file, logger, 1);
+    }
+
+    /**
+     * Deletes the given file. If the given file exists but could not be deleted
+     * this will be printed as a warning to the given logger
+     *
+     * @param file to delete
+     * @param logger to notify
+     * @param attempts indicates how many times an attempt to delete should be
+     * made
+     * @return true if given file no longer exists
+     */
+    public static boolean deleteFile(final File file, final Logger logger, final int attempts) {
+        if (file == null) {
+            return false;
+        }
+        boolean isGone = false;
+        try {
+            if (file.exists()) {
+                final int effectiveAttempts = Math.max(1, attempts);
+                for (int i = 0; i < effectiveAttempts && !isGone; i++) {
+                    isGone = file.delete() || !file.exists();
+                    if (!isGone && (effectiveAttempts - i) > 1) {
+                        FileUtils.sleepQuietly(MILLIS_BETWEEN_ATTEMPTS);
+                    }
+                }
+                if (!isGone && logger != null) {
+                    logger.warn("File appears to exist but unable to delete file: " + file.getAbsolutePath());
+                }
+            }
+        } catch (final Throwable t) {
+            if (logger != null) {
+                logger.warn("Unable to delete file: '" + file.getAbsolutePath() + "' due to " + t);
+            }
+        }
+        return isGone;
+    }
+
+    /**
+     * Deletes all files (not directories..) in the given directory (non
+     * recursive) that match the given filename filter. If any file cannot be
+     * deleted then this is printed at warn to the given logger.
+     *
+     * @param directory to delete contents of
+     * @param filter if null then no filter is used
+     * @param logger to notify
+     * @throws IOException if abstract pathname does not denote a directory, or
+     * if an I/O error occurs
+     */
+    public static void deleteFilesInDirectory(final File directory, final FilenameFilter filter, final Logger logger) throws IOException {
+        FileUtils.deleteFilesInDirectory(directory, filter, logger, false);
+    }
+
+    /**
+     * Deletes all files (not directories) in the given directory (recursive)
+     * that match the given filename filter. If any file cannot be deleted then
+     * this is printed at warn to the given logger.
+     *
+     * @param directory to delete contents of
+     * @param filter if null then no filter is used
+     * @param logger to notify
+     * @param recurse true if should recurse
+     * @throws IOException if abstract pathname does not denote a directory, or
+     * if an I/O error occurs
+     */
+    public static void deleteFilesInDirectory(final File directory, final FilenameFilter filter, final Logger logger, final boolean recurse) throws IOException {
+        FileUtils.deleteFilesInDirectory(directory, filter, logger, recurse, false);
+    }
+
+    /**
+     * Deletes all files (not directories) in the given directory (recursive)
+     * that match the given filename filter. If any file cannot be deleted then
+     * this is printed at warn to the given logger.
+     *
+     * @param directory to delete contents of
+     * @param filter if null then no filter is used
+     * @param logger to notify
+     * @param recurse will look for contents of sub directories.
+     * @param deleteEmptyDirectories default is false; if true will delete
+     * directories found that are empty
+     * @throws IOException if abstract pathname does not denote a directory, or
+     * if an I/O error occurs
+     */
+    public static void deleteFilesInDirectory(final File directory, final FilenameFilter filter, final Logger logger, final boolean recurse, final boolean deleteEmptyDirectories) throws IOException {
+        // ensure the specified directory is actually a directory and that it exists
+        if (null != directory && directory.isDirectory()) {
+            final File ingestFiles[] = directory.listFiles();
+            if (ingestFiles == null) {
+                // null if abstract pathname does not denote a directory, or if an I/O error occurs
+                throw new IOException("Unable to list directory content in: " + directory.getAbsolutePath());
+            }
+            for (File ingestFile : ingestFiles) {
+                boolean process = (filter == null) ? true : filter.accept(directory, ingestFile.getName());
+                if (ingestFile.isFile() && process) {
+                    FileUtils.deleteFile(ingestFile, logger, 3);
+                }
+                if (ingestFile.isDirectory() && recurse) {
+                    FileUtils.deleteFilesInDirectory(ingestFile, filter, logger, recurse, deleteEmptyDirectories);
+                    if (deleteEmptyDirectories && ingestFile.list().length == 0) {
+                        FileUtils.deleteFile(ingestFile, logger, 3);
+                    }
+                }
+            }
+        }
+    }
+
+    /**
+     * Deletes given files.
+     *
+     * @param files to delete
+     * @param recurse will recurse
+     * @throws IOException if issues deleting files
+     */
+    public static void deleteFiles(final Collection<File> files, final boolean recurse) throws IOException {
+        for (final File file : files) {
+            FileUtils.deleteFile(file, recurse);
+        }
+    }
+
+    public static void deleteFile(final File file, final boolean recurse) throws IOException {
+        final File[] list = file.listFiles();
+        if (file.isDirectory() && recurse && list != null) {
+            FileUtils.deleteFiles(Arrays.asList(list), recurse);
+        }
+        //now delete the file itself regardless of whether it is plain file or a directory
+        if (!FileUtils.deleteFile(file, null, 5)) {
+            throw new IOException("Unable to delete " + file.getAbsolutePath());
+        }
+    }
+
+    public static void sleepQuietly(final long millis) {
+        try {
+            Thread.sleep(millis);
+        } catch (final InterruptedException ex) {
+            /* do nothing */
+        }
+    }
+
+
+    // The invalid character list is derived from this Stackoverflow page.
+    // https://stackoverflow.com/questions/1155107/is-there-a-cross-platform-java-method-to-remove-filename-special-chars
+    private final static int[] INVALID_CHARS = {34, 60, 62, 124, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15,
+            16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 58, 42, 63, 92, 47, 32};
+
+    static {
+        Arrays.sort(INVALID_CHARS);
+    }
+
+    /**
+     * Replaces invalid characters for a file system name within a given filename string to underscore '_'.
+     * Be careful not to pass a file path as this method replaces path delimiter characters (i.e forward/back slashes).
+     * @param filename The filename to clean
+     * @return sanitized filename
+     */
+    public static String sanitizeFilename(String filename) {
+        if (filename == null || filename.isEmpty()) {
+            return filename;
+        }
+        int codePointCount = filename.codePointCount(0, filename.length());
+
+        final StringBuilder cleanName = new StringBuilder();
+        for (int i = 0; i < codePointCount; i++) {
+            int c = filename.codePointAt(i);
+            if (Arrays.binarySearch(INVALID_CHARS, c) < 0) {
+                cleanName.appendCodePoint(c);
+            } else {
+                cleanName.append('_');
+            }
+        }
+        return cleanName.toString();
+    }
+}

http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/6f26290d/nifi-registry-core/nifi-registry-utils/src/main/java/org/apache/nifi/registry/util/FormatUtils.java
----------------------------------------------------------------------
diff --git a/nifi-registry-core/nifi-registry-utils/src/main/java/org/apache/nifi/registry/util/FormatUtils.java b/nifi-registry-core/nifi-registry-utils/src/main/java/org/apache/nifi/registry/util/FormatUtils.java
new file mode 100644
index 0000000..c1e353d
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-utils/src/main/java/org/apache/nifi/registry/util/FormatUtils.java
@@ -0,0 +1,261 @@
+/*
+ * 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.util;
+
+import java.text.NumberFormat;
+import java.util.concurrent.TimeUnit;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+public class FormatUtils {
+
+    private static final String UNION = "|";
+
+    // for Data Sizes
+    private static final double BYTES_IN_KILOBYTE = 1024;
+    private static final double BYTES_IN_MEGABYTE = BYTES_IN_KILOBYTE * 1024;
+    private static final double BYTES_IN_GIGABYTE = BYTES_IN_MEGABYTE * 1024;
+    private static final double BYTES_IN_TERABYTE = BYTES_IN_GIGABYTE * 1024;
+
+    // for Time Durations
+    private static final String NANOS = join(UNION, "ns", "nano", "nanos", "nanosecond", "nanoseconds");
+    private static final String MILLIS = join(UNION, "ms", "milli", "millis", "millisecond", "milliseconds");
+    private static final String SECS = join(UNION, "s", "sec", "secs", "second", "seconds");
+    private static final String MINS = join(UNION, "m", "min", "mins", "minute", "minutes");
+    private static final String HOURS = join(UNION, "h", "hr", "hrs", "hour", "hours");
+    private static final String DAYS = join(UNION, "d", "day", "days");
+    private static final String WEEKS = join(UNION, "w", "wk", "wks", "week", "weeks");
+
+    private static final String VALID_TIME_UNITS = join(UNION, NANOS, MILLIS, SECS, MINS, HOURS, DAYS, WEEKS);
+    public static final String TIME_DURATION_REGEX = "(\\d+)\\s*(" + VALID_TIME_UNITS + ")";
+    public static final Pattern TIME_DURATION_PATTERN = Pattern.compile(TIME_DURATION_REGEX);
+
+    /**
+     * Formats the specified count by adding commas.
+     *
+     * @param count the value to add commas to
+     * @return the string representation of the given value with commas included
+     */
+    public static String formatCount(final long count) {
+        return NumberFormat.getIntegerInstance().format(count);
+    }
+
+    /**
+     * Formats the specified duration in 'mm:ss.SSS' format.
+     *
+     * @param sourceDuration the duration to format
+     * @param sourceUnit the unit to interpret the duration
+     * @return representation of the given time data in minutes/seconds
+     */
+    public static String formatMinutesSeconds(final long sourceDuration, final TimeUnit sourceUnit) {
+        final long millis = TimeUnit.MILLISECONDS.convert(sourceDuration, sourceUnit);
+
+        final long millisInMinute = TimeUnit.MILLISECONDS.convert(1, TimeUnit.MINUTES);
+        final int minutes = (int) (millis / millisInMinute);
+        final long secondsMillisLeft = millis - minutes * millisInMinute;
+
+        final long millisInSecond = TimeUnit.MILLISECONDS.convert(1, TimeUnit.SECONDS);
+        final int seconds = (int) (secondsMillisLeft / millisInSecond);
+        final long millisLeft = secondsMillisLeft - seconds * millisInSecond;
+
+        return pad2Places(minutes) + ":" + pad2Places(seconds) + "." + pad3Places(millisLeft);
+    }
+
+    /**
+     * Formats the specified duration in 'HH:mm:ss.SSS' format.
+     *
+     * @param sourceDuration the duration to format
+     * @param sourceUnit the unit to interpret the duration
+     * @return representation of the given time data in hours/minutes/seconds
+     */
+    public static String formatHoursMinutesSeconds(final long sourceDuration, final TimeUnit sourceUnit) {
+        final long millis = TimeUnit.MILLISECONDS.convert(sourceDuration, sourceUnit);
+
+        final long millisInHour = TimeUnit.MILLISECONDS.convert(1, TimeUnit.HOURS);
+        final int hours = (int) (millis / millisInHour);
+        final long minutesSecondsMillisLeft = millis - hours * millisInHour;
+
+        return pad2Places(hours) + ":" + formatMinutesSeconds(minutesSecondsMillisLeft, TimeUnit.MILLISECONDS);
+    }
+
+    private static String pad2Places(final long val) {
+        return (val < 10) ? "0" + val : String.valueOf(val);
+    }
+
+    private static String pad3Places(final long val) {
+        return (val < 100) ? "0" + pad2Places(val) : String.valueOf(val);
+    }
+
+    /**
+     * Formats the specified data size in human readable format.
+     *
+     * @param dataSize Data size in bytes
+     * @return Human readable format
+     */
+    public static String formatDataSize(final double dataSize) {
+        // initialize the formatter
+        final NumberFormat format = NumberFormat.getNumberInstance();
+        format.setMaximumFractionDigits(2);
+
+        // check terabytes
+        double dataSizeToFormat = dataSize / BYTES_IN_TERABYTE;
+        if (dataSizeToFormat > 1) {
+            return format.format(dataSizeToFormat) + " TB";
+        }
+
+        // check gigabytes
+        dataSizeToFormat = dataSize / BYTES_IN_GIGABYTE;
+        if (dataSizeToFormat > 1) {
+            return format.format(dataSizeToFormat) + " GB";
+        }
+
+        // check megabytes
+        dataSizeToFormat = dataSize / BYTES_IN_MEGABYTE;
+        if (dataSizeToFormat > 1) {
+            return format.format(dataSizeToFormat) + " MB";
+        }
+
+        // check kilobytes
+        dataSizeToFormat = dataSize / BYTES_IN_KILOBYTE;
+        if (dataSizeToFormat > 1) {
+            return format.format(dataSizeToFormat) + " KB";
+        }
+
+        // default to bytes
+        return format.format(dataSize) + " bytes";
+    }
+
+    public static long getTimeDuration(final String value, final TimeUnit desiredUnit) {
+        final Matcher matcher = TIME_DURATION_PATTERN.matcher(value.toLowerCase());
+        if (!matcher.matches()) {
+            throw new IllegalArgumentException("Value '" + value + "' is not a valid Time Duration");
+        }
+
+        final String duration = matcher.group(1);
+        final String units = matcher.group(2);
+        TimeUnit specifiedTimeUnit = null;
+        switch (units.toLowerCase()) {
+            case "ns":
+            case "nano":
+            case "nanos":
+            case "nanoseconds":
+                specifiedTimeUnit = TimeUnit.NANOSECONDS;
+                break;
+            case "ms":
+            case "milli":
+            case "millis":
+            case "milliseconds":
+                specifiedTimeUnit = TimeUnit.MILLISECONDS;
+                break;
+            case "s":
+            case "sec":
+            case "secs":
+            case "second":
+            case "seconds":
+                specifiedTimeUnit = TimeUnit.SECONDS;
+                break;
+            case "m":
+            case "min":
+            case "mins":
+            case "minute":
+            case "minutes":
+                specifiedTimeUnit = TimeUnit.MINUTES;
+                break;
+            case "h":
+            case "hr":
+            case "hrs":
+            case "hour":
+            case "hours":
+                specifiedTimeUnit = TimeUnit.HOURS;
+                break;
+            case "d":
+            case "day":
+            case "days":
+                specifiedTimeUnit = TimeUnit.DAYS;
+                break;
+            case "w":
+            case "wk":
+            case "wks":
+            case "week":
+            case "weeks":
+                final long durationVal = Long.parseLong(duration);
+                return desiredUnit.convert(durationVal, TimeUnit.DAYS)*7;
+        }
+
+        final long durationVal = Long.parseLong(duration);
+        return desiredUnit.convert(durationVal, specifiedTimeUnit);
+    }
+
+    public static String formatUtilization(final double utilization) {
+        return utilization + "%";
+    }
+
+    private static String join(final String delimiter, final String... values) {
+        if (values.length == 0) {
+            return "";
+        } else if (values.length == 1) {
+            return values[0];
+        }
+
+        final StringBuilder sb = new StringBuilder();
+        sb.append(values[0]);
+        for (int i = 1; i < values.length; i++) {
+            sb.append(delimiter).append(values[i]);
+        }
+
+        return sb.toString();
+    }
+
+    /**
+     * Formats nanoseconds in the format:
+     * 3 seconds, 8 millis, 3 nanos - if includeTotalNanos = false,
+     * 3 seconds, 8 millis, 3 nanos (3008000003 nanos) - if includeTotalNanos = true
+     *
+     * @param nanos the number of nanoseconds to format
+     * @param includeTotalNanos whether or not to include the total number of nanoseconds in parentheses in the returned value
+     * @return a human-readable String that is a formatted representation of the given number of nanoseconds.
+     */
+    public static String formatNanos(final long nanos, final boolean includeTotalNanos) {
+        final StringBuilder sb = new StringBuilder();
+
+        final long seconds = nanos > 1000000000L ? nanos / 1000000000L : 0L;
+        long millis = nanos > 1000000L ? nanos / 1000000L : 0L;
+        final long nanosLeft = nanos % 1000000L;
+
+        if (seconds > 0) {
+            sb.append(seconds).append(" seconds");
+        }
+        if (millis > 0) {
+            if (seconds > 0) {
+                sb.append(", ");
+                millis -= seconds * 1000L;
+            }
+
+            sb.append(millis).append(" millis");
+        }
+        if (seconds > 0 || millis > 0) {
+            sb.append(", ");
+        }
+        sb.append(nanosLeft).append(" nanos");
+
+        if (includeTotalNanos) {
+            sb.append(" (").append(nanos).append(" nanos)");
+        }
+
+        return sb.toString();
+    }
+}

http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/6f26290d/nifi-registry-core/nifi-registry-utils/src/main/java/org/apache/nifi/registry/util/PropertyValue.java
----------------------------------------------------------------------
diff --git a/nifi-registry-core/nifi-registry-utils/src/main/java/org/apache/nifi/registry/util/PropertyValue.java b/nifi-registry-core/nifi-registry-utils/src/main/java/org/apache/nifi/registry/util/PropertyValue.java
new file mode 100644
index 0000000..4950772
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-utils/src/main/java/org/apache/nifi/registry/util/PropertyValue.java
@@ -0,0 +1,91 @@
+/*
+ * 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.util;
+
+import java.util.concurrent.TimeUnit;
+
+/**
+ * <p>
+ * A PropertyValue provides a mechanism whereby the currently configured value
+ * can be obtained in different forms.
+ * </p>
+ */
+public interface PropertyValue {
+
+    /**
+     * @return the raw property value as a string
+     */
+    String getValue();
+
+    /**
+     * @return an integer representation of the property value, or
+     * <code>null</code> if not set
+     * @throws NumberFormatException if not able to parse
+     */
+    Integer asInteger();
+
+    /**
+     * @return a Long representation of the property value, or <code>null</code>
+     * if not set
+     * @throws NumberFormatException if not able to parse
+     */
+    Long asLong();
+
+    /**
+     * @return a Boolean representation of the property value, or
+     * <code>null</code> if not set
+     */
+    Boolean asBoolean();
+
+    /**
+     * @return a Float representation of the property value, or
+     * <code>null</code> if not set
+     * @throws NumberFormatException if not able to parse
+     */
+    Float asFloat();
+
+    /**
+     * @return a Double representation of the property value, of
+     * <code>null</code> if not set
+     * @throws NumberFormatException if not able to parse
+     */
+    Double asDouble();
+
+    /**
+     * @param timeUnit specifies the TimeUnit to convert the time duration into
+     * @return a Long value representing the value of the configured time period
+     * in terms of the specified TimeUnit; if the property is not set, returns
+     * <code>null</code>
+     */
+    Long asTimePeriod(TimeUnit timeUnit);
+
+    /**
+     *
+     * @param dataUnit specifies the DataUnit to convert the data size into
+     * @return a Long value representing the value of the configured data size
+     * in terms of the specified DataUnit; if hte property is not set, returns
+     * <code>null</code>
+     */
+    Double asDataSize(DataUnit dataUnit);
+
+    /**
+     * @return <code>true</code> if the user has configured a value, or if the
+     * PropertyDescriptor for the associated property has a default
+     * value, <code>false</code> otherwise
+     */
+    boolean isSet();
+}

http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/6f26290d/nifi-registry-core/nifi-registry-utils/src/main/java/org/apache/nifi/registry/util/StandardPropertyValue.java
----------------------------------------------------------------------
diff --git a/nifi-registry-core/nifi-registry-utils/src/main/java/org/apache/nifi/registry/util/StandardPropertyValue.java b/nifi-registry-core/nifi-registry-utils/src/main/java/org/apache/nifi/registry/util/StandardPropertyValue.java
new file mode 100644
index 0000000..b185fad
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-utils/src/main/java/org/apache/nifi/registry/util/StandardPropertyValue.java
@@ -0,0 +1,79 @@
+/*
+ * 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.util;
+
+import java.util.concurrent.TimeUnit;
+
+public class StandardPropertyValue implements PropertyValue {
+
+    private final String rawValue;
+
+    public StandardPropertyValue(final String rawValue) {
+        this.rawValue = rawValue;
+    }
+
+    @Override
+    public String getValue() {
+        return rawValue;
+    }
+
+    @Override
+    public Integer asInteger() {
+        return (rawValue == null) ? null : Integer.parseInt(rawValue.trim());
+    }
+
+    @Override
+    public Long asLong() {
+        return (rawValue == null) ? null : Long.parseLong(rawValue.trim());
+    }
+
+    @Override
+    public Boolean asBoolean() {
+        return (rawValue == null) ? null : Boolean.parseBoolean(rawValue.trim());
+    }
+
+    @Override
+    public Float asFloat() {
+        return (rawValue == null) ? null : Float.parseFloat(rawValue.trim());
+    }
+
+    @Override
+    public Double asDouble() {
+        return (rawValue == null) ? null : Double.parseDouble(rawValue.trim());
+    }
+
+    @Override
+    public Long asTimePeriod(final TimeUnit timeUnit) {
+        return (rawValue == null) ? null : FormatUtils.getTimeDuration(rawValue.trim(), timeUnit);
+    }
+
+    @Override
+    public Double asDataSize(final DataUnit dataUnit) {
+        return rawValue == null ? null : DataUnit.parseDataSize(rawValue.trim(), dataUnit);
+    }
+
+    @Override
+    public boolean isSet() {
+        return rawValue != null;
+    }
+
+    @Override
+    public String toString() {
+        return rawValue;
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/6f26290d/nifi-registry-core/nifi-registry-utils/src/test/java/org/apache/nifi/registry/util/TestFileUtils.java
----------------------------------------------------------------------
diff --git a/nifi-registry-core/nifi-registry-utils/src/test/java/org/apache/nifi/registry/util/TestFileUtils.java b/nifi-registry-core/nifi-registry-utils/src/test/java/org/apache/nifi/registry/util/TestFileUtils.java
new file mode 100644
index 0000000..d4bc963
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-utils/src/test/java/org/apache/nifi/registry/util/TestFileUtils.java
@@ -0,0 +1,31 @@
+/*
+ * 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.util;
+
+import org.junit.Test;
+
+import static org.junit.Assert.assertEquals;
+
+public class TestFileUtils {
+    @Test
+    public void testSanitizeFilename() {
+        String filename = "This / is / a test";
+        final String sanitizedFilename = FileUtils.sanitizeFilename(filename);
+        assertEquals("This___is___a_test", sanitizedFilename);
+    }
+}
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/6f26290d/nifi-registry-core/nifi-registry-web-api/pom.xml
----------------------------------------------------------------------
diff --git a/nifi-registry-core/nifi-registry-web-api/pom.xml b/nifi-registry-core/nifi-registry-web-api/pom.xml
new file mode 100644
index 0000000..65ec265
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-web-api/pom.xml
@@ -0,0 +1,359 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+  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.
+-->
+
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+    <modelVersion>4.0.0</modelVersion>
+    <parent>
+        <groupId>org.apache.nifi.registry</groupId>
+        <artifactId>nifi-registry-core</artifactId>
+        <version>0.3.0-SNAPSHOT</version>
+    </parent>
+    <artifactId>nifi-registry-web-api</artifactId>
+    <version>0.3.0-SNAPSHOT</version>
+    <packaging>war</packaging>
+
+    <build>
+        <resources>
+            <resource>
+                <directory>src/main/resources</directory>
+            </resource>
+        </resources>
+        <plugins>
+            <plugin>
+                <groupId>org.springframework.boot</groupId>
+                <artifactId>spring-boot-maven-plugin</artifactId>
+                <version>${spring.boot.version}</version>
+            </plugin>
+            <plugin>
+                <artifactId>maven-war-plugin</artifactId>
+                <configuration>
+                    <failOnMissingWebXml>false</failOnMissingWebXml>
+                </configuration>
+            </plugin>
+            <plugin>
+                <groupId>com.github.kongchen</groupId>
+                <artifactId>swagger-maven-plugin</artifactId>
+                <version>3.1.6</version>
+                <executions>
+                    <execution>
+                        <phase>compile</phase>
+                        <goals>
+                            <goal>generate</goal>
+                        </goals>
+                        <configuration>
+                            <apiSources>
+                                <apiSource>
+                                    <locations>
+                                        <location>org.apache.nifi.registry.web.api</location>
+                                    </locations>
+                                    <schemes>
+                                        <scheme>http</scheme>
+                                        <scheme>https</scheme>
+                                    </schemes>
+                                    <basePath>/nifi-registry-api</basePath>
+                                    <info>
+                                        <title>NiFi Registry REST API</title>
+                                        <version>${project.version}</version>
+                                        <description>
+                                            The REST API provides an interface to a registry with operations for saving, versioning, reading NiFi flows and components.
+                                        </description>
+                                        <contact>
+                                            <name>Apache NiFi Registry</name>
+                                            <email>dev@nifi.apache.org</email>
+                                            <url>https://nifi.apache.org</url>
+                                        </contact>
+                                        <license>
+                                            <url>http://www.apache.org/licenses/LICENSE-2.0.html</url>
+                                            <name>Apache 2.0 License</name>
+                                        </license>
+                                    </info>
+                                    <securityDefinitions>
+                                        <securityDefinition>
+                                            <jsonPath>${project.basedir}/src/main/resources/swagger/security-definitions.json</jsonPath>
+                                        </securityDefinition>
+                                    </securityDefinitions>
+                                    <templatePath>classpath:/templates/index.html.hbs</templatePath>
+                                    <outputPath>
+                                        ${project.build.directory}/${project.artifactId}-${project.version}/docs/rest-api/index.html
+                                    </outputPath>
+                                    <swaggerDirectory>${project.build.directory}/swagger</swaggerDirectory>
+                                </apiSource>
+                            </apiSources>
+                        </configuration>
+                    </execution>
+                </executions>
+            </plugin>
+            <plugin>
+                <artifactId>maven-resources-plugin</artifactId>
+                <executions>
+                    <execution>
+                        <id>copy-resources</id>
+                        <phase>validate</phase>
+                        <goals>
+                            <goal>copy-resources</goal>
+                        </goals>
+                        <configuration>
+                            <outputDirectory>${project.build.directory}/${project.artifactId}-${project.version}/docs/rest-api/images</outputDirectory>
+                            <resources>
+                                <resource>
+                                    <directory>src/main/resources/images</directory>
+                                </resource>
+                            </resources>
+                        </configuration>
+                    </execution>
+                </executions>
+            </plugin>
+            <plugin>
+                <groupId>com.googlecode.maven-download-plugin</groupId>
+                <artifactId>download-maven-plugin</artifactId>
+                <version>1.2.1</version>
+                <executions>
+                    <execution>
+                        <id>download-swagger-ui</id>
+                        <!-- This plugin downloads swagger UI static assets during the build, to be
+                             served by the web app to render the dynamically generated Swagger spec.
+                             For offline development, or to build without the swagger UI, activate
+                             the "no-swagger-ui" maven profile during the build with the "-P" flag -->
+                        <goals>
+                            <goal>wget</goal>
+                        </goals>
+                        <configuration>
+                            <url>
+                                https://github.com/swagger-api/swagger-ui/archive/v${swagger.ui.version}.tar.gz
+                            </url>
+                            <unpack>true</unpack>
+                            <outputDirectory>${project.build.directory}
+                            </outputDirectory>
+                        </configuration>
+                    </execution>
+                </executions>
+            </plugin>
+            <plugin>
+                <groupId>org.apache.maven.plugins</groupId>
+                <artifactId>maven-antrun-plugin</artifactId>
+                <version>1.8</version>
+                <executions>
+                    <execution>
+                        <id>bundle-swagger-ui</id>
+                        <phase>prepare-package</phase>
+                        <goals>
+                            <goal>run</goal>
+                        </goals>
+                        <configuration>
+                            <target>
+                                <sequential>
+                                    <echo>Copy static Swagger UI files to target</echo>
+                                    <copy todir="${project.build.directory}/classes/static/swagger">
+                                        <fileset dir="${project.build.directory}/swagger-ui-${swagger.ui.version}/dist">
+                                            <include name="**" />
+                                        </fileset>
+                                    </copy>
+                                    <echo>Disable schema validation by removing validatorUrl</echo>
+                                    <replace token="https://online.swagger.io/validator" value="" dir="${project.build.directory}/classes/static/swagger">
+                                        <include name="swagger-ui-bundle.js" />
+                                        <include name="swagger-ui-standalone-preset.js" />
+                                    </replace>
+                                    <echo>Rename 'index.html' to 'ui.html'</echo>
+                                    <move file="${project.build.directory}/classes/static/swagger/index.html" tofile="${project.build.directory}/classes/static/swagger/ui.html" />
+                                    <echo>Replace default swagger.json location</echo>
+                                    <replace token="http://petstore.swagger.io/v2/swagger.json" value="/nifi-registry-api/swagger/swagger.json" dir="${project.build.directory}/classes/static/swagger">
+                                        <include name="ui.html" />
+                                    </replace>
+                                    <echo>Copy swagger.json into static assets folder</echo>
+                                    <copy todir="${project.build.directory}/classes/static/swagger">
+                                        <fileset dir="${project.build.directory}/swagger">
+                                            <include name="*.json" />
+                                        </fileset>
+                                    </copy>
+                                </sequential>
+                            </target>
+                        </configuration>
+                    </execution>
+                </executions>
+            </plugin>
+        </plugins>
+    </build>
+
+    <profiles>
+        <profile>
+            <id>no-swagger-ui</id>
+            <!-- Activate this profile with "-P no-swagger-ui" to disable the Swagger UI
+                 static assets from being downloaded and bundled with the web api WAR. -->
+            <build>
+                <plugins>
+                    <plugin>
+                        <groupId>com.googlecode.maven-download-plugin</groupId>
+                        <artifactId>download-maven-plugin</artifactId>
+                        <version>1.2.1</version>
+                        <executions>
+                            <execution>
+                                <id>download-swagger-ui</id>
+                                <phase>none</phase>
+                            </execution>
+                        </executions>
+                    </plugin>
+                    <plugin>
+                        <groupId>org.apache.maven.plugins</groupId>
+                        <artifactId>maven-antrun-plugin</artifactId>
+                        <version>1.8</version>
+                        <executions>
+                            <execution>
+                                <id>bundle-swagger-ui</id>
+                                <phase>none</phase>
+                            </execution>
+                        </executions>
+                    </plugin>
+                </plugins>
+            </build>
+        </profile>
+    </profiles>
+
+    <dependencies>
+        <dependency>
+            <groupId>org.springframework.boot</groupId>
+            <artifactId>spring-boot-starter-web</artifactId>
+            <version>${spring.boot.version}</version>
+        </dependency>
+        <dependency>
+            <groupId>org.springframework.boot</groupId>
+            <artifactId>spring-boot-starter-jersey</artifactId>
+            <version>${spring.boot.version}</version>
+        </dependency>
+        <!-- Exclude micrometer-core because it creates a class cast issue with logback, revisit later -->
+        <dependency>
+            <groupId>org.springframework.boot</groupId>
+            <artifactId>spring-boot-starter-actuator</artifactId>
+            <version>${spring.boot.version}</version>
+            <exclusions>
+                <exclusion>
+                    <groupId>io.micrometer</groupId>
+                    <artifactId>micrometer-core</artifactId>
+                </exclusion>
+            </exclusions>
+        </dependency>
+        <dependency>
+            <groupId>org.springframework.security.kerberos</groupId>
+            <artifactId>spring-security-kerberos-core</artifactId>
+            <version>1.0.1.RELEASE</version>
+            <exclusions>
+                <exclusion>
+                    <groupId>org.springframework</groupId>
+                    <artifactId>spring-core</artifactId>
+                </exclusion>
+                <exclusion>
+                    <groupId>org.springframework.security</groupId>
+                    <artifactId>spring-security-core</artifactId>
+                </exclusion>
+            </exclusions>
+        </dependency>
+        <!-- Must be marked provided in order to produce a correct WAR -->
+        <dependency>
+            <groupId>org.springframework.boot</groupId>
+            <artifactId>spring-boot-starter-tomcat</artifactId>
+            <version>${spring.boot.version}</version>
+            <scope>provided</scope>
+        </dependency>
+        <dependency>
+            <groupId>org.apache.nifi.registry</groupId>
+            <artifactId>nifi-registry-framework</artifactId>
+            <version>0.3.0-SNAPSHOT</version>
+        </dependency>
+        <dependency>
+            <groupId>org.apache.nifi.registry</groupId>
+            <artifactId>nifi-registry-properties</artifactId>
+            <version>0.3.0-SNAPSHOT</version>
+            <scope>provided</scope> <!-- This will be in the lib directory -->
+        </dependency>
+        <dependency>
+            <groupId>org.apache.nifi.registry</groupId>
+            <artifactId>nifi-registry-security-api</artifactId>
+            <version>0.3.0-SNAPSHOT</version>
+            <scope>provided</scope> <!-- This will be in lib directory -->
+        </dependency>
+        <dependency>
+            <groupId>org.apache.nifi.registry</groupId>
+            <artifactId>nifi-registry-provider-api</artifactId>
+            <version>0.3.0-SNAPSHOT</version>
+            <scope>provided</scope> <!-- This will be in lib directory -->
+        </dependency>
+        <dependency>
+            <groupId>org.apache.nifi.registry</groupId>
+            <artifactId>nifi-registry-security-utils</artifactId>
+            <version>0.3.0-SNAPSHOT</version>
+        </dependency>
+        <dependency>
+            <groupId>org.apache.commons</groupId>
+            <artifactId>commons-lang3</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>javax.servlet</groupId>
+            <artifactId>javax.servlet-api</artifactId>
+            <scope>provided</scope>
+        </dependency>
+        <dependency>
+            <groupId>io.swagger</groupId>
+            <artifactId>swagger-annotations</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>io.jsonwebtoken</groupId>
+            <artifactId>jjwt</artifactId>
+            <version>0.7.0</version>
+        </dependency>
+        <!-- Test Dependencies -->
+        <dependency>
+            <groupId>org.apache.nifi.registry</groupId>
+            <artifactId>nifi-registry-client</artifactId>
+            <version>0.3.0-SNAPSHOT</version>
+            <scope>test</scope>
+        </dependency>
+        <dependency>
+            <groupId>org.springframework.boot</groupId>
+            <artifactId>spring-boot-starter-test</artifactId>
+            <version>${spring.boot.version}</version>
+            <scope>test</scope>
+        </dependency>
+        <dependency>
+            <groupId>org.springframework.boot</groupId>
+            <artifactId>spring-boot-starter-jetty</artifactId>
+            <version>${spring.boot.version}</version>
+            <scope>test</scope>
+        </dependency>
+        <dependency>
+            <groupId>com.unboundid</groupId>
+            <artifactId>unboundid-ldapsdk</artifactId>
+            <version>3.2.1</version>
+            <scope>test</scope>
+        </dependency>
+        <dependency>
+            <groupId>org.spockframework</groupId>
+            <artifactId>spock-core</artifactId>
+            <version>1.0-groovy-2.4</version>
+            <scope>test</scope>
+        </dependency>
+        <dependency>
+            <groupId>org.codehaus.groovy</groupId>
+            <artifactId>groovy-all</artifactId>
+            <version>2.4.12</version>
+            <scope>test</scope>
+        </dependency>
+        <dependency>
+            <groupId>cglib</groupId>
+            <artifactId>cglib-nodep</artifactId>
+            <version>2.2.2</version>
+            <scope>test</scope>
+        </dependency>
+    </dependencies>
+</project>

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/NiFiRegistryApiApplication.java
----------------------------------------------------------------------
diff --git a/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/NiFiRegistryApiApplication.java b/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/NiFiRegistryApiApplication.java
new file mode 100644
index 0000000..d06555d
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/NiFiRegistryApiApplication.java
@@ -0,0 +1,85 @@
+/*
+ * 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;
+
+import org.apache.nifi.registry.event.EventService;
+import org.apache.nifi.registry.event.StandardEvent;
+import org.apache.nifi.registry.hook.Event;
+import org.apache.nifi.registry.hook.EventType;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.boot.SpringApplication;
+import org.springframework.boot.autoconfigure.SpringBootApplication;
+import org.springframework.boot.builder.SpringApplicationBuilder;
+import org.springframework.boot.context.event.ApplicationReadyEvent;
+import org.springframework.boot.web.servlet.support.SpringBootServletInitializer;
+import org.springframework.context.ApplicationListener;
+import org.springframework.stereotype.Component;
+
+import java.util.Properties;
+
+/**
+ * Main class for starting the NiFi Registry Web API as a Spring Boot application.
+ *
+ * This class is purposely in the org.apache.nifi.registry package since that is the common base
+ * package across other modules. This is done because spring-boot will use the package of this
+ * class to automatically scan for beans/config/entities/etc. and would otherwise require
+ * configuring custom packages to scan in several different places.
+ *
+ * WebMvcAutoConfiguration is excluded because our web app is using Jersey in place of SpringMVC
+ */
+@SpringBootApplication
+public class NiFiRegistryApiApplication extends SpringBootServletInitializer {
+
+    public static final String NIFI_REGISTRY_PROPERTIES_ATTRIBUTE = "nifi-registry.properties";
+    public static final String NIFI_REGISTRY_MASTER_KEY_ATTRIBUTE = "nifi-registry.key";
+
+    @Autowired
+    private EventService eventService;
+
+    @Override
+    protected SpringApplicationBuilder configure(SpringApplicationBuilder application) {
+        final Properties defaultProperties = new Properties();
+
+        // Enable Actuator Endpoints
+        defaultProperties.setProperty("management.endpoints.web.expose", "*");
+
+        // Run Jersey as a filter instead of a servlet so that requests can be forwarded to other handlers (e.g., actuator)
+        defaultProperties.setProperty("spring.jersey.type", "filter");
+
+        return application
+                .sources(NiFiRegistryApiApplication.class)
+                .properties(defaultProperties);
+    }
+
+    @Component
+    private class OnApplicationReadyEventing
+            implements ApplicationListener<ApplicationReadyEvent> {
+
+        @Override
+        public void onApplicationEvent(final ApplicationReadyEvent event) {
+            Event registryStartEvent = new StandardEvent.Builder()
+                    .eventType(EventType.REGISTRY_START)
+                    .build();
+            eventService.publish(registryStartEvent);
+        }
+    }
+
+    public static void main(String[] args) {
+        SpringApplication.run(NiFiRegistryApiApplication.class, args);
+    }
+
+}

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/NiFiRegistryPropertiesFactory.java
----------------------------------------------------------------------
diff --git a/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/NiFiRegistryPropertiesFactory.java b/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/NiFiRegistryPropertiesFactory.java
new file mode 100644
index 0000000..b7865bb
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/NiFiRegistryPropertiesFactory.java
@@ -0,0 +1,47 @@
+/*
+ * 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;
+
+import org.apache.nifi.registry.properties.NiFiRegistryProperties;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.web.context.ServletContextAware;
+
+import javax.servlet.ServletContext;
+
+/**
+ * The JettyServer puts an instance of NiFiRegistryProperties into the ServletContext, this class
+ * obtains that instance and makes it available to inject to all other places.
+ *
+ */
+@Configuration
+public class NiFiRegistryPropertiesFactory implements ServletContextAware {
+
+    private NiFiRegistryProperties properties;
+
+    @Override
+    public void setServletContext(ServletContext servletContext) {
+        properties = (NiFiRegistryProperties) servletContext.getAttribute(
+                NiFiRegistryApiApplication.NIFI_REGISTRY_PROPERTIES_ATTRIBUTE);
+    }
+
+    @Bean
+    public NiFiRegistryProperties getNiFiRegistryProperties() {
+        return properties;
+    }
+
+}

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/NiFiRegistryResourceConfig.java
----------------------------------------------------------------------
diff --git a/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/NiFiRegistryResourceConfig.java b/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/NiFiRegistryResourceConfig.java
new file mode 100644
index 0000000..a5ab5ef
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/NiFiRegistryResourceConfig.java
@@ -0,0 +1,78 @@
+/*
+ * 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;
+
+import org.apache.nifi.registry.web.api.AccessPolicyResource;
+import org.apache.nifi.registry.web.api.AccessResource;
+import org.apache.nifi.registry.web.api.BucketFlowResource;
+import org.apache.nifi.registry.web.api.BucketResource;
+import org.apache.nifi.registry.web.api.ConfigResource;
+import org.apache.nifi.registry.web.api.FlowResource;
+import org.apache.nifi.registry.web.api.ItemResource;
+import org.apache.nifi.registry.web.api.TenantResource;
+import org.glassfish.jersey.server.ResourceConfig;
+import org.glassfish.jersey.server.ServerProperties;
+import org.glassfish.jersey.server.filter.HttpMethodOverrideFilter;
+import org.glassfish.jersey.servlet.ServletProperties;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.context.annotation.Configuration;
+
+import javax.servlet.ServletContext;
+import javax.ws.rs.core.Context;
+
+/**
+ * This is the main Jersey configuration for the application.
+ *
+ *  NOTE: Don't set @ApplicationPath here because it has already been set to 'nifi-registry-api' in JettyServer
+ */
+@Configuration
+public class NiFiRegistryResourceConfig extends ResourceConfig {
+
+    private static final Logger logger = LoggerFactory.getLogger(NiFiRegistryResourceConfig.class);
+
+    public NiFiRegistryResourceConfig(@Context ServletContext servletContext) {
+        // register filters
+        register(HttpMethodOverrideFilter.class);
+
+        // register the exception mappers & jackson object mapper resolver
+        packages("org.apache.nifi.registry.web.mapper");
+
+        // register endpoints
+        register(AccessPolicyResource.class);
+        register(AccessResource.class);
+        register(BucketResource.class);
+        register(BucketFlowResource.class);
+        register(FlowResource.class);
+        register(ItemResource.class);
+        register(TenantResource.class);
+        register(ConfigResource.class);
+
+        // include bean validation errors in response
+        property(ServerProperties.BV_SEND_ERROR_IN_RESPONSE, true);
+
+        // this is necessary for the /access/token/kerberos endpoint to work correctly
+        // when sending 401 Unauthorized with a WWW-Authenticate: Negotiate header.
+        // if this value needs to be changed, kerberos authentication needs to move to filter chain
+        // so it can directly set the HttpServletResponse instead of indirectly through a JAX-RS Response
+        property(ServerProperties.RESPONSE_SET_STATUS_OVER_SEND_ERROR, true);
+
+        // configure jersey to ignore resource paths for actuator and swagger-ui
+        property(ServletProperties.FILTER_STATIC_CONTENT_REGEX, "/(actuator|swagger/).*");
+    }
+
+}

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/AccessPolicyResource.java
----------------------------------------------------------------------
diff --git a/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/api/AccessPolicyResource.java b/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/api/AccessPolicyResource.java
new file mode 100644
index 0000000..713b369
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/api/AccessPolicyResource.java
@@ -0,0 +1,407 @@
+/*
+ * 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.nifi.registry.authorization.AccessPolicy;
+import org.apache.nifi.registry.authorization.AccessPolicySummary;
+import org.apache.nifi.registry.authorization.Resource;
+import org.apache.nifi.registry.event.EventService;
+import org.apache.nifi.registry.exception.ResourceNotFoundException;
+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.resource.Authorizable;
+import org.apache.nifi.registry.service.AuthorizationService;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Component;
+
+import javax.servlet.http.HttpServletRequest;
+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 java.net.URI;
+import java.util.Collections;
+import java.util.List;
+
+/**
+ * RESTful endpoint for managing access policies.
+ */
+@Component
+@Path("/policies")
+@Api(
+        value = "policies",
+        description = "Endpoint for managing access policies.",
+        authorizations = { @Authorization("Authorization") }
+)
+public class AccessPolicyResource extends AuthorizableApplicationResource {
+
+    private static final Logger logger = LoggerFactory.getLogger(AccessPolicyResource.class);
+
+    private Authorizer authorizer;
+
+    @Autowired
+    public AccessPolicyResource(
+            Authorizer authorizer,
+            AuthorizationService authorizationService,
+            EventService eventService) {
+        super(authorizationService, eventService);
+        this.authorizer = authorizer;
+    }
+
+    /**
+     * Create a new access policy.
+     *
+     * @param httpServletRequest request
+     * @param requestAccessPolicy the access policy to create.
+     * @return The created access policy.
+     */
+    @POST
+    @Consumes(MediaType.APPLICATION_JSON)
+    @Produces(MediaType.APPLICATION_JSON)
+    @ApiOperation(
+            value = "Creates an access policy",
+            response = AccessPolicy.class,
+            extensions = {
+                    @Extension(name = "access-policy", properties = {
+                            @ExtensionProperty(name = "action", value = "write"),
+                            @ExtensionProperty(name = "resource", value = "/policies") })
+            }
+    )
+    @ApiResponses({
+            @ApiResponse(code = 400, message = HttpStatusMessages.MESSAGE_400),
+            @ApiResponse(code = 401, message = HttpStatusMessages.MESSAGE_401),
+            @ApiResponse(code = 403, message = HttpStatusMessages.MESSAGE_403),
+            @ApiResponse(code = 409, message = HttpStatusMessages.MESSAGE_409 + " The NiFi Registry might not be configured to use a ConfigurableAccessPolicyProvider.") })
+    public Response createAccessPolicy(
+            @Context final HttpServletRequest httpServletRequest,
+            @ApiParam(value = "The access policy configuration details.", required = true)
+            final AccessPolicy requestAccessPolicy) {
+
+        verifyAuthorizerSupportsConfigurablePolicies();
+        authorizeAccess(RequestAction.WRITE);
+
+        if (requestAccessPolicy == null) {
+            throw new IllegalArgumentException("Access policy details must be specified when creating a new policy.");
+        }
+        if (requestAccessPolicy.getIdentifier() != null) {
+            throw new IllegalArgumentException("Access policy ID cannot be specified when creating a new policy.");
+        }
+        if (requestAccessPolicy.getResource() == null) {
+            throw new IllegalArgumentException("Resource must be specified when creating a new access policy.");
+        }
+        RequestAction.valueOfValue(requestAccessPolicy.getAction());
+
+        AccessPolicy createdPolicy = authorizationService.createAccessPolicy(requestAccessPolicy);
+
+        String locationUri = generateAccessPolicyUri(createdPolicy);
+        return generateCreatedResponse(URI.create(locationUri), createdPolicy).build();
+    }
+
+    /**
+     * Retrieves all access policies
+     *
+     * @return A list of access policies
+     */
+    @GET
+    @Consumes(MediaType.WILDCARD)
+    @Produces(MediaType.APPLICATION_JSON)
+    @ApiOperation(
+            value = "Gets all access policies",
+            response = AccessPolicy.class,
+            responseContainer = "List",
+            extensions = {
+                    @Extension(name = "access-policy", properties = {
+                            @ExtensionProperty(name = "action", value = "read"),
+                            @ExtensionProperty(name = "resource", value = "/policies") })
+            }
+    )
+    @ApiResponses({
+            @ApiResponse(code = 401, message = HttpStatusMessages.MESSAGE_401),
+            @ApiResponse(code = 403, message = HttpStatusMessages.MESSAGE_403),
+            @ApiResponse(code = 409, message = HttpStatusMessages.MESSAGE_409) })
+    public Response getAccessPolicies() {
+
+        verifyAuthorizerIsManaged();
+        authorizeAccess(RequestAction.READ);
+
+        List<AccessPolicy> accessPolicies = authorizationService.getAccessPolicies();
+        if (accessPolicies == null) {
+            accessPolicies = Collections.emptyList();
+        }
+
+        return generateOkResponse(accessPolicies).build();
+    }
+
+    /**
+     * Retrieves the specified access policy.
+     *
+     * @param identifier The id of the access policy to retrieve
+     * @return An accessPolicyEntity.
+     */
+    @GET
+    @Consumes(MediaType.WILDCARD)
+    @Produces(MediaType.APPLICATION_JSON)
+    @Path("{id}")
+    @ApiOperation(
+            value = "Gets an access policy",
+            response = AccessPolicy.class,
+            extensions = {
+                    @Extension(name = "access-policy", properties = {
+                            @ExtensionProperty(name = "action", value = "read"),
+                            @ExtensionProperty(name = "resource", value = "/policies") })
+            }
+    )
+    @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 getAccessPolicy(
+            @ApiParam(value = "The access policy id.", required = true)
+            @PathParam("id") final String identifier) {
+
+        verifyAuthorizerIsManaged();
+        authorizeAccess(RequestAction.READ);
+
+        final AccessPolicy accessPolicy = authorizationService.getAccessPolicy(identifier);
+        if (accessPolicy == null) {
+            logger.warn("The specified access policy id [{}] does not exist.", identifier);
+
+            throw new ResourceNotFoundException("The specified policy does not exist in this registry.");
+        }
+
+        return generateOkResponse(accessPolicy).build();
+    }
+
+
+    /**
+     * Retrieve a specified access policy for a given (action, resource) pair.
+     *
+     * @param action the action, i.e. "read", "write"
+     * @param rawResource the name of the resource as a raw string
+     * @return An access policy.
+     */
+    @GET
+    @Consumes(MediaType.WILDCARD)
+    @Produces(MediaType.APPLICATION_JSON)
+    @Path("{action}/{resource: .+}")
+    @ApiOperation(
+            value = "Gets an access policy for the specified action and resource",
+            response = AccessPolicy.class,
+            extensions = {
+                    @Extension(name = "access-policy", properties = {
+                            @ExtensionProperty(name = "action", value = "read"),
+                            @ExtensionProperty(name = "resource", value = "/policies") })
+            }
+    )
+    @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 getAccessPolicyForResource(
+            @ApiParam(value = "The request action.", allowableValues = "read, write, delete", required = true)
+            @PathParam("action")
+            final String action,
+            @ApiParam(value = "The resource of the policy.", required = true)
+            @PathParam("resource")
+            final String rawResource) {
+
+        verifyAuthorizerIsManaged();
+        authorizeAccess(RequestAction.READ);
+
+        // parse the action and resource type
+        final RequestAction requestAction = RequestAction.valueOfValue(action);
+        final String resource = "/" + rawResource;
+
+        AccessPolicy accessPolicy = authorizationService.getAccessPolicy(resource, requestAction);
+        if (accessPolicy == null) {
+            throw new ResourceNotFoundException("No policy found for action='" + action + "', resource='" + resource + "'");
+        }
+        return generateOkResponse(accessPolicy).build();
+    }
+
+
+    /**
+     * Update an access policy.
+     *
+     * @param httpServletRequest request
+     * @param identifier         The id of the access policy to update.
+     * @param requestAccessPolicy An access policy.
+     * @return the updated access policy.
+     */
+    @PUT
+    @Consumes(MediaType.APPLICATION_JSON)
+    @Produces(MediaType.APPLICATION_JSON)
+    @Path("{id}")
+    @ApiOperation(
+            value = "Updates a access policy",
+            response = AccessPolicy.class,
+            extensions = {
+                    @Extension(name = "access-policy", properties = {
+                            @ExtensionProperty(name = "action", value = "write"),
+                            @ExtensionProperty(name = "resource", value = "/policies") })
+            }
+    )
+    @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 + " The NiFi Registry might not be configured to use a ConfigurableAccessPolicyProvider.") })
+    public Response updateAccessPolicy(
+            @Context
+            final HttpServletRequest httpServletRequest,
+            @ApiParam(value = "The access policy id.", required = true)
+            @PathParam("id")
+            final String identifier,
+            @ApiParam(value = "The access policy configuration details.", required = true)
+            final AccessPolicy requestAccessPolicy) {
+
+        verifyAuthorizerSupportsConfigurablePolicies();
+        authorizeAccess(RequestAction.WRITE);
+
+        if (requestAccessPolicy == null) {
+            throw new IllegalArgumentException("Access policy details must be specified when updating a policy.");
+        }
+        if (!identifier.equals(requestAccessPolicy.getIdentifier())) {
+            throw new IllegalArgumentException(String.format("The policy id in the request body (%s) does not equal the "
+                    + "policy id of the requested resource (%s).", requestAccessPolicy.getIdentifier(), identifier));
+        }
+
+        AccessPolicy createdPolicy = authorizationService.updateAccessPolicy(requestAccessPolicy);
+
+        String locationUri = generateAccessPolicyUri(createdPolicy);
+        return generateOkResponse(createdPolicy).build();
+    }
+
+
+    /**
+     * Remove a specified access policy.
+     *
+     * @param httpServletRequest request
+     * @param identifier         The id of the access policy to remove.
+     * @return The deleted access policy
+     */
+    @DELETE
+    @Consumes(MediaType.WILDCARD)
+    @Produces(MediaType.APPLICATION_JSON)
+    @Path("{id}")
+    @ApiOperation(
+            value = "Deletes an access policy",
+            response = AccessPolicy.class,
+            extensions = {
+                    @Extension(name = "access-policy", properties = {
+                            @ExtensionProperty(name = "action", value = "delete"),
+                            @ExtensionProperty(name = "resource", value = "/policies") })
+            }
+    )
+    @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 + " The NiFi Registry might not be configured to use a ConfigurableAccessPolicyProvider.") })
+    public Response removeAccessPolicy(
+            @Context final HttpServletRequest httpServletRequest,
+            @ApiParam(value = "The access policy id.", required = true)
+            @PathParam("id")
+            final String identifier) {
+
+        verifyAuthorizerSupportsConfigurablePolicies();
+        authorizeAccess(RequestAction.DELETE);
+        AccessPolicy deletedPolicy = authorizationService.deleteAccessPolicy(identifier);
+        if (deletedPolicy == null) {
+            logger.warn("The specified access policy id [{}] does not exist.", identifier);
+
+            throw new ResourceNotFoundException("The specified policy does not exist in this registry.");
+        }
+        return generateOkResponse(deletedPolicy).build();
+    }
+
+    /**
+     * Gets the available resources that support access/authorization policies.
+     *
+     * @return A resourcesEntity.
+     */
+    @GET
+    @Path("/resources")
+    @Consumes(MediaType.WILDCARD)
+    @Produces(MediaType.APPLICATION_JSON)
+    @ApiOperation(
+            value = "Gets the available resources that support access/authorization policies",
+            response = Resource.class,
+            responseContainer = "List",
+            extensions = {
+                    @Extension(name = "access-policy", properties = {
+                            @ExtensionProperty(name = "action", value = "read"),
+                            @ExtensionProperty(name = "resource", value = "/policies") })
+            }
+    )
+    @ApiResponses({
+            @ApiResponse(code = 401, message = HttpStatusMessages.MESSAGE_401),
+            @ApiResponse(code = 403, message = HttpStatusMessages.MESSAGE_403) })
+    public Response getResources() {
+        authorizeAccess(RequestAction.READ);
+
+        final List<Resource> resources = authorizationService.getResources();
+
+        return generateOkResponse(resources).build();
+    }
+
+
+    private void verifyAuthorizerIsManaged() {
+        if (!AuthorizerCapabilityDetection.isManagedAuthorizer(authorizer)) {
+            throw new IllegalStateException(AuthorizationService.MSG_NON_MANAGED_AUTHORIZER);
+        }
+    }
+
+    private void verifyAuthorizerSupportsConfigurablePolicies() {
+        if (!AuthorizerCapabilityDetection.isConfigurableAccessPolicyProvider(authorizer)) {
+            verifyAuthorizerIsManaged();
+            throw new IllegalStateException(AuthorizationService.MSG_NON_CONFIGURABLE_POLICIES);
+        }
+    }
+
+    private void authorizeAccess(RequestAction actionType) {
+        final Authorizable policiesAuthorizable = authorizableLookup.getPoliciesAuthorizable();
+        authorizationService.authorize(policiesAuthorizable, actionType);
+    }
+
+    private String generateAccessPolicyUri(final AccessPolicySummary accessPolicy) {
+        return generateResourceUri("policies", accessPolicy.getIdentifier());
+    }
+
+}


[42/51] [partial] nifi-registry git commit: NIFIREG-201 Refactoring project structure to better isolate extensions

Posted by kd...@apache.org.
http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/6f26290d/nifi-registry-core/nifi-registry-docs/src/main/asciidoc/asciidoc-mod.css
----------------------------------------------------------------------
diff --git a/nifi-registry-core/nifi-registry-docs/src/main/asciidoc/asciidoc-mod.css b/nifi-registry-core/nifi-registry-docs/src/main/asciidoc/asciidoc-mod.css
new file mode 100644
index 0000000..527bd3e
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-docs/src/main/asciidoc/asciidoc-mod.css
@@ -0,0 +1,418 @@
+/* Asciidoctor default stylesheet | MIT License | http://asciidoctor.org */
+/* Copyright (C) 2012-2015 Dan Allen, Ryan Waldron and the Asciidoctor Project
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+THE SOFTWARE. */
+/* Remove the comments around the @import statement below when using this as a custom stylesheet */
+@import "https://fonts.googleapis.com/css?family=Open+Sans:300,300italic,400,400italic,600,600italic%7CNoto+Serif:400,400italic,700,700italic%7CDroid+Sans+Mono:400";
+article,aside,details,figcaption,figure,footer,header,hgroup,main,nav,section,summary{display:block}
+audio,canvas,video{display:inline-block}
+audio:not([controls]){display:none;height:0}
+[hidden],template{display:none}
+script{display:none!important}
+html{font-family:sans-serif;-ms-text-size-adjust:100%;-webkit-text-size-adjust:100%}
+body{margin:0}
+a{background:transparent}
+a:focus{outline:thin dotted}
+a:active,a:hover{outline:0}
+h1{font-size:2em;margin:.67em 0}
+abbr[title]{border-bottom:1px dotted}
+b,strong{font-weight:bold}
+dfn{font-style:italic}
+hr{-moz-box-sizing:content-box;box-sizing:content-box;height:0}
+mark{background:#ff0;color:#000}
+code,kbd,pre,samp{font-family:monospace;font-size:1em}
+pre{white-space:pre-wrap}
+q{quotes:"\201C" "\201D" "\2018" "\2019"}
+small{font-size:80%}
+sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline}
+sup{top:-.5em}
+sub{bottom:-.25em}
+img{border:0}
+svg:not(:root){overflow:hidden}
+figure{margin:0}
+fieldset{border:1px solid silver;margin:0 2px;padding:.35em .625em .75em}
+legend{border:0;padding:0}
+button,input,select,textarea{font-family:inherit;font-size:100%;margin:0}
+button,input{line-height:normal}
+button,select{text-transform:none}
+button,html input[type="button"],input[type="reset"],input[type="submit"]{-webkit-appearance:button;cursor:pointer}
+button[disabled],html input[disabled]{cursor:default}
+input[type="checkbox"],input[type="radio"]{box-sizing:border-box;padding:0}
+input[type="search"]{-webkit-appearance:textfield;-moz-box-sizing:content-box;-webkit-box-sizing:content-box;box-sizing:content-box}
+input[type="search"]::-webkit-search-cancel-button,input[type="search"]::-webkit-search-decoration{-webkit-appearance:none}
+button::-moz-focus-inner,input::-moz-focus-inner{border:0;padding:0}
+textarea{overflow:auto;vertical-align:top}
+table{border-collapse:collapse;border-spacing:0}
+*,*:before,*:after{-moz-box-sizing:border-box;-webkit-box-sizing:border-box;box-sizing:border-box}
+html,body{font-size:100%}
+body{background:#fff;color:rgba(0,0,0,.8);padding:0;margin:0;font-family:"Noto Serif","DejaVu Serif",serif;font-weight:400;font-style:normal;line-height:1;position:relative;cursor:auto}
+a:hover{cursor:pointer}
+img,object,embed{max-width:100%;height:auto}
+object,embed{height:100%}
+img{-ms-interpolation-mode:bicubic}
+#map_canvas img,#map_canvas embed,#map_canvas object,.map_canvas img,.map_canvas embed,.map_canvas object{max-width:none!important}
+.left{float:left!important}
+.right{float:right!important}
+.text-left{text-align:left!important}
+.text-right{text-align:right!important}
+.text-center{text-align:center!important}
+.text-justify{text-align:justify!important}
+.hide{display:none}
+.antialiased,body{-webkit-font-smoothing:antialiased}
+img{display:inline-block;vertical-align:middle}
+textarea{height:auto;min-height:50px}
+select{width:100%}
+p.lead,.paragraph.lead>p,#preamble>.sectionbody>.paragraph:first-of-type p{font-size:1.21875em;line-height:1.6}
+.subheader,.admonitionblock td.content>.title,.audioblock>.title,.exampleblock>.title,.imageblock>.title,.listingblock>.title,.literalblock>.title,.stemblock>.title,.openblock>.title,.paragraph>.title,.quoteblock>.title,table.tableblock>.title,.verseblock>.title,.videoblock>.title,.dlist>.title,.olist>.title,.ulist>.title,.qlist>.title,.hdlist>.title{line-height:1.45;color:#7a2518;font-weight:400;margin-top:0;margin-bottom:.25em}
+div,dl,dt,dd,ul,ol,li,h1,h2,h3,#toctitle,.sidebarblock>.content>.title,h4,h5,h6,pre,form,p,blockquote,th,td{margin:0;padding:0;direction:ltr}
+a{color:#2156a5;text-decoration:underline;line-height:inherit}
+a:hover,a:focus{color:#1d4b8f}
+a img{border:none}
+p{font-family:inherit;font-weight:400;font-size:1em;line-height:1.6;margin-bottom:1.25em;text-rendering:optimizeLegibility}
+p aside{font-size:.875em;line-height:1.35;font-style:italic}
+h1,h2,h3,#toctitle,.sidebarblock>.content>.title,h4,h5,h6{font-family:"Open Sans","DejaVu Sans",sans-serif;font-weight:300;font-style:normal;color:#ba3925;text-rendering:optimizeLegibility;margin-top:1em;margin-bottom:.5em;line-height:1.0125em}
+h1 small,h2 small,h3 small,#toctitle small,.sidebarblock>.content>.title small,h4 small,h5 small,h6 small{font-size:60%;color:#e99b8f;line-height:0}
+h1{font-size:2.125em}
+h2{font-size:1.6875em}
+h3,#toctitle,.sidebarblock>.content>.title{font-size:1.375em}
+h4,h5{font-size:1.125em}
+h6{font-size:1em}
+hr{border:solid #ddddd8;border-width:1px 0 0;clear:both;margin:1.25em 0 1.1875em;height:0}
+em,i{font-style:italic;line-height:inherit}
+strong,b{font-weight:bold;line-height:inherit}
+small{font-size:60%;line-height:inherit}
+code{font-family:"Droid Sans Mono","DejaVu Sans Mono",monospace;font-weight:400;color:rgba(0,0,0,.9);padding-right: 1px;}
+ul,ol,dl{font-size:1em;line-height:1.6;margin-bottom:1.25em;list-style-position:outside;font-family:inherit}
+ul,ol,ul.no-bullet,ol.no-bullet{margin-left:1.5em}
+ul li ul,ul li ol{margin-left:1.25em;margin-bottom:0;font-size:1em}
+ul.square li ul,ul.circle li ul,ul.disc li ul{list-style:inherit}
+ul.square{list-style-type:square}
+ul.circle{list-style-type:circle}
+ul.disc{list-style-type:disc}
+ul.no-bullet{list-style:none}
+ol li ul,ol li ol{margin-left:1.25em;margin-bottom:0}
+dl dt{margin-bottom:.3125em;font-weight:bold}
+dl dd{margin-bottom:1.25em}
+abbr,acronym{text-transform:uppercase;font-size:90%;color:rgba(0,0,0,.8);border-bottom:1px dotted #ddd;cursor:help}
+abbr{text-transform:none}
+blockquote{margin:0 0 1.25em;padding:.5625em 1.25em 0 1.1875em;border-left:1px solid #ddd}
+blockquote cite{display:block;font-size:.9375em;color:rgba(0,0,0,.6)}
+blockquote cite:before{content:"\2014 \0020"}
+blockquote cite a,blockquote cite a:visited{color:rgba(0,0,0,.6)}
+blockquote,blockquote p{line-height:1.6;color:rgba(0,0,0,.85)}
+@media only screen and (min-width:768px){h1,h2,h3,#toctitle,.sidebarblock>.content>.title,h4,h5,h6{line-height:1.2}
+h1{font-size:2.75em}
+h2{font-size:2.3125em}
+h3,#toctitle,.sidebarblock>.content>.title{font-size:1.6875em}
+h4{font-size:1.4375em}}table{background:#fff;margin-bottom:1.25em;border:solid 1px #dedede}
+table thead,table tfoot{background:#f7f8f7;font-weight:bold}
+table thead tr th,table thead tr td,table tfoot tr th,table tfoot tr td{padding:.5em .625em .625em;font-size:inherit;color:rgba(0,0,0,.8);text-align:left}
+table tr th,table tr td{padding:.5625em .625em;font-size:inherit;color:rgba(0,0,0,.8)}
+table tr.even,table tr.alt,table tr:nth-of-type(even){background:#f8f8f7}
+table thead tr th,table tfoot tr th,table tbody tr td,table tr td,table tfoot tr td{display:table-cell;line-height:1.6}
+h1,h2,h3,#toctitle,.sidebarblock>.content>.title,h4,h5,h6{line-height:1.2;word-spacing:-.05em}
+h1 strong,h2 strong,h3 strong,#toctitle strong,.sidebarblock>.content>.title strong,h4 strong,h5 strong,h6 strong{font-weight:400}
+.clearfix:before,.clearfix:after,.float-group:before,.float-group:after{content:" ";display:table}
+.clearfix:after,.float-group:after{clear:both}
+*:not(pre)>code{font-size:.9375em;font-style:normal!important;letter-spacing:0;word-spacing:-.15em;background-color:#f7f7f8;-webkit-border-radius:4px;border-radius:4px;line-height:1.45;text-rendering:optimizeSpeed}
+pre,pre>code{line-height:1.45;color:rgba(0,0,0,.9);font-family:"Droid Sans Mono","DejaVu Sans Mono",monospace;font-weight:400;text-rendering:optimizeSpeed}
+.keyseq{color:rgba(51,51,51,.8)}
+kbd{display:inline-block;color:rgba(0,0,0,.8);font-size:.75em;line-height:1.4;background-color:#f7f7f7;border:1px solid #ccc;-webkit-border-radius:3px;border-radius:3px;-webkit-box-shadow:0 1px 0 rgba(0,0,0,.2),0 0 0 .1em white inset;box-shadow:0 1px 0 rgba(0,0,0,.2),0 0 0 .1em #fff inset;margin:-.15em .15em 0 .15em;padding:.2em .6em .2em .5em;vertical-align:middle;white-space:nowrap}
+.keyseq kbd:first-child{margin-left:0}
+.keyseq kbd:last-child{margin-right:0}
+.menuseq,.menu{color:rgba(0,0,0,.8)}
+b.button:before,b.button:after{position:relative;top:-1px;font-weight:400}
+b.button:before{content:"[";padding:0 3px 0 2px}
+b.button:after{content:"]";padding:0 2px 0 3px}
+p a>code:hover{color:rgba(0,0,0,.9)}
+#header,#content,#footnotes,#footer{width:100%;margin-left:auto;margin-right:auto;margin-top:0;margin-bottom:0;max-width:62.5em;*zoom:1;position:relative;padding-left:.9375em;padding-right:.9375em}
+#header:before,#header:after,#content:before,#content:after,#footnotes:before,#footnotes:after,#footer:before,#footer:after{content:" ";display:table}
+#header:after,#content:after,#footnotes:after,#footer:after{clear:both}
+#content{margin-top:1.25em}
+#content:before{content:none}
+#header>h1:first-child{color:rgba(0,0,0,.85);margin-top:2.25rem;margin-bottom:0}
+#header>h1:first-child+#toc{margin-top:8px;border-top:1px solid #ddddd8}
+#header>h1:only-child,body.toc2 #header>h1:nth-last-child(2){border-bottom:1px solid #ddddd8;padding-bottom:8px}
+#header .details{border-bottom:1px solid #ddddd8;line-height:1.45;padding-top:.25em;padding-bottom:.25em;padding-left:.25em;color:rgba(0,0,0,.6);display:-ms-flexbox;display:-webkit-flex;display:flex;-ms-flex-flow:row wrap;-webkit-flex-flow:row wrap;flex-flow:row wrap}
+#header .details span:first-child{margin-left:-.125em}
+#header .details span.email a{color:rgba(0,0,0,.85)}
+#header .details br{display:none}
+#header .details br+span:before{content:"\00a0\2013\00a0"}
+#header .details br+span.author:before{content:"\00a0\22c5\00a0";color:rgba(0,0,0,.85)}
+#header .details br+span#revremark:before{content:"\00a0|\00a0"}
+#header #revnumber{text-transform:capitalize}
+#header #revnumber:after{content:"\00a0"}
+#content>h1:first-child:not([class]){color:rgba(0,0,0,.85);border-bottom:1px solid #ddddd8;padding-bottom:8px;margin-top:0;padding-top:1rem;margin-bottom:1.25rem}
+#toc{border-bottom:1px solid #efefed;padding-bottom:.5em}
+#toc>ul{margin-left:.125em}
+#toc ul.sectlevel0>li>a{font-style:italic}
+#toc ul.sectlevel0 ul.sectlevel1{margin:.5em 0}
+#toc ul{font-family:"Open Sans","DejaVu Sans",sans-serif;list-style-type:none}
+#toc a{text-decoration:none}
+#toc a:active{text-decoration:underline}
+#toctitle{color:#7a2518;font-size:1.2em}
+@media only screen and (min-width:768px){#toctitle{font-size:1.375em}
+body.toc2{padding-left:15em;padding-right:0}
+#toc.toc2{margin-top:0!important;background-color:#f8f8f7;position:fixed;width:15em;left:0;top:0;border-right:1px solid #efefed;border-top-width:0!important;border-bottom-width:0!important;z-index:1000;padding:1.25em 1em;height:100%;overflow:auto}
+#toc.toc2 #toctitle{margin-top:0;font-size:1.2em}
+#toc.toc2>ul{font-size:.9em;margin-bottom:0}
+#toc.toc2 ul ul{margin-left:0;padding-left:1em}
+#toc.toc2 ul.sectlevel0 ul.sectlevel1{padding-left:0;margin-top:.5em;margin-bottom:.5em}
+body.toc2.toc-right{padding-left:0;padding-right:15em}
+body.toc2.toc-right #toc.toc2{border-right-width:0;border-left:1px solid #efefed;left:auto;right:0}}@media only screen and (min-width:1280px){body.toc2{padding-left:20em;padding-right:0}
+#toc.toc2{width:20em}
+#toc.toc2 #toctitle{font-size:1.375em}
+#toc.toc2>ul{font-size:.95em}
+#toc.toc2 ul ul{padding-left:1.25em}
+body.toc2.toc-right{padding-left:0;padding-right:20em}}#content #toc{border-style:solid;border-width:1px;border-color:#e0e0dc;margin-bottom:1.25em;padding:1.25em;background:#f8f8f7;-webkit-border-radius:4px;border-radius:4px}
+#content #toc>:first-child{margin-top:0}
+#content #toc>:last-child{margin-bottom:0}
+#footer{max-width:100%;background-color:rgba(0,0,0,.8);padding:1.25em}
+#footer-text{color:rgba(255,255,255,.8);line-height:1.44}
+.sect1{padding-bottom:.625em}
+@media only screen and (min-width:768px){.sect1{padding-bottom:1.25em}}.sect1+.sect1{border-top:1px solid #efefed}
+#content h1>a.anchor,h2>a.anchor,h3>a.anchor,#toctitle>a.anchor,.sidebarblock>.content>.title>a.anchor,h4>a.anchor,h5>a.anchor,h6>a.anchor{position:absolute;z-index:1001;width:1.5ex;margin-left:-1.5ex;display:block;text-decoration:none!important;visibility:hidden;text-align:center;font-weight:400}
+#content h1>a.anchor:before,h2>a.anchor:before,h3>a.anchor:before,#toctitle>a.anchor:before,.sidebarblock>.content>.title>a.anchor:before,h4>a.anchor:before,h5>a.anchor:before,h6>a.anchor:before{content:"\00A7";font-size:.85em;display:block;padding-top:.1em}
+#content h1:hover>a.anchor,#content h1>a.anchor:hover,h2:hover>a.anchor,h2>a.anchor:hover,h3:hover>a.anchor,#toctitle:hover>a.anchor,.sidebarblock>.content>.title:hover>a.anchor,h3>a.anchor:hover,#toctitle>a.anchor:hover,.sidebarblock>.content>.title>a.anchor:hover,h4:hover>a.anchor,h4>a.anchor:hover,h5:hover>a.anchor,h5>a.anchor:hover,h6:hover>a.anchor,h6>a.anchor:hover{visibility:visible}
+#content h1>a.link,h2>a.link,h3>a.link,#toctitle>a.link,.sidebarblock>.content>.title>a.link,h4>a.link,h5>a.link,h6>a.link{color:#ba3925;text-decoration:none}
+#content h1>a.link:hover,h2>a.link:hover,h3>a.link:hover,#toctitle>a.link:hover,.sidebarblock>.content>.title>a.link:hover,h4>a.link:hover,h5>a.link:hover,h6>a.link:hover{color:#a53221}
+.audioblock,.imageblock,.literalblock,.listingblock,.stemblock,.videoblock{margin-bottom:1.25em}
+.admonitionblock td.content>.title,.audioblock>.title,.exampleblock>.title,.imageblock>.title,.listingblock>.title,.literalblock>.title,.stemblock>.title,.openblock>.title,.paragraph>.title,.quoteblock>.title,table.tableblock>.title,.verseblock>.title,.videoblock>.title,.dlist>.title,.olist>.title,.ulist>.title,.qlist>.title,.hdlist>.title{text-rendering:optimizeLegibility;text-align:left;font-family:"Noto Serif","DejaVu Serif",serif;font-size:1rem;font-style:italic}
+table.tableblock>caption.title{white-space:nowrap;overflow:visible;max-width:0}
+.paragraph.lead>p,#preamble>.sectionbody>.paragraph:first-of-type p{color:rgba(0,0,0,.85)}
+table.tableblock #preamble>.sectionbody>.paragraph:first-of-type p{font-size:inherit}
+.admonitionblock>table{border-collapse:separate;border:0;background:none;width:100%}
+.admonitionblock>table td.icon{text-align:center;width:80px}
+.admonitionblock>table td.icon img{max-width:none}
+.admonitionblock>table td.icon .title{font-weight:bold;font-family:"Open Sans","DejaVu Sans",sans-serif;text-transform:uppercase}
+.admonitionblock>table td.content{padding-left:1.125em;padding-right:1.25em;border-left:1px solid #ddddd8;color:rgba(0,0,0,.6)}
+.admonitionblock>table td.content>:last-child>:last-child{margin-bottom:0}
+.exampleblock>.content{border-style:solid;border-width:1px;border-color:#e6e6e6;margin-bottom:1.25em;padding:1.25em;background:#fff;-webkit-border-radius:4px;border-radius:4px}
+.exampleblock>.content>:first-child{margin-top:0}
+.exampleblock>.content>:last-child{margin-bottom:0}
+.sidebarblock{border-style:solid;border-width:1px;border-color:#e0e0dc;margin-bottom:1.25em;padding:1.25em;background:#f8f8f7;-webkit-border-radius:4px;border-radius:4px}
+.sidebarblock>:first-child{margin-top:0}
+.sidebarblock>:last-child{margin-bottom:0}
+.sidebarblock>.content>.title{color:#7a2518;margin-top:0;text-align:center}
+.exampleblock>.content>:last-child>:last-child,.exampleblock>.content .olist>ol>li:last-child>:last-child,.exampleblock>.content .ulist>ul>li:last-child>:last-child,.exampleblock>.content .qlist>ol>li:last-child>:last-child,.sidebarblock>.content>:last-child>:last-child,.sidebarblock>.content .olist>ol>li:last-child>:last-child,.sidebarblock>.content .ulist>ul>li:last-child>:last-child,.sidebarblock>.content .qlist>ol>li:last-child>:last-child{margin-bottom:0}
+.literalblock pre,.listingblock pre:not(.highlight),.listingblock pre[class="highlight"],.listingblock pre[class^="highlight "],.listingblock pre.CodeRay,.listingblock pre.prettyprint{background:#f7f7f8}
+.sidebarblock .literalblock pre,.sidebarblock .listingblock pre:not(.highlight),.sidebarblock .listingblock pre[class="highlight"],.sidebarblock .listingblock pre[class^="highlight "],.sidebarblock .listingblock pre.CodeRay,.sidebarblock .listingblock pre.prettyprint{background:#f2f1f1}
+.literalblock pre,.literalblock pre[class],.listingblock pre,.listingblock pre[class]{-webkit-border-radius:4px;border-radius:4px;word-wrap:break-word;padding:1em;font-size:.8125em}
+.literalblock pre.nowrap,.literalblock pre[class].nowrap,.listingblock pre.nowrap,.listingblock pre[class].nowrap{overflow-x:auto;white-space:pre;word-wrap:normal}
+@media only screen and (min-width:768px){.literalblock pre,.literalblock pre[class],.listingblock pre,.listingblock pre[class]{font-size:.90625em}}@media only screen and (min-width:1280px){.literalblock pre,.literalblock pre[class],.listingblock pre,.listingblock pre[class]{font-size:1em}}.literalblock.output pre{color:#f7f7f8;background-color:rgba(0,0,0,.9)}
+.listingblock pre.highlightjs{padding:0}
+.listingblock pre.highlightjs>code{padding:1em;-webkit-border-radius:4px;border-radius:4px}
+.listingblock pre.prettyprint{border-width:0}
+.listingblock>.content{position:relative}
+.listingblock code[data-lang]:before{display:none;content:attr(data-lang);position:absolute;font-size:.75em;top:.425rem;right:.5rem;line-height:1;text-transform:uppercase;color:#999}
+.listingblock:hover code[data-lang]:before{display:block}
+.listingblock.terminal pre .command:before{content:attr(data-prompt);padding-right:.5em;color:#999}
+.listingblock.terminal pre .command:not([data-prompt]):before{content:"$"}
+table.pyhltable{border-collapse:separate;border:0;margin-bottom:0;background:none}
+table.pyhltable td{vertical-align:top;padding-top:0;padding-bottom:0}
+table.pyhltable td.code{padding-left:.75em;padding-right:0}
+pre.pygments .lineno,table.pyhltable td:not(.code){color:#999;padding-left:0;padding-right:.5em;border-right:1px solid #ddddd8}
+pre.pygments .lineno{display:inline-block;margin-right:.25em}
+table.pyhltable .linenodiv{background:none!important;padding-right:0!important}
+.quoteblock{margin:0 1em 1.25em 1.5em;display:table}
+.quoteblock>.title{margin-left:-1.5em;margin-bottom:.75em}
+.quoteblock blockquote,.quoteblock blockquote p{color:rgba(0,0,0,.85);font-size:1.15rem;line-height:1.75;word-spacing:.1em;letter-spacing:0;font-style:italic;text-align:justify}
+.quoteblock blockquote{margin:0;padding:0;border:0}
+.quoteblock blockquote:before{content:"\201c";float:left;font-size:2.75em;font-weight:bold;line-height:.6em;margin-left:-.6em;color:#7a2518;text-shadow:0 1px 2px rgba(0,0,0,.1)}
+.quoteblock blockquote>.paragraph:last-child p{margin-bottom:0}
+.quoteblock .attribution{margin-top:.5em;margin-right:.5ex;text-align:right}
+.quoteblock .quoteblock{margin-left:0;margin-right:0;padding:.5em 0;border-left:3px solid rgba(0,0,0,.6)}
+.quoteblock .quoteblock blockquote{padding:0 0 0 .75em}
+.quoteblock .quoteblock blockquote:before{display:none}
+.verseblock{margin:0 1em 1.25em 1em}
+.verseblock pre{font-family:"Open Sans","DejaVu Sans",sans;font-size:1.15rem;color:rgba(0,0,0,.85);font-weight:300;text-rendering:optimizeLegibility}
+.verseblock pre strong{font-weight:400}
+.verseblock .attribution{margin-top:1.25rem;margin-left:.5ex}
+.quoteblock .attribution,.verseblock .attribution{font-size:.9375em;line-height:1.45;font-style:italic}
+.quoteblock .attribution br,.verseblock .attribution br{display:none}
+.quoteblock .attribution cite,.verseblock .attribution cite{display:block;letter-spacing:-.05em;color:rgba(0,0,0,.6)}
+.quoteblock.abstract{margin:0 0 1.25em 0;display:block}
+.quoteblock.abstract blockquote,.quoteblock.abstract blockquote p{text-align:left;word-spacing:0}
+.quoteblock.abstract blockquote:before,.quoteblock.abstract blockquote p:first-of-type:before{display:none}
+table.tableblock{max-width:100%;border-collapse:separate}
+table.tableblock td>.paragraph:last-child p>p:last-child,table.tableblock th>p:last-child,table.tableblock td>p:last-child{margin-bottom:0}
+table.spread{width:100%}
+table.tableblock,th.tableblock,td.tableblock{border:0 solid #dedede}
+table.grid-all th.tableblock,table.grid-all td.tableblock{border-width:0 1px 1px 0}
+table.grid-all tfoot>tr>th.tableblock,table.grid-all tfoot>tr>td.tableblock{border-width:1px 1px 0 0}
+table.grid-cols th.tableblock,table.grid-cols td.tableblock{border-width:0 1px 0 0}
+table.grid-all *>tr>.tableblock:last-child,table.grid-cols *>tr>.tableblock:last-child{border-right-width:0}
+table.grid-rows th.tableblock,table.grid-rows td.tableblock{border-width:0 0 1px 0}
+table.grid-all tbody>tr:last-child>th.tableblock,table.grid-all tbody>tr:last-child>td.tableblock,table.grid-all thead:last-child>tr>th.tableblock,table.grid-rows tbody>tr:last-child>th.tableblock,table.grid-rows tbody>tr:last-child>td.tableblock,table.grid-rows thead:last-child>tr>th.tableblock{border-bottom-width:0}
+table.grid-rows tfoot>tr>th.tableblock,table.grid-rows tfoot>tr>td.tableblock{border-width:1px 0 0 0}
+table.frame-all{border-width:1px}
+table.frame-sides{border-width:0 1px}
+table.frame-topbot{border-width:1px 0}
+th.halign-left,td.halign-left{text-align:left}
+th.halign-right,td.halign-right{text-align:right}
+th.halign-center,td.halign-center{text-align:center}
+th.valign-top,td.valign-top{vertical-align:top}
+th.valign-bottom,td.valign-bottom{vertical-align:bottom}
+th.valign-middle,td.valign-middle{vertical-align:middle}
+table thead th,table tfoot th{font-weight:bold}
+tbody tr th{display:table-cell;line-height:1.6;background:#f7f8f7}
+tbody tr th,tbody tr th p,tfoot tr th,tfoot tr th p{color:rgba(0,0,0,.8);font-weight:bold}
+p.tableblock>code:only-child{background:none;padding:0}
+p.tableblock{font-size:1em}
+td>div.verse{white-space:pre}
+ol{margin-left:1.75em}
+ul li ol{margin-left:1.5em}
+dl dd{margin-left:1.125em}
+dl dd:last-child,dl dd:last-child>:last-child{margin-bottom:0}
+ol>li p,ul>li p,ul dd,ol dd,.olist .olist,.ulist .ulist,.ulist .olist,.olist .ulist{margin-bottom:.625em}
+ul.unstyled,ol.unnumbered,ul.checklist,ul.none{list-style-type:none}
+ul.unstyled,ol.unnumbered,ul.checklist{margin-left:.625em}
+ul.checklist li>p:first-child>.fa-square-o:first-child,ul.checklist li>p:first-child>.fa-check-square-o:first-child{width:1em;font-size:.85em}
+ul.checklist li>p:first-child>input[type="checkbox"]:first-child{width:1em;position:relative;top:1px}
+ul.inline{margin:0 auto .625em auto;margin-left:-1.375em;margin-right:0;padding:0;list-style:none;overflow:hidden}
+ul.inline>li{list-style:none;float:left;margin-left:1.375em;display:block}
+ul.inline>li>*{display:block}
+.unstyled dl dt{font-weight:400;font-style:normal}
+ol.arabic{list-style-type:decimal}
+ol.decimal{list-style-type:decimal-leading-zero}
+ol.loweralpha{list-style-type:lower-alpha}
+ol.upperalpha{list-style-type:upper-alpha}
+ol.lowerroman{list-style-type:lower-roman}
+ol.upperroman{list-style-type:upper-roman}
+ol.lowergreek{list-style-type:lower-greek}
+.hdlist>table,.colist>table{border:0;background:none}
+.hdlist>table>tbody>tr,.colist>table>tbody>tr{background:none}
+td.hdlist1{padding-right:.75em;font-weight:bold}
+td.hdlist1,td.hdlist2{vertical-align:top}
+.literalblock+.colist,.listingblock+.colist{margin-top:-.5em}
+.colist>table tr>td:first-of-type{padding:0 .75em;line-height:1}
+.colist>table tr>td:last-of-type{padding:.25em 0}
+.thumb,.th{line-height:0;display:inline-block;border:solid 4px #fff;-webkit-box-shadow:0 0 0 1px #ddd;box-shadow:0 0 0 1px #ddd}
+.imageblock.left,.imageblock[style*="float: left"]{margin:.25em .625em 1.25em 0}
+.imageblock.right,.imageblock[style*="float: right"]{margin:.25em 0 1.25em .625em}
+.imageblock>.title{margin-bottom:0}
+.imageblock.thumb,.imageblock.th{border-width:6px}
+.imageblock.thumb>.title,.imageblock.th>.title{padding:0 .125em}
+.image.left,.image.right{margin-top:.25em;margin-bottom:.25em;display:inline-block;line-height:0}
+.image.left{margin-right:.625em}
+.image.right{margin-left:.625em}
+a.image{text-decoration:none}
+span.footnote,span.footnoteref{vertical-align:super;font-size:.875em}
+span.footnote a,span.footnoteref a{text-decoration:none}
+span.footnote a:active,span.footnoteref a:active{text-decoration:underline}
+#footnotes{padding-top:.75em;padding-bottom:.75em;margin-bottom:.625em}
+#footnotes hr{width:20%;min-width:6.25em;margin:-.25em 0 .75em 0;border-width:1px 0 0 0}
+#footnotes .footnote{padding:0 .375em;line-height:1.3;font-size:.875em;margin-left:1.2em;text-indent:-1.2em;margin-bottom:.2em}
+#footnotes .footnote a:first-of-type{font-weight:bold;text-decoration:none}
+#footnotes .footnote:last-of-type{margin-bottom:0}
+#content #footnotes{margin-top:-.625em;margin-bottom:0;padding:.75em 0}
+.gist .file-data>table{border:0;background:#fff;width:100%;margin-bottom:0}
+.gist .file-data>table td.line-data{width:99%}
+div.unbreakable{page-break-inside:avoid}
+.big{font-size:larger}
+.small{font-size:smaller}
+.underline{text-decoration:underline}
+.overline{text-decoration:overline}
+.line-through{text-decoration:line-through}
+.aqua{color:#00bfbf}
+.aqua-background{background-color:#00fafa}
+.black{color:#000}
+.black-background{background-color:#000}
+.blue{color:#0000bf}
+.blue-background{background-color:#0000fa}
+.fuchsia{color:#bf00bf}
+.fuchsia-background{background-color:#fa00fa}
+.gray{color:#606060}
+.gray-background{background-color:#7d7d7d}
+.green{color:#006000}
+.green-background{background-color:#007d00}
+.lime{color:#00bf00}
+.lime-background{background-color:#00fa00}
+.maroon{color:#600000}
+.maroon-background{background-color:#7d0000}
+.navy{color:#000060}
+.navy-background{background-color:#00007d}
+.olive{color:#606000}
+.olive-background{background-color:#7d7d00}
+.purple{color:#600060}
+.purple-background{background-color:#7d007d}
+.red{color:#bf0000}
+.red-background{background-color:#fa0000}
+.silver{color:#909090}
+.silver-background{background-color:#bcbcbc}
+.teal{color:#006060}
+.teal-background{background-color:#007d7d}
+.white{color:#bfbfbf}
+.white-background{background-color:#fafafa}
+.yellow{color:#bfbf00}
+.yellow-background{background-color:#fafa00}
+span.icon>.fa{cursor:default}
+.admonitionblock td.icon [class^="fa icon-"]{font-size:2.5em;text-shadow:1px 1px 2px rgba(0,0,0,.5);cursor:default}
+.admonitionblock td.icon .icon-note:before{content:"\f05a";color:#19407c}
+.admonitionblock td.icon .icon-tip:before{content:"\f0eb";text-shadow:1px 1px 2px rgba(155,155,0,.8);color:#111}
+.admonitionblock td.icon .icon-warning:before{content:"\f071";color:#bf6900}
+.admonitionblock td.icon .icon-caution:before{content:"\f06d";color:#bf3400}
+.admonitionblock td.icon .icon-important:before{content:"\f06a";color:#bf0000}
+.conum[data-value]{display:inline-block;color:#fff!important;background-color:rgba(0,0,0,.8);-webkit-border-radius:100px;border-radius:100px;text-align:center;font-size:.75em;width:1.67em;height:1.67em;line-height:1.67em;font-family:"Open Sans","DejaVu Sans",sans-serif;font-style:normal;font-weight:bold}
+.conum[data-value] *{color:#fff!important}
+.conum[data-value]+b{display:none}
+.conum[data-value]:after{content:attr(data-value)}
+pre .conum[data-value]{position:relative;top:-.125em}
+b.conum *{color:inherit!important}
+.conum:not([data-value]):empty{display:none}
+h1,h2{letter-spacing:-.01em}
+dt,th.tableblock,td.content{text-rendering:optimizeLegibility}
+p,td.content{letter-spacing:-.01em}
+p strong,td.content strong{letter-spacing:-.005em}
+p,blockquote,dt,td.content{font-size:1.0625rem}
+p{margin-bottom:1.25rem}
+.sidebarblock p,.sidebarblock dt,.sidebarblock td.content,p.tableblock{font-size:1em}
+.exampleblock>.content{background-color:#fffef7;border-color:#e0e0dc;-webkit-box-shadow:0 1px 4px #e0e0dc;box-shadow:0 1px 4px #e0e0dc}
+.print-only{display:none!important}
+@media print{@page{margin:1.25cm .75cm}
+*{-webkit-box-shadow:none!important;box-shadow:none!important;text-shadow:none!important}
+a{color:inherit!important;text-decoration:underline!important}
+a.bare,a[href^="#"],a[href^="mailto:"]{text-decoration:none!important}
+a[href^="http:"]:not(.bare):after,a[href^="https:"]:not(.bare):after{content:"(" attr(href) ")";display:inline-block;font-size:.875em;padding-left:.25em}
+abbr[title]:after{content:" (" attr(title) ")"}
+pre,blockquote,tr,img{page-break-inside:avoid}
+thead{display:table-header-group}
+img{max-width:100%!important}
+p,blockquote,dt,td.content{font-size:1em;orphans:3;widows:3}
+h2,h3,#toctitle,.sidebarblock>.content>.title{page-break-after:avoid}
+#toc,.sidebarblock,.exampleblock>.content{background:none!important}
+#toc{border-bottom:1px solid #ddddd8!important;padding-bottom:0!important}
+.sect1{padding-bottom:0!important}
+.sect1+.sect1{border:0!important}
+#header>h1:first-child{margin-top:1.25rem}
+body.book #header{text-align:center}
+body.book #header>h1:first-child{border:0!important;margin:2.5em 0 1em 0}
+body.book #header .details{border:0!important;display:block;padding:0!important}
+body.book #header .details span:first-child{margin-left:0!important}
+body.book #header .details br{display:block}
+body.book #header .details br+span:before{content:none!important}
+body.book #toc{border:0!important;text-align:left!important;padding:0!important;margin:0!important}
+body.book #toc,body.book #preamble,body.book h1.sect0,body.book .sect1>h2{page-break-before:always}
+.listingblock code[data-lang]:before{display:block}
+#footer{background:none!important;padding:0 .9375em}
+#footer-text{color:rgba(0,0,0,.6)!important;font-size:.9em}
+.hide-on-print{display:none!important}
+.print-only{display:block!important}
+.hide-for-print{display:none!important}
+.show-for-print{display:inherit!important}}
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/6f26290d/nifi-registry-core/nifi-registry-docs/src/main/asciidoc/getting-started.adoc
----------------------------------------------------------------------
diff --git a/nifi-registry-core/nifi-registry-docs/src/main/asciidoc/getting-started.adoc b/nifi-registry-core/nifi-registry-docs/src/main/asciidoc/getting-started.adoc
new file mode 100644
index 0000000..7472306
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-docs/src/main/asciidoc/getting-started.adoc
@@ -0,0 +1,171 @@
+//
+// 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.
+//
+= Getting Started with Apache NiFi Registry
+Apache NiFi Team <de...@nifi.apache.org>
+:homepage: http://nifi.apache.org
+:linkattrs:
+
+
+== Who is This Guide For?
+This guide is written for users who have basic experience with NiFi but have little familiarity with the NiFi Registry. This guide is not intended to be an exhaustive instruction manual or a reference guide. The link:user-guide.html[NiFi Registry User Guide] and link:https://nifi.apache.org/docs/nifi-docs/html/user-guide.html[NiFi User Guide^] provide a great deal more information about using the Registry and integrating it with NiFi. This guide, in comparison, is intended to provide users with just the information needed in order to understand how to configure NiFi Registry, connect with NiFi and start using versioned NiFi dataflows.
+
+
+== Terminology Used in This Guide
+In order to talk about NiFi Registry, there are a few key terms that readers should be familiar with:
+
+*Flow*: A process group level NiFi dataflow that has been placed under version control and saved to the Registry.
+
+*Bucket*: A container that stores and organizes flows.
+
+
+== Downloading and Installing NiFi Registry
+NiFi Registry can be downloaded from the link:https://nifi.apache.org/registry.html[NiFi Registry Page^]. There are two packaging options available: a tarball and a zip file.  Supported operating systems include Linux, Unix and Mac OS X.
+
+For users who are not running OS X, after downloading NiFi Registry simply extract the archive to the location that you wish to run the application from. The registry is unsecured by default.
+
+For information on how to configure an instance of NiFi Registry (for example, to implement security or change the port that NiFi Registry is running on), see the link:administration-guide.html[System Administrator's Guide].
+
+
+== Starting NiFi Registry
+Once NiFi Registry has been downloaded and installed as described above, it can be started by using the mechanism appropriate for your operating system.
+
+
+=== For Linux/Unix/Mac OS X users
+Use a Terminal window to navigate to the directory where NiFi Registry was installed. To run NiFi Registry in the foreground, run `bin/nifi-registry.sh run`. This will leave the application running until the user presses Ctrl-C. At that time, it will initiate shutdown of the application.
+
+To run NiFi Registry in the background, instead run `bin/nifi-registry.sh start`. This will initiate the application to begin running. To check the status and see if NiFi Registry is currently running, execute the command `bin/nifi-registry.sh status`.
+NiFi Registry can be shutdown by executing the command `bin/nifi-registry.sh stop`.
+
+
+=== Installing as a Service
+To install the application as a service, navigate to the installation directory in a Terminal window and execute the command `bin/nifi-registry.sh install` to install the service with the default name `nifi-registry`. To specify a custom name for the service, execute the command with an optional second argument that is the name of the service. For example, to install NiFi Registry as a service with the name `flow-registry`, use the command `bin/nifi-registry.sh install flow-registry`.
+
+Once installed, the service can be started and stopped using the appropriate commands, such as `sudo service nifi-registry start` and `sudo service nifi-registry stop`. Additionally, the running status can be checked via `sudo service nifi-registry status`.
+
+
+== I Started NiFi Registry. Now What?
+Now that NiFi Registry has been started, we can bring up the User Interface (UI).  To get started, open a web browser and navigate to
+link:http://localhost:18080/nifi-registry[`http://localhost:18080/nifi-registry`^]. The port can be changed by editing the `nifi-registry.properties` file in the NiFi Registry _conf_ directory, but the default port is `18080`.
+
+This will bring up the Registry UI, which at this point is empty as there are no flow resources available to share yet:
+
+image:empty_registry.png["Empty Registry"]
+
+
+=== Create a Bucket
+A bucket is needed in our registry to store and organize NiFi dataflows.  To create one, select the Settings icon (image:iconSettings.png["Settings Icon"])in the top right corner of the screen. In the Buckets window, select the "New Bucket" button.
+
+image::new_test_bucket.png["New Bucket"]
+
+Enter the bucket name "Test" and select the "Create" button.
+
+image::test_bucket_dialog.png["Test Bucket Dialog"]
+
+The "Test" bucket is created:
+
+image:test_bucket.png["Test Bucket"]
+
+There are no permissions configured by default, so anyone is able to view, create and modify buckets in this instance. For information on securing the system, see the link:administration-guide.html[System Administrator's Guide].
+
+
+=== Connect NiFi to the Registry
+Now it is time to tell NiFi about the local registry instance.
+
+Start a NiFi instance if one isn't already running and bring up the UI.  Go to  controller settings from the top-right menu:
+
+image::controller-settings-selection.png["Global Menu - Controller Settings"]
+
+Select the Registry Clients tab and add a new Registry Client giving it a name and the URL of link:http://localhost:18080[`http://localhost:18080`^]:
+
+image::local_registry.png["Local Registry Client"]
+
+
+=== Start Version Control on a Process Group
+NiFi can now place a process group under version control.
+
+Right-click on a process group and select "Version->Start version control" from the context menu:
+
+image::ABCD_process_group_menu.png["ABCD Process Group Menu"]
+
+The local registry instance and "Test" bucket are chosen by default to store your flow since they are the only registry connected and bucket available.  Enter a flow name, flow description, comments and select "Save":
+
+image::save_ABCD_flow_dialog.png["Initial Save of ABCD Flow"]
+
+As indicated by the Version State icon (image:iconUpToDate.png["Up To Date Icon"]) in the top left corner of the component, the process group is now saved as a versioned flow in the registry.
+
+image::ABCD_flow_saved.png["ABCD Flow Saved"]
+
+Go back to the Registry UI and return to the main page to see
+the versioned flow you just saved (a refresh may be required):
+
+image::ABCD_flow_in_test_bucket.png["ABCD Flow in Test Bucket"]
+
+
+=== Save Changes to a Versioned Flow
+Changes made to the versioned process group can be reviewed, reverted or saved.
+
+For example, if changes are made to the ABCD flow, the Version State changes to "Locally modified" (image:iconLocallyModified.png["Locally Modified Icon"]). The right-click menu will now show the options "Commit local changes", "Show local changes" or "Revert local changes":
+
+image::changed_flow_options.png["Changed Flow Options"]
+
+Select "Show local changes" to see the details of the changes made:
+
+image::ABCD_flow_changes.png["Show ABCD Flow Changes"]
+
+Select "Commit local changes", enter comments and select "Save" to save the changes:
+
+image::ABCD_save_flow_version_2.png["Save ABCD Version 2"]
+
+Version 2 of the flow is saved:
+
+image::ABCD_version_2.png["ABCD Version 2"]
+
+
+=== Import a Versioned Flow
+With a flow existing in the registry, we can use it to illustrate how to import a versioned process group.
+
+In NiFi, select Process Group from the Components toolbar and drag it onto the canvas:
+
+image::drag_process_group.png["Drag Process Group"]
+
+Instead of entering a name, click the Import link:
+
+image::import_flow_from_registry.png["Import Flow From Registry"]
+
+Choose the version of the flow you want imported and select "Import":
+
+image:import_ABCD_version_2.png["Import ABCD Version 2"]
+
+A second identical PG is now added:
+
+image::two_ABCD_flows.png["Two ABCD Flow on Canvas"]
+
+
+== Where To Go For More Information
+In addition to this Getting Started Guide, more information about NiFi Registry and related features in NiFi can be found in the following guides:
+
+- link:user-guide.html[Apache NiFi Registry User Guide] - This guide provides information on how to navigate the Registry UI and explains in detail how to manage flows/policies/special privileges and configure users/groups when the Registry is secured.
+- link:administration-guide.html[Apache NiFi Registry System Administrator's Guide] - A guide for setting up and administering Apache NiFi Registry. Topics covered include: system requirements, security configuration, user authentication, authorization, proxy configuration and details about the different system-level settings.
+- link:https://nifi.apache.org/docs/nifi-docs/html/user-guide.html[Apache NiFi User Guide^] - A fairly extensive guide that is often used more as a Reference Guide, as it provides information on each of the different components available in NiFi and explains how to use the different features provided by the application. It includes the section "Versioning a Dataflow" which covers the integration of NiFi with NiFi Registry. Topics covered include: connecting to a registry, version states, importing a versioned flow and managing local changes.
+- link:https://cwiki.apache.org/confluence/display/NIFI/Contributor+Guide[Contributor's Guide^] - A guide for explaining how to contribute work back to the Apache NiFi community so that others can make use of it.
+
+In addition to the guides provided here, you can browse the different
+link:https://nifi.apache.org/mailing_lists.html[NiFi Mailing Lists^] or send an e-mail to one of the mailing lists at
+link:mailto:users@nifi.apache.org[users@nifi.apache.org] or
+link:mailto:dev@nifi.apache.org[dev@nifi.apache.org].
+
+Many of the members of the NiFi community are also available on Twitter and actively monitor for tweets that mention @apachenifi.

http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/6f26290d/nifi-registry-core/nifi-registry-docs/src/main/asciidoc/images/ABCD_flow_changes.png
----------------------------------------------------------------------
diff --git a/nifi-registry-core/nifi-registry-docs/src/main/asciidoc/images/ABCD_flow_changes.png b/nifi-registry-core/nifi-registry-docs/src/main/asciidoc/images/ABCD_flow_changes.png
new file mode 100644
index 0000000..14b7d4d
Binary files /dev/null and b/nifi-registry-core/nifi-registry-docs/src/main/asciidoc/images/ABCD_flow_changes.png differ

http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/6f26290d/nifi-registry-core/nifi-registry-docs/src/main/asciidoc/images/ABCD_flow_in_test_bucket.png
----------------------------------------------------------------------
diff --git a/nifi-registry-core/nifi-registry-docs/src/main/asciidoc/images/ABCD_flow_in_test_bucket.png b/nifi-registry-core/nifi-registry-docs/src/main/asciidoc/images/ABCD_flow_in_test_bucket.png
new file mode 100644
index 0000000..da65abb
Binary files /dev/null and b/nifi-registry-core/nifi-registry-docs/src/main/asciidoc/images/ABCD_flow_in_test_bucket.png differ

http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/6f26290d/nifi-registry-core/nifi-registry-docs/src/main/asciidoc/images/ABCD_flow_saved.png
----------------------------------------------------------------------
diff --git a/nifi-registry-core/nifi-registry-docs/src/main/asciidoc/images/ABCD_flow_saved.png b/nifi-registry-core/nifi-registry-docs/src/main/asciidoc/images/ABCD_flow_saved.png
new file mode 100644
index 0000000..3d1f714
Binary files /dev/null and b/nifi-registry-core/nifi-registry-docs/src/main/asciidoc/images/ABCD_flow_saved.png differ

http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/6f26290d/nifi-registry-core/nifi-registry-docs/src/main/asciidoc/images/ABCD_process_group_menu.png
----------------------------------------------------------------------
diff --git a/nifi-registry-core/nifi-registry-docs/src/main/asciidoc/images/ABCD_process_group_menu.png b/nifi-registry-core/nifi-registry-docs/src/main/asciidoc/images/ABCD_process_group_menu.png
new file mode 100644
index 0000000..ace96ca
Binary files /dev/null and b/nifi-registry-core/nifi-registry-docs/src/main/asciidoc/images/ABCD_process_group_menu.png differ

http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/6f26290d/nifi-registry-core/nifi-registry-docs/src/main/asciidoc/images/ABCD_save_flow_version_2.png
----------------------------------------------------------------------
diff --git a/nifi-registry-core/nifi-registry-docs/src/main/asciidoc/images/ABCD_save_flow_version_2.png b/nifi-registry-core/nifi-registry-docs/src/main/asciidoc/images/ABCD_save_flow_version_2.png
new file mode 100644
index 0000000..7f8c772
Binary files /dev/null and b/nifi-registry-core/nifi-registry-docs/src/main/asciidoc/images/ABCD_save_flow_version_2.png differ

http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/6f26290d/nifi-registry-core/nifi-registry-docs/src/main/asciidoc/images/ABCD_version_2.png
----------------------------------------------------------------------
diff --git a/nifi-registry-core/nifi-registry-docs/src/main/asciidoc/images/ABCD_version_2.png b/nifi-registry-core/nifi-registry-docs/src/main/asciidoc/images/ABCD_version_2.png
new file mode 100644
index 0000000..870ed02
Binary files /dev/null and b/nifi-registry-core/nifi-registry-docs/src/main/asciidoc/images/ABCD_version_2.png differ

http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/6f26290d/nifi-registry-core/nifi-registry-docs/src/main/asciidoc/images/add_user_button.png
----------------------------------------------------------------------
diff --git a/nifi-registry-core/nifi-registry-docs/src/main/asciidoc/images/add_user_button.png b/nifi-registry-core/nifi-registry-docs/src/main/asciidoc/images/add_user_button.png
new file mode 100644
index 0000000..69b29fa
Binary files /dev/null and b/nifi-registry-core/nifi-registry-docs/src/main/asciidoc/images/add_user_button.png differ

http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/6f26290d/nifi-registry-core/nifi-registry-docs/src/main/asciidoc/images/add_user_dialog.png
----------------------------------------------------------------------
diff --git a/nifi-registry-core/nifi-registry-docs/src/main/asciidoc/images/add_user_dialog.png b/nifi-registry-core/nifi-registry-docs/src/main/asciidoc/images/add_user_dialog.png
new file mode 100644
index 0000000..0e1d6ea
Binary files /dev/null and b/nifi-registry-core/nifi-registry-docs/src/main/asciidoc/images/add_user_dialog.png differ

http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/6f26290d/nifi-registry-core/nifi-registry-docs/src/main/asciidoc/images/add_user_to_groups_dialog.png
----------------------------------------------------------------------
diff --git a/nifi-registry-core/nifi-registry-docs/src/main/asciidoc/images/add_user_to_groups_dialog.png b/nifi-registry-core/nifi-registry-docs/src/main/asciidoc/images/add_user_to_groups_dialog.png
new file mode 100644
index 0000000..8890bbb
Binary files /dev/null and b/nifi-registry-core/nifi-registry-docs/src/main/asciidoc/images/add_user_to_groups_dialog.png differ

http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/6f26290d/nifi-registry-core/nifi-registry-docs/src/main/asciidoc/images/bucket_menu.png
----------------------------------------------------------------------
diff --git a/nifi-registry-core/nifi-registry-docs/src/main/asciidoc/images/bucket_menu.png b/nifi-registry-core/nifi-registry-docs/src/main/asciidoc/images/bucket_menu.png
new file mode 100644
index 0000000..29a52dd
Binary files /dev/null and b/nifi-registry-core/nifi-registry-docs/src/main/asciidoc/images/bucket_menu.png differ

http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/6f26290d/nifi-registry-core/nifi-registry-docs/src/main/asciidoc/images/bucket_nav_name_edit.png
----------------------------------------------------------------------
diff --git a/nifi-registry-core/nifi-registry-docs/src/main/asciidoc/images/bucket_nav_name_edit.png b/nifi-registry-core/nifi-registry-docs/src/main/asciidoc/images/bucket_nav_name_edit.png
new file mode 100644
index 0000000..8724e78
Binary files /dev/null and b/nifi-registry-core/nifi-registry-docs/src/main/asciidoc/images/bucket_nav_name_edit.png differ

http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/6f26290d/nifi-registry-core/nifi-registry-docs/src/main/asciidoc/images/buckets_filter_by_name.png
----------------------------------------------------------------------
diff --git a/nifi-registry-core/nifi-registry-docs/src/main/asciidoc/images/buckets_filter_by_name.png b/nifi-registry-core/nifi-registry-docs/src/main/asciidoc/images/buckets_filter_by_name.png
new file mode 100644
index 0000000..ed1c65a
Binary files /dev/null and b/nifi-registry-core/nifi-registry-docs/src/main/asciidoc/images/buckets_filter_by_name.png differ

http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/6f26290d/nifi-registry-core/nifi-registry-docs/src/main/asciidoc/images/buckets_sort_by_name.png
----------------------------------------------------------------------
diff --git a/nifi-registry-core/nifi-registry-docs/src/main/asciidoc/images/buckets_sort_by_name.png b/nifi-registry-core/nifi-registry-docs/src/main/asciidoc/images/buckets_sort_by_name.png
new file mode 100644
index 0000000..4d40f9a
Binary files /dev/null and b/nifi-registry-core/nifi-registry-docs/src/main/asciidoc/images/buckets_sort_by_name.png differ

http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/6f26290d/nifi-registry-core/nifi-registry-docs/src/main/asciidoc/images/changed_flow_options.png
----------------------------------------------------------------------
diff --git a/nifi-registry-core/nifi-registry-docs/src/main/asciidoc/images/changed_flow_options.png b/nifi-registry-core/nifi-registry-docs/src/main/asciidoc/images/changed_flow_options.png
new file mode 100644
index 0000000..88f6abb
Binary files /dev/null and b/nifi-registry-core/nifi-registry-docs/src/main/asciidoc/images/changed_flow_options.png differ

http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/6f26290d/nifi-registry-core/nifi-registry-docs/src/main/asciidoc/images/check_multiple_buckets.png
----------------------------------------------------------------------
diff --git a/nifi-registry-core/nifi-registry-docs/src/main/asciidoc/images/check_multiple_buckets.png b/nifi-registry-core/nifi-registry-docs/src/main/asciidoc/images/check_multiple_buckets.png
new file mode 100644
index 0000000..cf08d44
Binary files /dev/null and b/nifi-registry-core/nifi-registry-docs/src/main/asciidoc/images/check_multiple_buckets.png differ

http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/6f26290d/nifi-registry-core/nifi-registry-docs/src/main/asciidoc/images/check_multiple_users.png
----------------------------------------------------------------------
diff --git a/nifi-registry-core/nifi-registry-docs/src/main/asciidoc/images/check_multiple_users.png b/nifi-registry-core/nifi-registry-docs/src/main/asciidoc/images/check_multiple_users.png
new file mode 100644
index 0000000..230b5b6
Binary files /dev/null and b/nifi-registry-core/nifi-registry-docs/src/main/asciidoc/images/check_multiple_users.png differ

http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/6f26290d/nifi-registry-core/nifi-registry-docs/src/main/asciidoc/images/controller-settings-selection.png
----------------------------------------------------------------------
diff --git a/nifi-registry-core/nifi-registry-docs/src/main/asciidoc/images/controller-settings-selection.png b/nifi-registry-core/nifi-registry-docs/src/main/asciidoc/images/controller-settings-selection.png
new file mode 100644
index 0000000..80dca40
Binary files /dev/null and b/nifi-registry-core/nifi-registry-docs/src/main/asciidoc/images/controller-settings-selection.png differ

http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/6f26290d/nifi-registry-core/nifi-registry-docs/src/main/asciidoc/images/create_new_group.png
----------------------------------------------------------------------
diff --git a/nifi-registry-core/nifi-registry-docs/src/main/asciidoc/images/create_new_group.png b/nifi-registry-core/nifi-registry-docs/src/main/asciidoc/images/create_new_group.png
new file mode 100644
index 0000000..288c588
Binary files /dev/null and b/nifi-registry-core/nifi-registry-docs/src/main/asciidoc/images/create_new_group.png differ

http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/6f26290d/nifi-registry-core/nifi-registry-docs/src/main/asciidoc/images/create_new_group_dialog.png
----------------------------------------------------------------------
diff --git a/nifi-registry-core/nifi-registry-docs/src/main/asciidoc/images/create_new_group_dialog.png b/nifi-registry-core/nifi-registry-docs/src/main/asciidoc/images/create_new_group_dialog.png
new file mode 100644
index 0000000..f29fc9f
Binary files /dev/null and b/nifi-registry-core/nifi-registry-docs/src/main/asciidoc/images/create_new_group_dialog.png differ

http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/6f26290d/nifi-registry-core/nifi-registry-docs/src/main/asciidoc/images/delete_bucket_dialog.png
----------------------------------------------------------------------
diff --git a/nifi-registry-core/nifi-registry-docs/src/main/asciidoc/images/delete_bucket_dialog.png b/nifi-registry-core/nifi-registry-docs/src/main/asciidoc/images/delete_bucket_dialog.png
new file mode 100644
index 0000000..7e43135
Binary files /dev/null and b/nifi-registry-core/nifi-registry-docs/src/main/asciidoc/images/delete_bucket_dialog.png differ

http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/6f26290d/nifi-registry-core/nifi-registry-docs/src/main/asciidoc/images/delete_bucket_policy.png
----------------------------------------------------------------------
diff --git a/nifi-registry-core/nifi-registry-docs/src/main/asciidoc/images/delete_bucket_policy.png b/nifi-registry-core/nifi-registry-docs/src/main/asciidoc/images/delete_bucket_policy.png
new file mode 100644
index 0000000..35e56a9
Binary files /dev/null and b/nifi-registry-core/nifi-registry-docs/src/main/asciidoc/images/delete_bucket_policy.png differ

http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/6f26290d/nifi-registry-core/nifi-registry-docs/src/main/asciidoc/images/delete_bucket_policy_dialog.png
----------------------------------------------------------------------
diff --git a/nifi-registry-core/nifi-registry-docs/src/main/asciidoc/images/delete_bucket_policy_dialog.png b/nifi-registry-core/nifi-registry-docs/src/main/asciidoc/images/delete_bucket_policy_dialog.png
new file mode 100644
index 0000000..6b6e237
Binary files /dev/null and b/nifi-registry-core/nifi-registry-docs/src/main/asciidoc/images/delete_bucket_policy_dialog.png differ

http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/6f26290d/nifi-registry-core/nifi-registry-docs/src/main/asciidoc/images/delete_bucket_single.png
----------------------------------------------------------------------
diff --git a/nifi-registry-core/nifi-registry-docs/src/main/asciidoc/images/delete_bucket_single.png b/nifi-registry-core/nifi-registry-docs/src/main/asciidoc/images/delete_bucket_single.png
new file mode 100644
index 0000000..9fc4a2a
Binary files /dev/null and b/nifi-registry-core/nifi-registry-docs/src/main/asciidoc/images/delete_bucket_single.png differ

http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/6f26290d/nifi-registry-core/nifi-registry-docs/src/main/asciidoc/images/delete_buckets_dialog.png
----------------------------------------------------------------------
diff --git a/nifi-registry-core/nifi-registry-docs/src/main/asciidoc/images/delete_buckets_dialog.png b/nifi-registry-core/nifi-registry-docs/src/main/asciidoc/images/delete_buckets_dialog.png
new file mode 100644
index 0000000..cc55a83
Binary files /dev/null and b/nifi-registry-core/nifi-registry-docs/src/main/asciidoc/images/delete_buckets_dialog.png differ

http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/6f26290d/nifi-registry-core/nifi-registry-docs/src/main/asciidoc/images/delete_multiple_buckets.png
----------------------------------------------------------------------
diff --git a/nifi-registry-core/nifi-registry-docs/src/main/asciidoc/images/delete_multiple_buckets.png b/nifi-registry-core/nifi-registry-docs/src/main/asciidoc/images/delete_multiple_buckets.png
new file mode 100644
index 0000000..8b5f9b2
Binary files /dev/null and b/nifi-registry-core/nifi-registry-docs/src/main/asciidoc/images/delete_multiple_buckets.png differ

http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/6f26290d/nifi-registry-core/nifi-registry-docs/src/main/asciidoc/images/delete_multiple_users.png
----------------------------------------------------------------------
diff --git a/nifi-registry-core/nifi-registry-docs/src/main/asciidoc/images/delete_multiple_users.png b/nifi-registry-core/nifi-registry-docs/src/main/asciidoc/images/delete_multiple_users.png
new file mode 100644
index 0000000..31f4ad2
Binary files /dev/null and b/nifi-registry-core/nifi-registry-docs/src/main/asciidoc/images/delete_multiple_users.png differ

http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/6f26290d/nifi-registry-core/nifi-registry-docs/src/main/asciidoc/images/delete_user_dialog.png
----------------------------------------------------------------------
diff --git a/nifi-registry-core/nifi-registry-docs/src/main/asciidoc/images/delete_user_dialog.png b/nifi-registry-core/nifi-registry-docs/src/main/asciidoc/images/delete_user_dialog.png
new file mode 100644
index 0000000..35f2253
Binary files /dev/null and b/nifi-registry-core/nifi-registry-docs/src/main/asciidoc/images/delete_user_dialog.png differ

http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/6f26290d/nifi-registry-core/nifi-registry-docs/src/main/asciidoc/images/delete_user_single.png
----------------------------------------------------------------------
diff --git a/nifi-registry-core/nifi-registry-docs/src/main/asciidoc/images/delete_user_single.png b/nifi-registry-core/nifi-registry-docs/src/main/asciidoc/images/delete_user_single.png
new file mode 100644
index 0000000..82599e4
Binary files /dev/null and b/nifi-registry-core/nifi-registry-docs/src/main/asciidoc/images/delete_user_single.png differ

http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/6f26290d/nifi-registry-core/nifi-registry-docs/src/main/asciidoc/images/delete_users_groups_dialog.png
----------------------------------------------------------------------
diff --git a/nifi-registry-core/nifi-registry-docs/src/main/asciidoc/images/delete_users_groups_dialog.png b/nifi-registry-core/nifi-registry-docs/src/main/asciidoc/images/delete_users_groups_dialog.png
new file mode 100644
index 0000000..0c00989
Binary files /dev/null and b/nifi-registry-core/nifi-registry-docs/src/main/asciidoc/images/delete_users_groups_dialog.png differ

http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/6f26290d/nifi-registry-core/nifi-registry-docs/src/main/asciidoc/images/drag_process_group.png
----------------------------------------------------------------------
diff --git a/nifi-registry-core/nifi-registry-docs/src/main/asciidoc/images/drag_process_group.png b/nifi-registry-core/nifi-registry-docs/src/main/asciidoc/images/drag_process_group.png
new file mode 100644
index 0000000..d187616
Binary files /dev/null and b/nifi-registry-core/nifi-registry-docs/src/main/asciidoc/images/drag_process_group.png differ

http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/6f26290d/nifi-registry-core/nifi-registry-docs/src/main/asciidoc/images/empty_registry.png
----------------------------------------------------------------------
diff --git a/nifi-registry-core/nifi-registry-docs/src/main/asciidoc/images/empty_registry.png b/nifi-registry-core/nifi-registry-docs/src/main/asciidoc/images/empty_registry.png
new file mode 100644
index 0000000..e2959ae
Binary files /dev/null and b/nifi-registry-core/nifi-registry-docs/src/main/asciidoc/images/empty_registry.png differ

http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/6f26290d/nifi-registry-core/nifi-registry-docs/src/main/asciidoc/images/flow_change_log.png
----------------------------------------------------------------------
diff --git a/nifi-registry-core/nifi-registry-docs/src/main/asciidoc/images/flow_change_log.png b/nifi-registry-core/nifi-registry-docs/src/main/asciidoc/images/flow_change_log.png
new file mode 100644
index 0000000..ece10bd
Binary files /dev/null and b/nifi-registry-core/nifi-registry-docs/src/main/asciidoc/images/flow_change_log.png differ

http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/6f26290d/nifi-registry-core/nifi-registry-docs/src/main/asciidoc/images/flow_delete_action.png
----------------------------------------------------------------------
diff --git a/nifi-registry-core/nifi-registry-docs/src/main/asciidoc/images/flow_delete_action.png b/nifi-registry-core/nifi-registry-docs/src/main/asciidoc/images/flow_delete_action.png
new file mode 100644
index 0000000..5ae40a9
Binary files /dev/null and b/nifi-registry-core/nifi-registry-docs/src/main/asciidoc/images/flow_delete_action.png differ

http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/6f26290d/nifi-registry-core/nifi-registry-docs/src/main/asciidoc/images/flow_delete_confirm.png
----------------------------------------------------------------------
diff --git a/nifi-registry-core/nifi-registry-docs/src/main/asciidoc/images/flow_delete_confirm.png b/nifi-registry-core/nifi-registry-docs/src/main/asciidoc/images/flow_delete_confirm.png
new file mode 100644
index 0000000..491f4d3
Binary files /dev/null and b/nifi-registry-core/nifi-registry-docs/src/main/asciidoc/images/flow_delete_confirm.png differ

http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/6f26290d/nifi-registry-core/nifi-registry-docs/src/main/asciidoc/images/flows_all.png
----------------------------------------------------------------------
diff --git a/nifi-registry-core/nifi-registry-docs/src/main/asciidoc/images/flows_all.png b/nifi-registry-core/nifi-registry-docs/src/main/asciidoc/images/flows_all.png
new file mode 100644
index 0000000..0faef8f
Binary files /dev/null and b/nifi-registry-core/nifi-registry-docs/src/main/asciidoc/images/flows_all.png differ

http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/6f26290d/nifi-registry-core/nifi-registry-docs/src/main/asciidoc/images/flows_filter_by_name.png
----------------------------------------------------------------------
diff --git a/nifi-registry-core/nifi-registry-docs/src/main/asciidoc/images/flows_filter_by_name.png b/nifi-registry-core/nifi-registry-docs/src/main/asciidoc/images/flows_filter_by_name.png
new file mode 100644
index 0000000..5d53d73
Binary files /dev/null and b/nifi-registry-core/nifi-registry-docs/src/main/asciidoc/images/flows_filter_by_name.png differ

http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/6f26290d/nifi-registry-core/nifi-registry-docs/src/main/asciidoc/images/flows_sort_menu.png
----------------------------------------------------------------------
diff --git a/nifi-registry-core/nifi-registry-docs/src/main/asciidoc/images/flows_sort_menu.png b/nifi-registry-core/nifi-registry-docs/src/main/asciidoc/images/flows_sort_menu.png
new file mode 100644
index 0000000..517a762
Binary files /dev/null and b/nifi-registry-core/nifi-registry-docs/src/main/asciidoc/images/flows_sort_menu.png differ

http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/6f26290d/nifi-registry-core/nifi-registry-docs/src/main/asciidoc/images/group_added.png
----------------------------------------------------------------------
diff --git a/nifi-registry-core/nifi-registry-docs/src/main/asciidoc/images/group_added.png b/nifi-registry-core/nifi-registry-docs/src/main/asciidoc/images/group_added.png
new file mode 100644
index 0000000..fc71971
Binary files /dev/null and b/nifi-registry-core/nifi-registry-docs/src/main/asciidoc/images/group_added.png differ

http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/6f26290d/nifi-registry-core/nifi-registry-docs/src/main/asciidoc/images/iconDelete.png
----------------------------------------------------------------------
diff --git a/nifi-registry-core/nifi-registry-docs/src/main/asciidoc/images/iconDelete.png b/nifi-registry-core/nifi-registry-docs/src/main/asciidoc/images/iconDelete.png
new file mode 100644
index 0000000..cf4d048
Binary files /dev/null and b/nifi-registry-core/nifi-registry-docs/src/main/asciidoc/images/iconDelete.png differ

http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/6f26290d/nifi-registry-core/nifi-registry-docs/src/main/asciidoc/images/iconHelp.png
----------------------------------------------------------------------
diff --git a/nifi-registry-core/nifi-registry-docs/src/main/asciidoc/images/iconHelp.png b/nifi-registry-core/nifi-registry-docs/src/main/asciidoc/images/iconHelp.png
new file mode 100644
index 0000000..601ec42
Binary files /dev/null and b/nifi-registry-core/nifi-registry-docs/src/main/asciidoc/images/iconHelp.png differ

http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/6f26290d/nifi-registry-core/nifi-registry-docs/src/main/asciidoc/images/iconLocallyModified.png
----------------------------------------------------------------------
diff --git a/nifi-registry-core/nifi-registry-docs/src/main/asciidoc/images/iconLocallyModified.png b/nifi-registry-core/nifi-registry-docs/src/main/asciidoc/images/iconLocallyModified.png
new file mode 100644
index 0000000..4f72251
Binary files /dev/null and b/nifi-registry-core/nifi-registry-docs/src/main/asciidoc/images/iconLocallyModified.png differ

http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/6f26290d/nifi-registry-core/nifi-registry-docs/src/main/asciidoc/images/iconManage.png
----------------------------------------------------------------------
diff --git a/nifi-registry-core/nifi-registry-docs/src/main/asciidoc/images/iconManage.png b/nifi-registry-core/nifi-registry-docs/src/main/asciidoc/images/iconManage.png
new file mode 100644
index 0000000..d70dedf
Binary files /dev/null and b/nifi-registry-core/nifi-registry-docs/src/main/asciidoc/images/iconManage.png differ

http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/6f26290d/nifi-registry-core/nifi-registry-docs/src/main/asciidoc/images/iconSettings.png
----------------------------------------------------------------------
diff --git a/nifi-registry-core/nifi-registry-docs/src/main/asciidoc/images/iconSettings.png b/nifi-registry-core/nifi-registry-docs/src/main/asciidoc/images/iconSettings.png
new file mode 100644
index 0000000..693255c
Binary files /dev/null and b/nifi-registry-core/nifi-registry-docs/src/main/asciidoc/images/iconSettings.png differ

http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/6f26290d/nifi-registry-core/nifi-registry-docs/src/main/asciidoc/images/iconUpToDate.png
----------------------------------------------------------------------
diff --git a/nifi-registry-core/nifi-registry-docs/src/main/asciidoc/images/iconUpToDate.png b/nifi-registry-core/nifi-registry-docs/src/main/asciidoc/images/iconUpToDate.png
new file mode 100644
index 0000000..78e52eb
Binary files /dev/null and b/nifi-registry-core/nifi-registry-docs/src/main/asciidoc/images/iconUpToDate.png differ

http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/6f26290d/nifi-registry-core/nifi-registry-docs/src/main/asciidoc/images/import_ABCD_version_2.png
----------------------------------------------------------------------
diff --git a/nifi-registry-core/nifi-registry-docs/src/main/asciidoc/images/import_ABCD_version_2.png b/nifi-registry-core/nifi-registry-docs/src/main/asciidoc/images/import_ABCD_version_2.png
new file mode 100644
index 0000000..fa88678
Binary files /dev/null and b/nifi-registry-core/nifi-registry-docs/src/main/asciidoc/images/import_ABCD_version_2.png differ

http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/6f26290d/nifi-registry-core/nifi-registry-docs/src/main/asciidoc/images/import_flow_from_registry.png
----------------------------------------------------------------------
diff --git a/nifi-registry-core/nifi-registry-docs/src/main/asciidoc/images/import_flow_from_registry.png b/nifi-registry-core/nifi-registry-docs/src/main/asciidoc/images/import_flow_from_registry.png
new file mode 100644
index 0000000..c2fa67a
Binary files /dev/null and b/nifi-registry-core/nifi-registry-docs/src/main/asciidoc/images/import_flow_from_registry.png differ

http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/6f26290d/nifi-registry-core/nifi-registry-docs/src/main/asciidoc/images/local_registry.png
----------------------------------------------------------------------
diff --git a/nifi-registry-core/nifi-registry-docs/src/main/asciidoc/images/local_registry.png b/nifi-registry-core/nifi-registry-docs/src/main/asciidoc/images/local_registry.png
new file mode 100644
index 0000000..047dd71
Binary files /dev/null and b/nifi-registry-core/nifi-registry-docs/src/main/asciidoc/images/local_registry.png differ

http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/6f26290d/nifi-registry-core/nifi-registry-docs/src/main/asciidoc/images/loginRegistry.png
----------------------------------------------------------------------
diff --git a/nifi-registry-core/nifi-registry-docs/src/main/asciidoc/images/loginRegistry.png b/nifi-registry-core/nifi-registry-docs/src/main/asciidoc/images/loginRegistry.png
new file mode 100644
index 0000000..9fa3f9d
Binary files /dev/null and b/nifi-registry-core/nifi-registry-docs/src/main/asciidoc/images/loginRegistry.png differ

http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/6f26290d/nifi-registry-core/nifi-registry-docs/src/main/asciidoc/images/manage_bucket.png
----------------------------------------------------------------------
diff --git a/nifi-registry-core/nifi-registry-docs/src/main/asciidoc/images/manage_bucket.png b/nifi-registry-core/nifi-registry-docs/src/main/asciidoc/images/manage_bucket.png
new file mode 100644
index 0000000..e2159da
Binary files /dev/null and b/nifi-registry-core/nifi-registry-docs/src/main/asciidoc/images/manage_bucket.png differ

http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/6f26290d/nifi-registry-core/nifi-registry-docs/src/main/asciidoc/images/manage_user.png
----------------------------------------------------------------------
diff --git a/nifi-registry-core/nifi-registry-docs/src/main/asciidoc/images/manage_user.png b/nifi-registry-core/nifi-registry-docs/src/main/asciidoc/images/manage_user.png
new file mode 100644
index 0000000..d8174c0
Binary files /dev/null and b/nifi-registry-core/nifi-registry-docs/src/main/asciidoc/images/manage_user.png differ

http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/6f26290d/nifi-registry-core/nifi-registry-docs/src/main/asciidoc/images/new_bucket_button.png
----------------------------------------------------------------------
diff --git a/nifi-registry-core/nifi-registry-docs/src/main/asciidoc/images/new_bucket_button.png b/nifi-registry-core/nifi-registry-docs/src/main/asciidoc/images/new_bucket_button.png
new file mode 100644
index 0000000..147d5d0
Binary files /dev/null and b/nifi-registry-core/nifi-registry-docs/src/main/asciidoc/images/new_bucket_button.png differ

http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/6f26290d/nifi-registry-core/nifi-registry-docs/src/main/asciidoc/images/new_bucket_dialog.png
----------------------------------------------------------------------
diff --git a/nifi-registry-core/nifi-registry-docs/src/main/asciidoc/images/new_bucket_dialog.png b/nifi-registry-core/nifi-registry-docs/src/main/asciidoc/images/new_bucket_dialog.png
new file mode 100644
index 0000000..9bb7627
Binary files /dev/null and b/nifi-registry-core/nifi-registry-docs/src/main/asciidoc/images/new_bucket_dialog.png differ

http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/6f26290d/nifi-registry-core/nifi-registry-docs/src/main/asciidoc/images/new_bucket_policy_added.png
----------------------------------------------------------------------
diff --git a/nifi-registry-core/nifi-registry-docs/src/main/asciidoc/images/new_bucket_policy_added.png b/nifi-registry-core/nifi-registry-docs/src/main/asciidoc/images/new_bucket_policy_added.png
new file mode 100644
index 0000000..a18e8c8
Binary files /dev/null and b/nifi-registry-core/nifi-registry-docs/src/main/asciidoc/images/new_bucket_policy_added.png differ

http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/6f26290d/nifi-registry-core/nifi-registry-docs/src/main/asciidoc/images/new_bucket_policy_create.png
----------------------------------------------------------------------
diff --git a/nifi-registry-core/nifi-registry-docs/src/main/asciidoc/images/new_bucket_policy_create.png b/nifi-registry-core/nifi-registry-docs/src/main/asciidoc/images/new_bucket_policy_create.png
new file mode 100644
index 0000000..e29aa66
Binary files /dev/null and b/nifi-registry-core/nifi-registry-docs/src/main/asciidoc/images/new_bucket_policy_create.png differ

http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/6f26290d/nifi-registry-core/nifi-registry-docs/src/main/asciidoc/images/new_bucket_policy_user_permission.png
----------------------------------------------------------------------
diff --git a/nifi-registry-core/nifi-registry-docs/src/main/asciidoc/images/new_bucket_policy_user_permission.png b/nifi-registry-core/nifi-registry-docs/src/main/asciidoc/images/new_bucket_policy_user_permission.png
new file mode 100644
index 0000000..3367009
Binary files /dev/null and b/nifi-registry-core/nifi-registry-docs/src/main/asciidoc/images/new_bucket_policy_user_permission.png differ

http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/6f26290d/nifi-registry-core/nifi-registry-docs/src/main/asciidoc/images/new_test_bucket.png
----------------------------------------------------------------------
diff --git a/nifi-registry-core/nifi-registry-docs/src/main/asciidoc/images/new_test_bucket.png b/nifi-registry-core/nifi-registry-docs/src/main/asciidoc/images/new_test_bucket.png
new file mode 100644
index 0000000..26aa4b1
Binary files /dev/null and b/nifi-registry-core/nifi-registry-docs/src/main/asciidoc/images/new_test_bucket.png differ

http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/6f26290d/nifi-registry-core/nifi-registry-docs/src/main/asciidoc/images/nifi-registry-components.png
----------------------------------------------------------------------
diff --git a/nifi-registry-core/nifi-registry-docs/src/main/asciidoc/images/nifi-registry-components.png b/nifi-registry-core/nifi-registry-docs/src/main/asciidoc/images/nifi-registry-components.png
new file mode 100644
index 0000000..2225ab6
Binary files /dev/null and b/nifi-registry-core/nifi-registry-docs/src/main/asciidoc/images/nifi-registry-components.png differ

http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/6f26290d/nifi-registry-core/nifi-registry-docs/src/main/asciidoc/images/nifi_user1_template.snagproj
----------------------------------------------------------------------
diff --git a/nifi-registry-core/nifi-registry-docs/src/main/asciidoc/images/nifi_user1_template.snagproj b/nifi-registry-core/nifi-registry-docs/src/main/asciidoc/images/nifi_user1_template.snagproj
new file mode 100644
index 0000000..5fd34a8
Binary files /dev/null and b/nifi-registry-core/nifi-registry-docs/src/main/asciidoc/images/nifi_user1_template.snagproj differ

http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/6f26290d/nifi-registry-core/nifi-registry-docs/src/main/asciidoc/images/nifi_user_template.snagproj
----------------------------------------------------------------------
diff --git a/nifi-registry-core/nifi-registry-docs/src/main/asciidoc/images/nifi_user_template.snagproj b/nifi-registry-core/nifi-registry-docs/src/main/asciidoc/images/nifi_user_template.snagproj
new file mode 100644
index 0000000..9aee06d
Binary files /dev/null and b/nifi-registry-core/nifi-registry-docs/src/main/asciidoc/images/nifi_user_template.snagproj differ

http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/6f26290d/nifi-registry-core/nifi-registry-docs/src/main/asciidoc/images/remove_group_from_user.png
----------------------------------------------------------------------
diff --git a/nifi-registry-core/nifi-registry-docs/src/main/asciidoc/images/remove_group_from_user.png b/nifi-registry-core/nifi-registry-docs/src/main/asciidoc/images/remove_group_from_user.png
new file mode 100644
index 0000000..023ccde
Binary files /dev/null and b/nifi-registry-core/nifi-registry-docs/src/main/asciidoc/images/remove_group_from_user.png differ

http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/6f26290d/nifi-registry-core/nifi-registry-docs/src/main/asciidoc/images/remove_user_from_group.png
----------------------------------------------------------------------
diff --git a/nifi-registry-core/nifi-registry-docs/src/main/asciidoc/images/remove_user_from_group.png b/nifi-registry-core/nifi-registry-docs/src/main/asciidoc/images/remove_user_from_group.png
new file mode 100644
index 0000000..c1c7497
Binary files /dev/null and b/nifi-registry-core/nifi-registry-docs/src/main/asciidoc/images/remove_user_from_group.png differ

http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/6f26290d/nifi-registry-core/nifi-registry-docs/src/main/asciidoc/images/save_ABCD_flow_dialog.png
----------------------------------------------------------------------
diff --git a/nifi-registry-core/nifi-registry-docs/src/main/asciidoc/images/save_ABCD_flow_dialog.png b/nifi-registry-core/nifi-registry-docs/src/main/asciidoc/images/save_ABCD_flow_dialog.png
new file mode 100644
index 0000000..0c20d2e
Binary files /dev/null and b/nifi-registry-core/nifi-registry-docs/src/main/asciidoc/images/save_ABCD_flow_dialog.png differ

http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/6f26290d/nifi-registry-core/nifi-registry-docs/src/main/asciidoc/images/select_users_create_new_group.png
----------------------------------------------------------------------
diff --git a/nifi-registry-core/nifi-registry-docs/src/main/asciidoc/images/select_users_create_new_group.png b/nifi-registry-core/nifi-registry-docs/src/main/asciidoc/images/select_users_create_new_group.png
new file mode 100644
index 0000000..36f8dd6
Binary files /dev/null and b/nifi-registry-core/nifi-registry-docs/src/main/asciidoc/images/select_users_create_new_group.png differ

http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/6f26290d/nifi-registry-core/nifi-registry-docs/src/main/asciidoc/images/select_users_create_new_group_dialog.png
----------------------------------------------------------------------
diff --git a/nifi-registry-core/nifi-registry-docs/src/main/asciidoc/images/select_users_create_new_group_dialog.png b/nifi-registry-core/nifi-registry-docs/src/main/asciidoc/images/select_users_create_new_group_dialog.png
new file mode 100644
index 0000000..33f9142
Binary files /dev/null and b/nifi-registry-core/nifi-registry-docs/src/main/asciidoc/images/select_users_create_new_group_dialog.png differ

http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/6f26290d/nifi-registry-core/nifi-registry-docs/src/main/asciidoc/images/select_users_new_group_added.png
----------------------------------------------------------------------
diff --git a/nifi-registry-core/nifi-registry-docs/src/main/asciidoc/images/select_users_new_group_added.png b/nifi-registry-core/nifi-registry-docs/src/main/asciidoc/images/select_users_new_group_added.png
new file mode 100644
index 0000000..51ed584
Binary files /dev/null and b/nifi-registry-core/nifi-registry-docs/src/main/asciidoc/images/select_users_new_group_added.png differ

http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/6f26290d/nifi-registry-core/nifi-registry-docs/src/main/asciidoc/images/test_bucket.png
----------------------------------------------------------------------
diff --git a/nifi-registry-core/nifi-registry-docs/src/main/asciidoc/images/test_bucket.png b/nifi-registry-core/nifi-registry-docs/src/main/asciidoc/images/test_bucket.png
new file mode 100644
index 0000000..bfccd63
Binary files /dev/null and b/nifi-registry-core/nifi-registry-docs/src/main/asciidoc/images/test_bucket.png differ

http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/6f26290d/nifi-registry-core/nifi-registry-docs/src/main/asciidoc/images/test_bucket_dialog.png
----------------------------------------------------------------------
diff --git a/nifi-registry-core/nifi-registry-docs/src/main/asciidoc/images/test_bucket_dialog.png b/nifi-registry-core/nifi-registry-docs/src/main/asciidoc/images/test_bucket_dialog.png
new file mode 100644
index 0000000..e9143b4
Binary files /dev/null and b/nifi-registry-core/nifi-registry-docs/src/main/asciidoc/images/test_bucket_dialog.png differ

http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/6f26290d/nifi-registry-core/nifi-registry-docs/src/main/asciidoc/images/two_ABCD_flows.png
----------------------------------------------------------------------
diff --git a/nifi-registry-core/nifi-registry-docs/src/main/asciidoc/images/two_ABCD_flows.png b/nifi-registry-core/nifi-registry-docs/src/main/asciidoc/images/two_ABCD_flows.png
new file mode 100644
index 0000000..afec033
Binary files /dev/null and b/nifi-registry-core/nifi-registry-docs/src/main/asciidoc/images/two_ABCD_flows.png differ

http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/6f26290d/nifi-registry-core/nifi-registry-docs/src/main/asciidoc/images/user_nav_add_to_group.png
----------------------------------------------------------------------
diff --git a/nifi-registry-core/nifi-registry-docs/src/main/asciidoc/images/user_nav_add_to_group.png b/nifi-registry-core/nifi-registry-docs/src/main/asciidoc/images/user_nav_add_to_group.png
new file mode 100644
index 0000000..68653df
Binary files /dev/null and b/nifi-registry-core/nifi-registry-docs/src/main/asciidoc/images/user_nav_add_to_group.png differ

http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/6f26290d/nifi-registry-core/nifi-registry-docs/src/main/asciidoc/images/user_nav_name_edit.png
----------------------------------------------------------------------
diff --git a/nifi-registry-core/nifi-registry-docs/src/main/asciidoc/images/user_nav_name_edit.png b/nifi-registry-core/nifi-registry-docs/src/main/asciidoc/images/user_nav_name_edit.png
new file mode 100644
index 0000000..bd19121
Binary files /dev/null and b/nifi-registry-core/nifi-registry-docs/src/main/asciidoc/images/user_nav_name_edit.png differ

http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/6f26290d/nifi-registry-core/nifi-registry-docs/src/main/asciidoc/images/user_special_privileges.png
----------------------------------------------------------------------
diff --git a/nifi-registry-core/nifi-registry-docs/src/main/asciidoc/images/user_special_privileges.png b/nifi-registry-core/nifi-registry-docs/src/main/asciidoc/images/user_special_privileges.png
new file mode 100644
index 0000000..eda0b43
Binary files /dev/null and b/nifi-registry-core/nifi-registry-docs/src/main/asciidoc/images/user_special_privileges.png differ

http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/6f26290d/nifi-registry-core/nifi-registry-docs/src/main/asciidoc/images/users_filter_by_name.png
----------------------------------------------------------------------
diff --git a/nifi-registry-core/nifi-registry-docs/src/main/asciidoc/images/users_filter_by_name.png b/nifi-registry-core/nifi-registry-docs/src/main/asciidoc/images/users_filter_by_name.png
new file mode 100644
index 0000000..7b994a3
Binary files /dev/null and b/nifi-registry-core/nifi-registry-docs/src/main/asciidoc/images/users_filter_by_name.png differ

http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/6f26290d/nifi-registry-core/nifi-registry-docs/src/main/asciidoc/images/users_non_configurable.png
----------------------------------------------------------------------
diff --git a/nifi-registry-core/nifi-registry-docs/src/main/asciidoc/images/users_non_configurable.png b/nifi-registry-core/nifi-registry-docs/src/main/asciidoc/images/users_non_configurable.png
new file mode 100644
index 0000000..f2500a1
Binary files /dev/null and b/nifi-registry-core/nifi-registry-docs/src/main/asciidoc/images/users_non_configurable.png differ

http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/6f26290d/nifi-registry-core/nifi-registry-docs/src/main/asciidoc/images/users_sort_by_name.png
----------------------------------------------------------------------
diff --git a/nifi-registry-core/nifi-registry-docs/src/main/asciidoc/images/users_sort_by_name.png b/nifi-registry-core/nifi-registry-docs/src/main/asciidoc/images/users_sort_by_name.png
new file mode 100644
index 0000000..4548813
Binary files /dev/null and b/nifi-registry-core/nifi-registry-docs/src/main/asciidoc/images/users_sort_by_name.png differ


[30/51] [partial] nifi-registry git commit: NIFIREG-201 Refactoring project structure to better isolate extensions

Posted by kd...@apache.org.
http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/6f26290d/nifi-registry-core/nifi-registry-framework/src/main/xsd/authorizations.xsd
----------------------------------------------------------------------
diff --git a/nifi-registry-core/nifi-registry-framework/src/main/xsd/authorizations.xsd b/nifi-registry-core/nifi-registry-framework/src/main/xsd/authorizations.xsd
new file mode 100644
index 0000000..2c8f805
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-framework/src/main/xsd/authorizations.xsd
@@ -0,0 +1,87 @@
+<?xml version="1.0"?>
+<!--
+  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.
+-->
+<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema">
+
+    <xs:complexType name="Policy">
+        <xs:sequence>
+            <xs:element name="group" minOccurs="0" maxOccurs="unbounded" >
+                <xs:complexType>
+                    <xs:attribute name="identifier">
+                        <xs:simpleType>
+                            <xs:restriction base="xs:string">
+                                <xs:minLength value="1"/>
+                                <xs:pattern value=".*[^\s].*"/>
+                            </xs:restriction>
+                        </xs:simpleType>
+                    </xs:attribute>
+                </xs:complexType>
+            </xs:element>
+            <xs:element name="user" minOccurs="0" maxOccurs="unbounded" >
+                <xs:complexType>
+                    <xs:attribute name="identifier">
+                        <xs:simpleType>
+                            <xs:restriction base="xs:string">
+                                <xs:minLength value="1"/>
+                                <xs:pattern value=".*[^\s].*"/>
+                            </xs:restriction>
+                        </xs:simpleType>
+                    </xs:attribute>
+                </xs:complexType>
+            </xs:element>
+        </xs:sequence>
+        <xs:attribute name="identifier">
+            <xs:simpleType>
+                <xs:restriction base="xs:string">
+                    <xs:minLength value="1"/>
+                    <xs:pattern value=".*[^\s].*"/>
+                </xs:restriction>
+            </xs:simpleType>
+        </xs:attribute>
+        <xs:attribute name="resource">
+            <xs:simpleType>
+                <xs:restriction base="xs:string">
+                    <xs:minLength value="1"/>
+                    <xs:pattern value=".*[^\s].*"/>
+                </xs:restriction>
+            </xs:simpleType>
+        </xs:attribute>
+        <xs:attribute name="action">
+            <xs:simpleType>
+                <xs:restriction base="xs:string">
+                    <xs:enumeration value="R"/>
+                    <xs:enumeration value="W"/>
+                    <xs:enumeration value="D"/>
+                </xs:restriction>
+            </xs:simpleType>
+        </xs:attribute>
+    </xs:complexType>
+
+    <xs:complexType name="Policies">
+        <xs:sequence>
+            <xs:element name="policy" type="Policy" minOccurs="0" maxOccurs="unbounded"/>
+        </xs:sequence>
+    </xs:complexType>
+
+    <!-- top-level authorizations element -->
+    <xs:element name="authorizations">
+        <xs:complexType>
+            <xs:sequence>
+                <xs:element name="policies" type="Policies" minOccurs="0" maxOccurs="1" />
+            </xs:sequence>
+        </xs:complexType>
+    </xs:element>
+
+</xs:schema>
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/6f26290d/nifi-registry-core/nifi-registry-framework/src/main/xsd/authorizers.xsd
----------------------------------------------------------------------
diff --git a/nifi-registry-core/nifi-registry-framework/src/main/xsd/authorizers.xsd b/nifi-registry-core/nifi-registry-framework/src/main/xsd/authorizers.xsd
new file mode 100644
index 0000000..ed2a293
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-framework/src/main/xsd/authorizers.xsd
@@ -0,0 +1,68 @@
+<?xml version="1.0"?>
+<!--
+  ~ 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.
+  -->
+<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema">
+
+    <!-- user group providers type -->
+    <xs:complexType name="UserGroupProvider">
+        <xs:sequence>
+            <xs:element name="identifier" type="xs:string"/>
+            <xs:element name="class" type="xs:string"/>
+            <xs:element name="property" type="Prop" minOccurs="0" maxOccurs="unbounded" />
+        </xs:sequence>
+    </xs:complexType>
+
+    <!-- access policy provider type -->
+    <xs:complexType name="AccessPolicyProvider">
+        <xs:sequence>
+            <xs:element name="identifier" type="xs:string"/>
+            <xs:element name="class" type="xs:string"/>
+            <xs:element name="property" type="Prop" minOccurs="0" maxOccurs="unbounded" />
+        </xs:sequence>
+    </xs:complexType>
+
+    <!-- authorizers type -->
+    <xs:complexType name="Authorizer">
+        <xs:sequence>
+            <xs:element name="identifier" type="xs:string"/>
+            <xs:element name="class" type="xs:string"/>
+            <xs:element name="classpath" type="xs:string" minOccurs="0"/>
+            <xs:element name="property" type="Prop" minOccurs="0" maxOccurs="unbounded"/>
+        </xs:sequence>
+    </xs:complexType>
+
+    <!-- Name/Value properties-->
+    <xs:complexType name="Prop">
+        <xs:simpleContent>
+            <xs:extension base="xs:string">
+                <xs:attribute name="name" type="xs:string"></xs:attribute>
+                <xs:attribute name="encryption" type="xs:string"/>
+            </xs:extension>
+        </xs:simpleContent>
+    </xs:complexType>
+
+    <!-- authorizers -->
+    <xs:element name="authorizers">
+        <xs:complexType>
+            <xs:sequence>
+                <xs:element name="userGroupProvider" type="UserGroupProvider" minOccurs="0" maxOccurs="unbounded"/>
+                <xs:element name="accessPolicyProvider" type="AccessPolicyProvider" minOccurs="0" maxOccurs="unbounded"/>
+                <xs:element name="authorizer" type="Authorizer" minOccurs="0" maxOccurs="unbounded"/>
+            </xs:sequence>
+        </xs:complexType>
+    </xs:element>
+</xs:schema>
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/6f26290d/nifi-registry-core/nifi-registry-framework/src/main/xsd/identity-providers.xsd
----------------------------------------------------------------------
diff --git a/nifi-registry-core/nifi-registry-framework/src/main/xsd/identity-providers.xsd b/nifi-registry-core/nifi-registry-framework/src/main/xsd/identity-providers.xsd
new file mode 100644
index 0000000..bcca014
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-framework/src/main/xsd/identity-providers.xsd
@@ -0,0 +1,50 @@
+<?xml version="1.0"?>
+<!--
+  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.
+-->
+<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema">
+    <!-- role -->
+    <xs:complexType name="Provider">
+        <xs:sequence>
+            <xs:element name="identifier" type="NonEmptyStringType"/>
+            <xs:element name="class" type="NonEmptyStringType"/>
+            <xs:element name="property" type="Property" minOccurs="0" maxOccurs="unbounded" />
+        </xs:sequence>
+    </xs:complexType>
+
+    <!-- Name/Value properties-->
+    <xs:complexType name="Property">
+        <xs:simpleContent>
+            <xs:extension base="xs:string">
+                <xs:attribute name="name" type="NonEmptyStringType"/>
+                <xs:attribute name="encryption" type="xs:string"/>
+            </xs:extension>
+        </xs:simpleContent>
+    </xs:complexType>
+
+    <xs:simpleType name="NonEmptyStringType">
+        <xs:restriction base="xs:string">
+            <xs:minLength value="1"/>
+        </xs:restriction>
+    </xs:simpleType>
+
+    <!-- login identity provider -->
+    <xs:element name="identityProviders">
+        <xs:complexType>
+            <xs:sequence>
+                <xs:element name="provider" type="Provider" minOccurs="0" maxOccurs="unbounded"/>
+            </xs:sequence>
+        </xs:complexType>
+    </xs:element>
+</xs:schema>
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/6f26290d/nifi-registry-core/nifi-registry-framework/src/main/xsd/providers.xsd
----------------------------------------------------------------------
diff --git a/nifi-registry-core/nifi-registry-framework/src/main/xsd/providers.xsd b/nifi-registry-core/nifi-registry-framework/src/main/xsd/providers.xsd
new file mode 100644
index 0000000..ce82dcc
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-framework/src/main/xsd/providers.xsd
@@ -0,0 +1,51 @@
+<?xml version="1.0"?>
+<!--
+  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.
+-->
+<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema">
+
+    <!-- Provider type -->
+    <xs:complexType name="Provider">
+        <xs:sequence>
+            <xs:element name="class" type="NonEmptyStringType"/>
+            <xs:element name="property" type="Property" minOccurs="0" maxOccurs="unbounded"/>
+        </xs:sequence>
+    </xs:complexType>
+
+    <!-- Name/Value properties-->
+    <xs:complexType name="Property">
+        <xs:simpleContent>
+            <xs:extension base="xs:string">
+                <xs:attribute name="name" type="NonEmptyStringType"></xs:attribute>
+            </xs:extension>
+        </xs:simpleContent>
+    </xs:complexType>
+
+    <xs:simpleType name="NonEmptyStringType">
+        <xs:restriction base="xs:string">
+            <xs:minLength value="1"/>
+        </xs:restriction>
+    </xs:simpleType>
+
+    <!-- providers -->
+    <xs:element name="providers">
+        <xs:complexType>
+            <xs:sequence>
+                <xs:element name="flowPersistenceProvider" type="Provider" minOccurs="1" maxOccurs="1" />
+                <xs:element name="eventHookProvider" type="Provider" minOccurs="0" maxOccurs="unbounded" />
+            </xs:sequence>
+        </xs:complexType>
+    </xs:element>
+
+</xs:schema>
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/6f26290d/nifi-registry-core/nifi-registry-framework/src/main/xsd/tenants.xsd
----------------------------------------------------------------------
diff --git a/nifi-registry-core/nifi-registry-framework/src/main/xsd/tenants.xsd b/nifi-registry-core/nifi-registry-framework/src/main/xsd/tenants.xsd
new file mode 100644
index 0000000..c1193c3
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-framework/src/main/xsd/tenants.xsd
@@ -0,0 +1,96 @@
+<?xml version="1.0"?>
+<!--
+  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.
+-->
+<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema">
+
+    <!-- group -->
+    <xs:complexType name="Group">
+        <xs:sequence>
+            <xs:element name="user" minOccurs="0" maxOccurs="unbounded" >
+                <xs:complexType>
+                    <xs:attribute name="identifier">
+                        <xs:simpleType>
+                            <xs:restriction base="xs:string">
+                                <xs:minLength value="1"/>
+                                <xs:pattern value=".*[^\s].*"/>
+                            </xs:restriction>
+                        </xs:simpleType>
+                    </xs:attribute>
+                </xs:complexType>
+            </xs:element>
+        </xs:sequence>
+        <xs:attribute name="identifier">
+            <xs:simpleType>
+                <xs:restriction base="xs:string">
+                    <xs:minLength value="1"/>
+                    <xs:pattern value=".*[^\s].*"/>
+                </xs:restriction>
+            </xs:simpleType>
+        </xs:attribute>
+        <xs:attribute name="name">
+            <xs:simpleType>
+                <xs:restriction base="xs:string">
+                    <xs:minLength value="1"/>
+                    <xs:pattern value=".*[^\s].*"/>
+                </xs:restriction>
+            </xs:simpleType>
+        </xs:attribute>
+    </xs:complexType>
+
+    <!-- groups -->
+    <xs:complexType name="Groups">
+        <xs:sequence>
+            <xs:element name="group" type="Group" minOccurs="0" maxOccurs="unbounded"/>
+        </xs:sequence>
+    </xs:complexType>
+
+    <!-- user -->
+    <xs:complexType name="User">
+        <xs:attribute name="identifier">
+            <xs:simpleType>
+                <xs:restriction base="xs:string">
+                    <xs:minLength value="1"/>
+                    <xs:pattern value=".*[^\s].*"/>
+                </xs:restriction>
+            </xs:simpleType>
+        </xs:attribute>
+        <xs:attribute name="identity">
+            <xs:simpleType>
+                <xs:restriction base="xs:string">
+                    <xs:minLength value="1"/>
+                    <xs:pattern value=".*[^\s].*"/>
+                </xs:restriction>
+            </xs:simpleType>
+        </xs:attribute>
+    </xs:complexType>
+
+    <!-- users -->
+    <xs:complexType name="Users">
+        <xs:sequence>
+            <xs:element name="user" type="User" minOccurs="0" maxOccurs="unbounded"/>
+        </xs:sequence>
+    </xs:complexType>
+
+    <!-- top-level authorizations element -->
+    <xs:element name="tenants">
+        <xs:complexType>
+            <xs:sequence>
+                <xs:element name="groups" type="Groups" minOccurs="0" maxOccurs="1" />
+                <xs:element name="users" type="Users" minOccurs="0" maxOccurs="1" />
+            </xs:sequence>
+        </xs:complexType>
+    </xs:element>
+
+</xs:schema>
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/6f26290d/nifi-registry-core/nifi-registry-framework/src/test/groovy/org/apache/nifi/registry/security/authorization/AuthorizerFactorySpec.groovy
----------------------------------------------------------------------
diff --git a/nifi-registry-core/nifi-registry-framework/src/test/groovy/org/apache/nifi/registry/security/authorization/AuthorizerFactorySpec.groovy b/nifi-registry-core/nifi-registry-framework/src/test/groovy/org/apache/nifi/registry/security/authorization/AuthorizerFactorySpec.groovy
new file mode 100644
index 0000000..932d483
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-framework/src/test/groovy/org/apache/nifi/registry/security/authorization/AuthorizerFactorySpec.groovy
@@ -0,0 +1,129 @@
+/*
+ * 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.security.authorization
+
+import org.apache.nifi.registry.extension.ExtensionManager
+import org.apache.nifi.registry.properties.NiFiRegistryProperties
+import org.apache.nifi.registry.security.authorization.file.FileAccessPolicyProvider
+import org.apache.nifi.registry.security.authorization.file.FileUserGroupProvider
+import org.apache.nifi.registry.security.authorization.resource.ResourceFactory
+import spock.lang.Specification
+
+class AuthorizerFactorySpec extends Specification {
+
+    def mockProperties = Mock(NiFiRegistryProperties)
+    def mockExtensionManager = Mock(ExtensionManager)
+
+    AuthorizerFactory authorizerFactory
+
+    // runs before every feature method
+    def setup() {
+        mockExtensionManager.getExtensionClassLoader(_) >> this.getClass().getClassLoader()
+        mockProperties.getPropertyKeys() >> new HashSet<String>() // Called by IdentityMappingUtil.getIdentityMappings()
+
+        authorizerFactory = new AuthorizerFactory(mockProperties, mockExtensionManager, null)
+    }
+
+    // runs after every feature method
+    def cleanup() {
+        authorizerFactory = null
+    }
+
+    // runs before the first feature method
+    def setupSpec() {}
+
+    // runs after the last feature method
+    def cleanupSpec() {}
+
+    def "create default authorizer"() {
+
+        setup: "properties indicate nifi-registry is unsecured"
+        mockProperties.getProperty(NiFiRegistryProperties.WEB_HTTPS_PORT) >> ""
+
+        when: "getAuthorizer() is first called"
+        def authorizer = authorizerFactory.getAuthorizer()
+
+        then: "the default authorizer is returned"
+        authorizer != null
+
+        and: "any authorization request made to that authorizer is approved"
+        def authorizationResult = authorizer.authorize(getTestAuthorizationRequest())
+        authorizationResult.result == AuthorizationResult.Result.Approved
+
+    }
+
+    def "create file-backed authorizer"() {
+
+        setup:
+        setMockPropsAuthorizersConfig("src/test/resources/security/authorizers-good-file-providers.xml", "managed-authorizer")
+
+        when: "getAuthorizer() is first called"
+        def authorizer = authorizerFactory.getAuthorizer()
+
+        then: "an authorizer is returned with the expected providers"
+        authorizer != null
+        authorizer instanceof ManagedAuthorizer
+        def apProvider = ((ManagedAuthorizer) authorizer).getAccessPolicyProvider()
+        apProvider instanceof ConfigurableAccessPolicyProvider
+        def ugProvider = ((ConfigurableAccessPolicyProvider) apProvider).getUserGroupProvider()
+        ugProvider instanceof ConfigurableUserGroupProvider
+
+    }
+
+    def "invalid authorizer configuration fails"() {
+
+        when: "a bad configuration is provided and getAuthorizer() is called"
+        setMockPropsAuthorizersConfig(authorizersConfigFile, selectedAuthorizer)
+        authorizerFactory = new AuthorizerFactory(mockProperties, mockExtensionManager, null)
+        authorizerFactory.getAuthorizer()
+
+        then: "expect an exception"
+        def e = thrown AuthorizerFactoryException
+        e.message =~ expectedExceptionMessage || e.getCause().getMessage() =~ expectedExceptionMessage
+
+        where:
+        authorizersConfigFile                                                    | selectedAuthorizer        | expectedExceptionMessage
+        "src/test/resources/security/authorizers-good-file-providers.xml"        | ""                        | "When running securely, the authorizer identifier must be specified in the nifi-registry.properties file."
+        "src/test/resources/security/authorizers-good-file-providers.xml"        | "non-existent-authorizer" | "The specified authorizer 'non-existent-authorizer' could not be found."
+        "src/test/resources/security/authorizers-bad-ug-provider-ids.xml"        | "managed-authorizer"      | "Duplicate User Group Provider identifier in Authorizers configuration"
+        "src/test/resources/security/authorizers-bad-ap-provider-ids.xml"        | "managed-authorizer"      | "Duplicate Access Policy Provider identifier in Authorizers configuration"
+        "src/test/resources/security/authorizers-bad-authorizer-ids.xml"         | "managed-authorizer"      | "Duplicate Authorizer identifier in Authorizers configuration"
+        "src/test/resources/security/authorizers-bad-composite.xml"              | "managed-authorizer"      | "Duplicate provider in Composite User Group Provider configuration"
+        "src/test/resources/security/authorizers-bad-configurable-composite.xml" | "managed-authorizer"      | "Duplicate provider in Composite Configurable User Group Provider configuration"
+
+    }
+
+    // Helper methods
+
+    private void setMockPropsAuthorizersConfig(String filePath, String authorizer = "managed-authorizer") {
+        mockProperties.getProperty(NiFiRegistryProperties.WEB_HTTPS_PORT) >> "443"
+        mockProperties.getSslPort() >> 443 // required to be non-null to create authorizer
+        mockProperties.getProperty(NiFiRegistryProperties.SECURITY_AUTHORIZERS_CONFIGURATION_FILE) >> filePath
+        mockProperties.getAuthorizersConfigurationFile() >> new File(filePath)
+        mockProperties.getProperty(NiFiRegistryProperties.SECURITY_AUTHORIZER) >> authorizer
+    }
+
+    private static AuthorizationRequest getTestAuthorizationRequest() {
+        return new AuthorizationRequest.Builder()
+                .resource(ResourceFactory.getBucketsResource())
+                .action(RequestAction.WRITE)
+                .accessAttempt(false)
+                .anonymous(true)
+                .build()
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/6f26290d/nifi-registry-core/nifi-registry-framework/src/test/groovy/org/apache/nifi/registry/service/AuthorizationServiceSpec.groovy
----------------------------------------------------------------------
diff --git a/nifi-registry-core/nifi-registry-framework/src/test/groovy/org/apache/nifi/registry/service/AuthorizationServiceSpec.groovy b/nifi-registry-core/nifi-registry-framework/src/test/groovy/org/apache/nifi/registry/service/AuthorizationServiceSpec.groovy
new file mode 100644
index 0000000..149ec36
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-framework/src/test/groovy/org/apache/nifi/registry/service/AuthorizationServiceSpec.groovy
@@ -0,0 +1,615 @@
+/*
+ * 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.service
+
+import org.apache.nifi.registry.authorization.AccessPolicy
+import org.apache.nifi.registry.authorization.User
+import org.apache.nifi.registry.authorization.UserGroup
+import org.apache.nifi.registry.bucket.Bucket
+import org.apache.nifi.registry.security.authorization.AccessPolicy as AuthAccessPolicy
+import org.apache.nifi.registry.security.authorization.AuthorizableLookup
+import org.apache.nifi.registry.security.authorization.ConfigurableAccessPolicyProvider
+import org.apache.nifi.registry.security.authorization.ConfigurableUserGroupProvider
+import org.apache.nifi.registry.security.authorization.Group
+import org.apache.nifi.registry.security.authorization.RequestAction
+import org.apache.nifi.registry.security.authorization.StandardManagedAuthorizer
+import org.apache.nifi.registry.security.authorization.User as AuthUser
+import org.apache.nifi.registry.security.authorization.exception.AccessDeniedException
+import org.apache.nifi.registry.security.authorization.resource.Authorizable
+import org.apache.nifi.registry.security.authorization.resource.ResourceType
+import spock.lang.Specification
+
+class AuthorizationServiceSpec extends Specification {
+
+    def registryService = Mock(RegistryService)
+    def authorizableLookup = Mock(AuthorizableLookup)
+    def userGroupProvider = Mock(ConfigurableUserGroupProvider)
+    def accessPolicyProvider = Mock(ConfigurableAccessPolicyProvider)
+
+    AuthorizationService authorizationService
+
+    def setup() {
+        accessPolicyProvider.getUserGroupProvider() >> userGroupProvider
+        def authorizer = new StandardManagedAuthorizer(accessPolicyProvider, userGroupProvider)
+        authorizationService = new AuthorizationService(authorizableLookup, authorizer, registryService)
+    }
+
+    // ----- User tests -------------------------------------------------------
+
+    def "create user"() {
+
+        setup:
+        userGroupProvider.addUser(!null as AuthUser) >> {
+            AuthUser u -> new AuthUser.Builder().identifier(u.identifier).identity(u.identity).build()
+        }
+        userGroupProvider.getGroups() >> new HashSet<Group>()  // needed for converting user to DTO
+        accessPolicyProvider.getAccessPolicies() >> new HashSet<AccessPolicy>()  // needed for converting user to DTO
+
+        when: "new user is created successfully"
+        def user = new User(null, "username")
+        User createdUser = authorizationService.createUser(user)
+
+        then: "created user has been assigned an identifier"
+        with(createdUser) {
+            identifier != null
+            identity == "username"
+        }
+
+    }
+
+    def "list users"() {
+
+        setup:
+        userGroupProvider.getUsers() >> [
+                new AuthUser.Builder().identifier("user1").identity("username1").build(),
+                new AuthUser.Builder().identifier("user2").identity("username2").build(),
+                new AuthUser.Builder().identifier("user3").identity("username3").build(),
+        ]
+        userGroupProvider.getGroups() >> new HashSet<Group>()
+        accessPolicyProvider.getAccessPolicies() >> new HashSet<AccessPolicy>()
+
+        when: "list of users is queried"
+        def users = authorizationService.getUsers()
+
+        then: "users are successfully returned as list of DTO objects"
+        users != null
+        users.size() == 3
+        with(users[0]) {
+            identifier == "user1"
+            identity == "username1"
+        }
+        with(users[1]) {
+            identifier == "user2"
+            identity == "username2"
+        }
+        with(users[2]) {
+            identifier == "user3"
+            identity == "username3"
+        }
+
+    }
+
+    def "get user"() {
+
+        setup:
+        userGroupProvider.getGroups() >> new HashSet<Group>()
+        accessPolicyProvider.getAccessPolicies() >> new HashSet<AccessPolicy>()
+
+
+        when: "get user for existing user identifier"
+        userGroupProvider.getUser("userId") >> new AuthUser.Builder().identifier("userId").identity("username").build()
+        def user1 = authorizationService.getUser("userId")
+
+        then: "user is returned converted to DTO"
+        with(user1) {
+            identifier == "userId"
+            identity == "username"
+        }
+
+
+        when: "get user for non-existent user identifier"
+        userGroupProvider.getUser("nonExistentUserId") >> null
+        userGroupProvider.getGroup("nonExistentUserId") >> null
+        def user2 = authorizationService.getUser("nonExistentUserId")
+
+        then: "no user is returned"
+        user2 == null
+
+    }
+
+    def "update user"() {
+
+        setup:
+        userGroupProvider.updateUser(!null as AuthUser) >> {
+            AuthUser u -> new AuthUser.Builder().identifier(u.identifier).identity(u.identity).build()
+        }
+        userGroupProvider.getGroups() >> new HashSet<Group>()
+        accessPolicyProvider.getAccessPolicies() >> new HashSet<AccessPolicy>()
+
+
+        when: "user is updated"
+        def user = authorizationService.updateUser(new User("userId", "username"))
+
+        then: "updated user is returned"
+        with(user) {
+            identifier == "userId"
+            identity == "username"
+        }
+
+    }
+
+    def "delete user"() {
+
+        setup:
+        userGroupProvider.getUser("userId") >> new AuthUser.Builder().identifier("userId").identity("username").build()
+        userGroupProvider.deleteUser("userId") >> new AuthUser.Builder().identifier("userId").identity("username").build()
+        userGroupProvider.getGroups() >> new HashSet<Group>()
+        accessPolicyProvider.getAccessPolicies() >> new HashSet<AccessPolicy>()
+
+
+        when: "user is deleted"
+        def user = authorizationService.deleteUser("userId")
+
+        then: "deleted user is returned converted to DTO"
+        with(user) {
+            identifier == "userId"
+            identity == "username"
+        }
+
+    }
+
+    // ----- User Group tests -------------------------------------------------
+
+    def "create user group"() {
+
+        setup:
+        userGroupProvider.addGroup(!null as Group) >> {
+            Group g -> new Group.Builder().identifier(g.identifier).name(g.name).build()
+        }
+        accessPolicyProvider.getAccessPolicies() >> new HashSet<AccessPolicy>()  // needed for converting to DTO
+
+        when: "new group is created successfully"
+        def group = new UserGroup(null, "groupName")
+        UserGroup createdGroup = authorizationService.createUserGroup(group)
+
+        then: "created group has been assigned an identifier"
+        with(createdGroup) {
+            identifier != null
+            identity == "groupName"
+        }
+
+    }
+
+    def "list user groups"() {
+
+        setup:
+        userGroupProvider.getGroups() >> [
+                new Group.Builder().identifier("groupId1").name("groupName1").build(),
+                new Group.Builder().identifier("groupId2").name("groupName2").build(),
+                new Group.Builder().identifier("groupId3").name("groupName3").build(),
+        ]
+        accessPolicyProvider.getAccessPolicies() >> new HashSet<AccessPolicy>()
+
+        when: "list of groups is queried"
+        def groups = authorizationService.getUserGroups()
+
+        then: "groups are successfully returned as list of DTO objects"
+        groups != null
+        groups.size() == 3
+        with(groups[0]) {
+            identifier == "groupId1"
+            identity == "groupName1"
+        }
+        with(groups[1]) {
+            identifier == "groupId2"
+            identity == "groupName2"
+        }
+        with(groups[2]) {
+            identifier == "groupId3"
+            identity == "groupName3"
+        }
+
+    }
+
+    def "get user group"() {
+
+        setup:
+        accessPolicyProvider.getAccessPolicies() >> new HashSet<AccessPolicy>()
+
+
+        when: "get group for existing user identifier"
+        userGroupProvider.getGroup("groupId") >> new Group.Builder().identifier("groupId").name ("groupName").build()
+        def g1 = authorizationService.getUserGroup("groupId")
+
+        then: "group is returned converted to DTO"
+        with(g1) {
+            identifier == "groupId"
+            identity == "groupName"
+        }
+
+
+        when: "get group for non-existent group identifier"
+        userGroupProvider.getUser("nonExistentId") >> null
+        userGroupProvider.getGroup("nonExistentId") >> null
+        def g2 = authorizationService.getUserGroup("nonExistentId")
+
+        then: "no group is returned"
+        g2 == null
+
+    }
+
+    def "update user group"() {
+
+        setup:
+        userGroupProvider.updateGroup(!null as Group) >> {
+            Group g -> new Group.Builder().identifier(g.identifier).name(g.name).build()
+        }
+        accessPolicyProvider.getAccessPolicies() >> new HashSet<AccessPolicy>()
+
+
+        when: "group is updated"
+        def group = authorizationService.updateUserGroup(new UserGroup("id", "name"))
+
+        then: "updated group is returned converted to DTO"
+        with(group) {
+            identifier == "id"
+            identity == "name"
+        }
+
+    }
+
+    def "delete user group"() {
+
+        setup:
+        userGroupProvider.getGroup("id") >> new Group.Builder().identifier("id").name("name").build()
+        userGroupProvider.deleteGroup("id") >> new Group.Builder().identifier("id").name("name").build()
+        accessPolicyProvider.getAccessPolicies() >> new HashSet<AccessPolicy>()
+
+
+        when: "group is deleted"
+        def group = authorizationService.deleteUserGroup("id")
+
+        then: "deleted user is returned"
+        with(group) {
+            identifier == "id"
+            identity == "name"
+        }
+
+    }
+
+    // ----- Access Policy tests ----------------------------------------------
+
+    def "create access policy"() {
+
+        setup:
+        accessPolicyProvider.addAccessPolicy(!null as AuthAccessPolicy) >> {
+            AuthAccessPolicy p -> new AuthAccessPolicy.Builder()
+                    .identifier(p.identifier)
+                    .resource(p.resource)
+                    .action(p.action)
+                    .addGroups(p.groups)
+                    .addUsers(p.users)
+                    .build()
+        }
+        accessPolicyProvider.isConfigurable(_ as AuthAccessPolicy) >> true
+
+
+        when: "new access policy is created successfully"
+        def createdPolicy = authorizationService.createAccessPolicy(new AccessPolicy([resource: "/resource", action: "read"]))
+
+        then: "created policy has been assigned an identifier"
+        with(createdPolicy) {
+            identifier != null
+            resource == "/resource"
+            action == "read"
+            configurable == true
+        }
+
+    }
+
+    def "list access policies"() {
+
+        setup:
+        accessPolicyProvider.getAccessPolicies() >> [
+                new AuthAccessPolicy.Builder().identifier("ap1").resource("r1").action(RequestAction.READ).build(),
+                new AuthAccessPolicy.Builder().identifier("ap2").resource("r2").action(RequestAction.WRITE).build()
+        ]
+
+        when: "list access polices is queried"
+        def policies = authorizationService.getAccessPolicies()
+
+        then: "access policies are successfully returned as list of DTO objects"
+        policies != null
+        policies.size() == 2
+        with(policies[0]) {
+            identifier == "ap1"
+            resource == "r1"
+            action == RequestAction.READ.toString()
+        }
+        with(policies[1]) {
+            identifier == "ap2"
+            resource == "r2"
+            action == RequestAction.WRITE.toString()
+        }
+
+    }
+
+    def "get access policy"() {
+
+        when: "get policy for existing identifier"
+        accessPolicyProvider.getAccessPolicy("id") >> new AuthAccessPolicy.Builder()
+                .identifier("id")
+                .resource("/resource")
+                .action(RequestAction.READ)
+                .build()
+        def p1 = authorizationService.getAccessPolicy("id")
+
+        then: "policy is returned converted to DTO"
+        with(p1) {
+            identifier == "id"
+            resource == "/resource"
+            action == RequestAction.READ.toString()
+        }
+
+
+        when: "get policy for non-existent identifier"
+        accessPolicyProvider.getAccessPolicy("nonExistentId") >> null
+        def p2 = authorizationService.getAccessPolicy("nonExistentId")
+
+        then: "no policy is returned"
+        p2 == null
+
+    }
+
+
+    def "update access policy"() {
+
+        setup:
+        def users = [
+                "user1": "alice",
+                "user2": "bob",
+                "user3": "charlie" ]
+        def groups = [
+                "group1": "users",
+                "group2": "devs",
+                "group3": "admins" ]
+        def policies = [
+                "policy1": [
+                        "resource": "/resource1",
+                        "action": "read",
+                        "users": [ "user1" ],
+                        "groups": []
+                ]
+        ]
+        def mapDtoUser = { String id -> new User(id, users[id])}
+        def mapDtoGroup = { String id -> new UserGroup(id, groups[id])}
+        def mapAuthUser = { String id -> new AuthUser.Builder().identifier(id).identity(users[id]).build() }
+        def mapAuthGroup = { String id -> new Group.Builder().identifier(id).name(groups[id]).build() }
+        def mapAuthAccessPolicy = {
+            String id -> return new AuthAccessPolicy.Builder()
+                    .identifier(id)
+                    .resource(policies[id]["resource"] as String)
+                    .action(RequestAction.valueOfValue(policies[id]["action"] as String))
+                    .addUsers(policies[id]["users"] as Set<String>)
+                    .addGroups(policies[id]["groups"] as Set<String>)
+                    .build()
+        }
+        userGroupProvider.getUser(!null as String) >> { String id -> users.containsKey(id) ? mapAuthUser(id) : null }
+        userGroupProvider.getGroup(!null as String) >> { String id -> groups.containsKey(id) ? mapAuthGroup(id) : null }
+        userGroupProvider.getUsers() >> {
+            def authUsers = []
+            users.each{ k, v -> authUsers.add(new AuthUser.Builder().identifier(k).identity(v).build()) }
+            return authUsers
+        }
+        userGroupProvider.getGroups() >> {
+            def authGroups = []
+            users.each{ k, v -> authGroups.add(new Group.Builder().identifier(k).name(v).build()) }
+            return authGroups
+        }
+        accessPolicyProvider.getAccessPolicy(!null as String) >> { String id -> policies.containsKey(id) ? mapAuthAccessPolicy(id) : null }
+        accessPolicyProvider.updateAccessPolicy(!null as AuthAccessPolicy) >> {
+            AuthAccessPolicy p -> new AuthAccessPolicy.Builder()
+                    .identifier(p.identifier)
+                    .resource(p.resource)
+                    .action(p.action)
+                    .addGroups(p.groups)
+                    .addUsers(p.users)
+                    .build()
+        }
+        accessPolicyProvider.isConfigurable(_ as AuthAccessPolicy) >> true
+
+
+        when: "policy is updated"
+        def policy = new AccessPolicy([identifier: "policy1", resource: "/resource1", action: "read"])
+        policy.addUsers([mapDtoUser("user1"), mapDtoUser("user2")])
+        policy.addUserGroups([mapDtoGroup("group1")])
+        def p1 = authorizationService.updateAccessPolicy(policy)
+
+        then: "updated group is returned converted to DTO"
+        p1 != null
+        p1.users.size() == 2
+        def sortedUsers = p1.users.sort{it.identifier}
+        with(sortedUsers[0]) {
+            identifier == "user1"
+            identity == "alice"
+        }
+        with(sortedUsers[1]) {
+            identifier == "user2"
+            identity == "bob"
+        }
+        p1.userGroups.size() == 1
+        with(p1.userGroups[0]) {
+            identifier == "group1"
+            identity == "users"
+        }
+
+
+        when: "attempt to change policy resource and action"
+        def p2 = authorizationService.updateAccessPolicy(new AccessPolicy([identifier: "policy1", resource: "/newResource", action: "write"]))
+
+        then: "resource and action are unchanged"
+        with(p2) {
+            identifier == "policy1"
+            resource == "/resource1"
+            action == "read"
+        }
+
+    }
+
+    def "delete access policy"() {
+
+        setup:
+        userGroupProvider.getGroups() >> new HashSet<Group>()
+        userGroupProvider.getUsers() >> new HashSet<AuthUser>()
+        accessPolicyProvider.getAccessPolicy("id") >> {
+            String id -> new AuthAccessPolicy.Builder()
+                    .identifier("id")
+                    .resource("/resource")
+                    .action(RequestAction.READ)
+                    .addGroups(new HashSet<String>())
+                    .addUsers(new HashSet<String>())
+                    .build()
+        }
+        accessPolicyProvider.deleteAccessPolicy(!null as String) >> {
+            String id -> new AuthAccessPolicy.Builder()
+                    .identifier(id)
+                    .resource("/resource")
+                    .action(RequestAction.READ)
+                    .addGroups(new HashSet<String>())
+                    .addUsers(new HashSet<String>())
+                    .build()
+        }
+
+        when: "access policy is deleted"
+        def policy = authorizationService.deleteAccessPolicy("id")
+
+        then: "deleted policy is returned"
+        with(policy) {
+            identifier == "id"
+            resource == "/resource"
+            action == RequestAction.READ.toString()
+        }
+
+    }
+
+    // ----- Resource tests ---------------------------------------------------
+
+    def "get resources"() {
+
+        setup:
+        def buckets = [
+                "b1": [
+                        "name": "Bucket #1",
+                        "description": "An initial bucket for testing",
+                        "createdTimestamp": 1
+                ],
+                "b2": [
+                        "name": "Bucket #2",
+                        "description": "A second bucket for testing",
+                        "createdTimestamp": 2
+                ],
+        ]
+        def mapBucket = {
+            String id -> new Bucket([
+                    identifier: id,
+                    name: buckets[id]["name"] as String,
+                    description: buckets[id]["description"] as String]) }
+
+        registryService.getBuckets() >> {[ mapBucket("b1"), mapBucket("b2") ]}
+
+        when:
+        def resources = authorizationService.getResources()
+
+        then:
+        resources != null
+        resources.size() == 8
+        def sortedResources = resources.sort{it.identifier}
+        sortedResources[0].identifier == "/actuator"
+        sortedResources[1].identifier == "/buckets"
+        sortedResources[2].identifier == "/buckets/b1"
+        sortedResources[3].identifier == "/buckets/b2"
+        sortedResources[4].identifier == "/policies"
+        sortedResources[5].identifier == "/proxy"
+        sortedResources[6].identifier == "/swagger"
+        sortedResources[7].identifier == "/tenants"
+
+    }
+
+    def "get authorized resources"() {
+
+        setup:
+        def buckets = [
+                "b1": [
+                        "name": "Bucket #1",
+                        "description": "An initial bucket for testing",
+                        "createdTimestamp": 1
+                ],
+                "b2": [
+                        "name": "Bucket #2",
+                        "description": "A second bucket for testing",
+                        "createdTimestamp": 2
+                ],
+        ]
+        def mapBucket = {
+            String id -> new Bucket([
+                    identifier: id,
+                    name: buckets[id]["name"] as String,
+                    description: buckets[id]["description"] as String]) }
+
+        registryService.getBuckets() >> {[ mapBucket("b1"), mapBucket("b2") ]}
+
+        def authorized = Mock(Authorizable)
+        authorized.authorize(_, _, _) >> { return }
+        def denied = Mock(Authorizable)
+        denied.authorize(_, _, _) >> { throw new AccessDeniedException("") }
+
+        authorizableLookup.getAuthorizableByResource("/actuator")   >> denied
+        authorizableLookup.getAuthorizableByResource("/buckets")    >> authorized
+        authorizableLookup.getAuthorizableByResource("/buckets/b1") >> authorized
+        authorizableLookup.getAuthorizableByResource("/buckets/b2") >> denied
+        authorizableLookup.getAuthorizableByResource("/policies")   >> authorized
+        authorizableLookup.getAuthorizableByResource("/proxy")      >> denied
+        authorizableLookup.getAuthorizableByResource("/swagger")    >> denied
+        authorizableLookup.getAuthorizableByResource("/tenants")    >> authorized
+
+
+        when:
+        def resources = authorizationService.getAuthorizedResources(RequestAction.READ)
+
+        then:
+        resources != null
+        resources.size() == 4
+        def sortedResources = resources.sort{it.identifier}
+        sortedResources[0].identifier == "/buckets"
+        sortedResources[1].identifier == "/buckets/b1"
+        sortedResources[2].identifier == "/policies"
+        sortedResources[3].identifier == "/tenants"
+
+
+        when:
+        def filteredResources = authorizationService.getAuthorizedResources(RequestAction.READ, ResourceType.Bucket)
+
+        then:
+        filteredResources != null
+        filteredResources.size() == 2
+        def sortedFilteredResources = filteredResources.sort{it.identifier}
+        sortedFilteredResources[0].identifier == "/buckets"
+        sortedFilteredResources[1].identifier == "/buckets/b1"
+
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/6f26290d/nifi-registry-core/nifi-registry-framework/src/test/java/org/apache/nifi/registry/db/DatabaseBaseTest.java
----------------------------------------------------------------------
diff --git a/nifi-registry-core/nifi-registry-framework/src/test/java/org/apache/nifi/registry/db/DatabaseBaseTest.java b/nifi-registry-core/nifi-registry-framework/src/test/java/org/apache/nifi/registry/db/DatabaseBaseTest.java
new file mode 100644
index 0000000..02b04c0
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-framework/src/test/java/org/apache/nifi/registry/db/DatabaseBaseTest.java
@@ -0,0 +1,33 @@
+/*
+ * 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.db;
+
+import org.junit.runner.RunWith;
+import org.springframework.boot.test.context.SpringBootTest;
+import org.springframework.test.context.TestExecutionListeners;
+import org.springframework.test.context.junit4.SpringRunner;
+import org.springframework.test.context.support.DependencyInjectionTestExecutionListener;
+import org.springframework.test.context.transaction.TransactionalTestExecutionListener;
+import org.springframework.transaction.annotation.Transactional;
+
+@Transactional
+@RunWith(SpringRunner.class)
+@SpringBootTest(classes = DatabaseTestApplication.class, webEnvironment = SpringBootTest.WebEnvironment.NONE)
+@TestExecutionListeners({DependencyInjectionTestExecutionListener.class, TransactionalTestExecutionListener.class})
+public abstract class DatabaseBaseTest {
+
+}

http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/6f26290d/nifi-registry-core/nifi-registry-framework/src/test/java/org/apache/nifi/registry/db/DatabaseTestApplication.java
----------------------------------------------------------------------
diff --git a/nifi-registry-core/nifi-registry-framework/src/test/java/org/apache/nifi/registry/db/DatabaseTestApplication.java b/nifi-registry-core/nifi-registry-framework/src/test/java/org/apache/nifi/registry/db/DatabaseTestApplication.java
new file mode 100644
index 0000000..0ce3812
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-framework/src/test/java/org/apache/nifi/registry/db/DatabaseTestApplication.java
@@ -0,0 +1,52 @@
+/*
+ * 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.db;
+
+import org.apache.nifi.registry.properties.NiFiRegistryProperties;
+import org.mockito.Mockito;
+import org.springframework.boot.SpringApplication;
+import org.springframework.boot.autoconfigure.SpringBootApplication;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.ComponentScan;
+import org.springframework.context.annotation.FilterType;
+
+/**
+ * Sets up the application context for database repository tests.
+ *
+ * The @SpringBootTest annotation on the repository tests will find this class by working up the package hierarchy.
+ * This class must be in the "db" package in order to find the entities in "db.entity" and repositories in "db.repository".
+ *
+ * The DataSourceFactory is excluded so that Spring Boot will load an in-memory H2 database.
+ */
+@SpringBootApplication
+@ComponentScan(
+        excludeFilters = {
+                @ComponentScan.Filter(
+                        type = FilterType.ASSIGNABLE_TYPE,
+                        value = DataSourceFactory.class)
+        })
+public class DatabaseTestApplication {
+
+    public static void main(String[] args) {
+        SpringApplication.run(DatabaseTestApplication.class, args);
+    }
+
+    @Bean
+    public NiFiRegistryProperties createNiFiRegistryProperties() {
+        return Mockito.mock(NiFiRegistryProperties.class);
+    }
+}

http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/6f26290d/nifi-registry-core/nifi-registry-framework/src/test/java/org/apache/nifi/registry/db/TestDatabaseKeyService.java
----------------------------------------------------------------------
diff --git a/nifi-registry-core/nifi-registry-framework/src/test/java/org/apache/nifi/registry/db/TestDatabaseKeyService.java b/nifi-registry-core/nifi-registry-framework/src/test/java/org/apache/nifi/registry/db/TestDatabaseKeyService.java
new file mode 100644
index 0000000..d0ab56a
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-framework/src/test/java/org/apache/nifi/registry/db/TestDatabaseKeyService.java
@@ -0,0 +1,76 @@
+/*
+ * 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.db;
+
+import org.apache.nifi.registry.security.key.Key;
+import org.apache.nifi.registry.security.key.KeyService;
+import org.junit.Test;
+import org.springframework.beans.factory.annotation.Autowired;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
+
+public class TestDatabaseKeyService extends DatabaseBaseTest {
+
+    @Autowired
+    private KeyService keyService;
+
+    @Test
+    public void testGetKeyByIdWhenExists() {
+        final Key existingKey = keyService.getKey("1");
+        assertNotNull(existingKey);
+        assertEquals("1", existingKey.getId());
+        assertEquals("unit_test_tenant_identity", existingKey.getIdentity());
+        assertEquals("0123456789abcdef", existingKey.getKey());
+    }
+
+    @Test
+    public void testGetKeyByIdWhenDoesNotExist() {
+        final Key existingKey = keyService.getKey("2");
+        assertNull(existingKey);
+    }
+
+    @Test
+    public void testGetOrCreateKeyWhenExists() {
+        final Key existingKey = keyService.getOrCreateKey("unit_test_tenant_identity");
+        assertNotNull(existingKey);
+        assertEquals("1", existingKey.getId());
+        assertEquals("unit_test_tenant_identity", existingKey.getIdentity());
+        assertEquals("0123456789abcdef", existingKey.getKey());
+    }
+
+    @Test
+    public void testGetOrCreateKeyWhenDoesNotExist() {
+        final Key createdKey = keyService.getOrCreateKey("does-not-exist");
+        assertNotNull(createdKey);
+        assertNotNull(createdKey.getId());
+        assertEquals("does-not-exist", createdKey.getIdentity());
+        assertNotNull(createdKey.getKey());
+    }
+
+    @Test
+    public void testDeleteKeyWhenExists() {
+        final Key existingKey = keyService.getKey("1");
+        assertNotNull(existingKey);
+
+        keyService.deleteKey(existingKey.getIdentity());
+
+        final Key deletedKey = keyService.getKey("1");
+        assertNull(deletedKey);
+    }
+}

http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/6f26290d/nifi-registry-core/nifi-registry-framework/src/test/java/org/apache/nifi/registry/db/TestDatabaseMetadataService.java
----------------------------------------------------------------------
diff --git a/nifi-registry-core/nifi-registry-framework/src/test/java/org/apache/nifi/registry/db/TestDatabaseMetadataService.java b/nifi-registry-core/nifi-registry-framework/src/test/java/org/apache/nifi/registry/db/TestDatabaseMetadataService.java
new file mode 100644
index 0000000..35ba757
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-framework/src/test/java/org/apache/nifi/registry/db/TestDatabaseMetadataService.java
@@ -0,0 +1,386 @@
+/*
+ * 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.db;
+
+import org.apache.nifi.registry.db.entity.BucketEntity;
+import org.apache.nifi.registry.db.entity.BucketItemEntity;
+import org.apache.nifi.registry.db.entity.BucketItemEntityType;
+import org.apache.nifi.registry.db.entity.FlowEntity;
+import org.apache.nifi.registry.db.entity.FlowSnapshotEntity;
+import org.apache.nifi.registry.service.MetadataService;
+import org.junit.Test;
+import org.springframework.beans.factory.annotation.Autowired;
+
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.Date;
+import java.util.HashSet;
+import java.util.List;
+import java.util.UUID;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertTrue;
+
+public class TestDatabaseMetadataService extends DatabaseBaseTest {
+
+    @Autowired
+    private MetadataService metadataService;
+
+    //----------------- Buckets ---------------------------------
+
+    @Test
+    public void testCreateAndGetBucket() {
+        final BucketEntity b = new BucketEntity();
+        b.setId("testBucketId");
+        b.setName("testBucketName");
+        b.setDescription("testBucketDesc");
+        b.setCreated(new Date());
+
+        metadataService.createBucket(b);
+
+        final BucketEntity createdBucket = metadataService.getBucketById(b.getId());
+        assertNotNull(createdBucket);
+        assertEquals(b.getId(), createdBucket.getId());
+        assertEquals(b.getName(), createdBucket.getName());
+        assertEquals(b.getDescription(), createdBucket.getDescription());
+        assertEquals(b.getCreated(), createdBucket.getCreated());
+    }
+
+    @Test
+    public void testGetBucketDoesNotExist() {
+        final BucketEntity bucket = metadataService.getBucketById("does-not-exist");
+        assertNull(bucket);
+    }
+
+    @Test
+    public void testGetBucketsByName() {
+        final List<BucketEntity> buckets = metadataService.getBucketsByName("Bucket 1");
+        assertNotNull(buckets);
+        assertEquals(1, buckets.size());
+        assertEquals("Bucket 1", buckets.get(0).getName());
+    }
+
+    @Test
+    public void testGetBucketsByNameNoneFound() {
+        final List<BucketEntity> buckets = metadataService.getBucketsByName("Bucket XYZ");
+        assertNotNull(buckets);
+        assertEquals(0, buckets.size());
+    }
+
+    @Test
+    public void testUpdateBucket() {
+        final BucketEntity bucket = metadataService.getBucketById("1");
+        assertNotNull(bucket);
+
+        final String updatedName = bucket.getName() + " UPDATED";
+        final String updatedDesc = bucket.getDescription() + "DESC";
+
+        bucket.setName(updatedName);
+        bucket.setDescription(updatedDesc);
+
+        metadataService.updateBucket(bucket);
+
+        final BucketEntity updatedBucket = metadataService.getBucketById(bucket.getId());
+        assertNotNull(updatedName);
+        assertEquals(updatedName, updatedBucket.getName());
+        assertEquals(updatedDesc, updatedBucket.getDescription());
+    }
+
+    @Test
+    public void testDeleteBucketNoChildren() {
+        final BucketEntity bucket = metadataService.getBucketById("6");
+        assertNotNull(bucket);
+
+        metadataService.deleteBucket(bucket);
+
+        final BucketEntity deletedBucket = metadataService.getBucketById("6");
+        assertNull(deletedBucket);
+    }
+
+    @Test
+    public void testDeleteBucketWithChildren() {
+        final BucketEntity bucket = metadataService.getBucketById("1");
+        assertNotNull(bucket);
+
+        metadataService.deleteBucket(bucket);
+
+        final BucketEntity deletedBucket = metadataService.getBucketById("1");
+        assertNull(deletedBucket);
+    }
+
+    @Test
+    public void testGetBucketsForIds() {
+        final List<BucketEntity> buckets = metadataService.getBuckets(new HashSet<>(Arrays.asList("1", "2")));
+        assertNotNull(buckets);
+        assertEquals(2, buckets.size());
+        assertEquals("1", buckets.get(0).getId());
+        assertEquals("2", buckets.get(1).getId());
+    }
+
+    @Test
+    public void testGetAllBuckets() {
+        final List<BucketEntity> buckets = metadataService.getAllBuckets();
+        assertNotNull(buckets);
+        assertEquals(6, buckets.size());
+    }
+
+    //----------------- BucketItems ---------------------------------
+
+    @Test
+    public void testGetBucketItemsForBucket() {
+        final BucketEntity bucket = metadataService.getBucketById("1");
+        assertNotNull(bucket);
+
+        final List<BucketItemEntity> items = metadataService.getBucketItems(bucket.getId());
+        assertNotNull(items);
+        assertEquals(2, items.size());
+
+        items.stream().forEach(i -> assertNotNull(i.getBucketName()));
+    }
+
+    @Test
+    public void testGetBucketItemsForBuckets() {
+        final List<BucketItemEntity> items = metadataService.getBucketItems(new HashSet<>(Arrays.asList("1", "2")));
+        assertNotNull(items);
+        assertEquals(3, items.size());
+
+        items.stream().forEach(i -> assertNotNull(i.getBucketName()));
+    }
+
+    @Test
+    public void testGetItemsWithCounts() {
+        final List<BucketItemEntity> items = metadataService.getBucketItems(new HashSet<>(Arrays.asList("1", "2")));
+        assertNotNull(items);
+
+        // 3 items across all buckets
+        assertEquals(3, items.size());
+
+        final BucketItemEntity item1 = items.stream().filter(i -> i.getId().equals("1")).findFirst().orElse(null);
+        assertNotNull(item1);
+        assertEquals(BucketItemEntityType.FLOW, item1.getType());
+
+        final FlowEntity flowEntity = (FlowEntity) item1;
+        assertEquals(3, flowEntity.getSnapshotCount());
+
+        items.stream().forEach(i -> assertNotNull(i.getBucketName()));
+    }
+
+    @Test
+    public void testGetItemsWithCountsFilteredByBuckets() {
+        final List<BucketItemEntity> items = metadataService.getBucketItems(Collections.singleton("1"));
+        assertNotNull(items);
+
+        // only 2 items in bucket 1
+        assertEquals(2, items.size());
+
+        final BucketItemEntity item1 = items.stream().filter(i -> i.getId().equals("1")).findFirst().orElse(null);
+        assertNotNull(item1);
+        assertEquals(BucketItemEntityType.FLOW, item1.getType());
+
+        final FlowEntity flowEntity = (FlowEntity) item1;
+        assertEquals(3, flowEntity.getSnapshotCount());
+
+        items.stream().forEach(i -> assertNotNull(i.getBucketName()));
+    }
+
+    //----------------- Flows ---------------------------------
+
+    @Test
+    public void testGetFlowByIdWhenExists() {
+        final FlowEntity flow = metadataService.getFlowById("1");
+        assertNotNull(flow);
+        assertEquals("1", flow.getId());
+        assertEquals("1", flow.getBucketId());
+    }
+
+    @Test
+    public void testGetFlowByIdWhenDoesNotExist() {
+        final FlowEntity flow = metadataService.getFlowById("does-not-exist");
+        assertNull(flow);
+    }
+
+    @Test
+    public void testCreateFlow() {
+        final String bucketId = "1";
+
+        final FlowEntity flow = new FlowEntity();
+        flow.setId(UUID.randomUUID().toString());
+        flow.setBucketId(bucketId);
+        flow.setName("Test Flow 1");
+        flow.setDescription("Description for Test Flow 1");
+        flow.setCreated(new Date());
+        flow.setModified(new Date());
+        flow.setType(BucketItemEntityType.FLOW);
+
+        metadataService.createFlow(flow);
+
+        final FlowEntity createdFlow = metadataService.getFlowById(flow.getId());
+        assertNotNull(flow);
+        assertEquals(flow.getId(), createdFlow.getId());
+        assertEquals(flow.getBucketId(), createdFlow.getBucketId());
+        assertEquals(flow.getName(), createdFlow.getName());
+        assertEquals(flow.getDescription(), createdFlow.getDescription());
+        assertEquals(flow.getCreated(), createdFlow.getCreated());
+        assertEquals(flow.getModified(), createdFlow.getModified());
+        assertEquals(flow.getType(), createdFlow.getType());
+    }
+
+    @Test
+    public void testGetFlowByIdWithSnapshotCount() {
+       final FlowEntity flowEntity = metadataService.getFlowByIdWithSnapshotCounts("1");
+        assertNotNull(flowEntity);
+        assertEquals(3, flowEntity.getSnapshotCount());
+    }
+
+    @Test
+    public void testGetFlowsByBucket() {
+        final BucketEntity bucketEntity = metadataService.getBucketById("1");
+        final List<FlowEntity> flows = metadataService.getFlowsByBucket(bucketEntity.getId());
+        assertEquals(2, flows.size());
+
+        final FlowEntity flowEntity = flows.stream().filter(f -> f.getId().equals("1")).findFirst().orElse(null);
+        assertNotNull(flowEntity);
+        assertEquals(3, flowEntity.getSnapshotCount());
+    }
+
+    @Test
+    public void testGetFlowsByName() {
+        final List<FlowEntity> flows = metadataService.getFlowsByName("Flow 1");
+        assertNotNull(flows);
+        assertEquals(2, flows.size());
+        assertEquals("Flow 1", flows.get(0).getName());
+        assertEquals("Flow 1", flows.get(1).getName());
+    }
+
+    @Test
+    public void testGetFlowsByNameByBucket() {
+        final List<FlowEntity> flows = metadataService.getFlowsByName("2","Flow 1");
+        assertNotNull(flows);
+        assertEquals(1, flows.size());
+        assertEquals("Flow 1", flows.get(0).getName());
+        assertEquals("2", flows.get(0).getBucketId());
+    }
+
+    @Test
+    public void testUpdateFlow() {
+        final FlowEntity flow = metadataService.getFlowById("1");
+        assertNotNull(flow);
+
+        final Date originalModified = flow.getModified();
+
+        flow.setName(flow.getName() + " UPDATED");
+        flow.setDescription(flow.getDescription() + " UPDATED");
+
+        metadataService.updateFlow(flow);
+
+        final FlowEntity updatedFlow = metadataService.getFlowById( "1");
+        assertNotNull(flow);
+        assertEquals(flow.getName(), updatedFlow.getName());
+        assertEquals(flow.getDescription(), updatedFlow.getDescription());
+        assertEquals(flow.getModified().getTime(), updatedFlow.getModified().getTime());
+        assertTrue(updatedFlow.getModified().getTime() > originalModified.getTime());
+    }
+
+    @Test
+    public void testDeleteFlowWithSnapshots() {
+        final FlowEntity flow = metadataService.getFlowById( "1");
+        assertNotNull(flow);
+
+        metadataService.deleteFlow(flow);
+
+        final FlowEntity deletedFlow = metadataService.getFlowById("1");
+        assertNull(deletedFlow);
+    }
+
+    //----------------- FlowSnapshots ---------------------------------
+
+    @Test
+    public void testGetFlowSnapshot() {
+        final FlowSnapshotEntity entity = metadataService.getFlowSnapshot( "1", 1);
+        assertNotNull(entity);
+        assertEquals("1", entity.getFlowId());
+        assertEquals(1, entity.getVersion().intValue());
+    }
+
+    @Test
+    public void testGetFlowSnapshotDoesNotExist() {
+        final FlowSnapshotEntity entity = metadataService.getFlowSnapshot( "DOES-NOT-EXIST", 1);
+        assertNull(entity);
+    }
+
+    @Test
+    public void testCreateFlowSnapshot() {
+        final FlowSnapshotEntity flowSnapshot = new FlowSnapshotEntity();
+        flowSnapshot.setFlowId("1");
+        flowSnapshot.setVersion(4);
+        flowSnapshot.setCreated(new Date());
+        flowSnapshot.setCreatedBy("test-user");
+        flowSnapshot.setComments("Comments");
+
+        metadataService.createFlowSnapshot(flowSnapshot);
+
+        final FlowSnapshotEntity createdFlowSnapshot = metadataService.getFlowSnapshot(flowSnapshot.getFlowId(), flowSnapshot.getVersion());
+        assertNotNull(createdFlowSnapshot);
+        assertEquals(flowSnapshot.getFlowId(), createdFlowSnapshot.getFlowId());
+        assertEquals(flowSnapshot.getVersion(), createdFlowSnapshot.getVersion());
+        assertEquals(flowSnapshot.getComments(), createdFlowSnapshot.getComments());
+        assertEquals(flowSnapshot.getCreated(), createdFlowSnapshot.getCreated());
+        assertEquals(flowSnapshot.getCreatedBy(), createdFlowSnapshot.getCreatedBy());
+    }
+
+    @Test
+    public void testGetLatestSnapshot() {
+        final FlowSnapshotEntity latest = metadataService.getLatestSnapshot("1");
+        assertNotNull(latest);
+        assertEquals("1", latest.getFlowId());
+        assertEquals(3, latest.getVersion().intValue());
+    }
+
+    @Test
+    public void testGetLatestSnapshotDoesNotExist() {
+        final FlowSnapshotEntity latest = metadataService.getLatestSnapshot("DOES-NOT-EXIST");
+        assertNull(latest);
+    }
+
+    @Test
+    public void testGetFlowSnapshots() {
+        final List<FlowSnapshotEntity> flowSnapshots = metadataService.getSnapshots( "1");
+        assertNotNull(flowSnapshots);
+        assertEquals(3, flowSnapshots.size());
+    }
+
+    @Test
+    public void testGetFlowSnapshotsNoneFound() {
+        final List<FlowSnapshotEntity> flowSnapshots = metadataService.getSnapshots( "2");
+        assertNotNull(flowSnapshots);
+        assertEquals(0, flowSnapshots.size());
+    }
+
+    @Test
+    public void testDeleteFlowSnapshot() {
+        final FlowSnapshotEntity entity = metadataService.getFlowSnapshot( "1", 1);
+        assertNotNull(entity);
+
+        metadataService.deleteFlowSnapshot(entity);
+
+        final FlowSnapshotEntity deletedEntity = metadataService.getFlowSnapshot( "1", 1);
+        assertNull(deletedEntity);
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/6f26290d/nifi-registry-core/nifi-registry-framework/src/test/java/org/apache/nifi/registry/db/migration/TestLegacyDatabaseService.java
----------------------------------------------------------------------
diff --git a/nifi-registry-core/nifi-registry-framework/src/test/java/org/apache/nifi/registry/db/migration/TestLegacyDatabaseService.java b/nifi-registry-core/nifi-registry-framework/src/test/java/org/apache/nifi/registry/db/migration/TestLegacyDatabaseService.java
new file mode 100644
index 0000000..df37e8e
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-framework/src/test/java/org/apache/nifi/registry/db/migration/TestLegacyDatabaseService.java
@@ -0,0 +1,141 @@
+/*
+ * 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.db.migration;
+
+import org.apache.nifi.registry.db.entity.BucketItemEntityType;
+import org.flywaydb.core.Flyway;
+import org.junit.Before;
+import org.junit.Test;
+import org.springframework.boot.jdbc.DataSourceBuilder;
+import org.springframework.jdbc.core.JdbcTemplate;
+
+import javax.sql.DataSource;
+import java.util.Date;
+import java.util.List;
+
+import static org.junit.Assert.assertEquals;
+
+/**
+ * Test running the legacy Flyway migrations against an in-memory H2 and then using the LegacyDatabaseService to
+ * retrieve data. Purposely not using Spring test annotations here to avoid interfering with the normal DB context/flyway.
+ */
+public class TestLegacyDatabaseService {
+
+    private DataSource dataSource;
+    private JdbcTemplate jdbcTemplate;
+    private Flyway flyway;
+
+    private BucketEntityV1 bucketEntityV1;
+    private FlowEntityV1 flowEntityV1;
+    private FlowSnapshotEntityV1 flowSnapshotEntityV1;
+
+    @Before
+    public void setup() {
+        dataSource = DataSourceBuilder.create()
+                .url("jdbc:h2:mem:legacydb")
+                .driverClassName("org.h2.Driver")
+                .build();
+
+        jdbcTemplate = new JdbcTemplate(dataSource);
+
+        flyway = new Flyway();
+        flyway.setDataSource(dataSource);
+        flyway.setLocations("db/original");
+        flyway.migrate();
+
+        bucketEntityV1 = new BucketEntityV1();
+        bucketEntityV1.setId("1");
+        bucketEntityV1.setName("Bucket1");
+        bucketEntityV1.setDescription("This is bucket 1");
+        bucketEntityV1.setCreated(new Date());
+
+        jdbcTemplate.update("INSERT INTO bucket (ID, NAME, DESCRIPTION, CREATED) VALUES (?, ?, ?, ?)",
+                bucketEntityV1.getId(),
+                bucketEntityV1.getName(),
+                bucketEntityV1.getDescription(),
+                bucketEntityV1.getCreated());
+
+        flowEntityV1 = new FlowEntityV1();
+        flowEntityV1.setId("1");
+        flowEntityV1.setBucketId(bucketEntityV1.getId());
+        flowEntityV1.setName("Flow1");
+        flowEntityV1.setDescription("This is flow1");
+        flowEntityV1.setCreated(new Date());
+        flowEntityV1.setModified(new Date());
+
+        jdbcTemplate.update("INSERT INTO bucket_item (ID, NAME, DESCRIPTION, CREATED, MODIFIED, ITEM_TYPE, BUCKET_ID) VALUES (?, ?, ?, ?, ?, ?, ?)",
+                flowEntityV1.getId(),
+                flowEntityV1.getName(),
+                flowEntityV1.getDescription(),
+                flowEntityV1.getCreated(),
+                flowEntityV1.getModified(),
+                BucketItemEntityType.FLOW.toString(),
+                flowEntityV1.getBucketId());
+
+        jdbcTemplate.update("INSERT INTO flow (ID) VALUES (?)", flowEntityV1.getId());
+
+        flowSnapshotEntityV1 = new FlowSnapshotEntityV1();
+        flowSnapshotEntityV1.setFlowId(flowEntityV1.getId());
+        flowSnapshotEntityV1.setVersion(1);
+        flowSnapshotEntityV1.setComments("This is v1");
+        flowSnapshotEntityV1.setCreated(new Date());
+        flowSnapshotEntityV1.setCreatedBy("user1");
+
+        jdbcTemplate.update("INSERT INTO flow_snapshot (FLOW_ID, VERSION, CREATED, CREATED_BY, COMMENTS) VALUES (?, ?, ?, ?, ?)",
+                flowSnapshotEntityV1.getFlowId(),
+                flowSnapshotEntityV1.getVersion(),
+                flowSnapshotEntityV1.getCreated(),
+                flowSnapshotEntityV1.getCreatedBy(),
+                flowSnapshotEntityV1.getComments());
+    }
+
+    @Test
+    public void testGetLegacyData() {
+        final LegacyDatabaseService service = new LegacyDatabaseService(dataSource);
+
+        final List<BucketEntityV1> buckets = service.getAllBuckets();
+        assertEquals(1, buckets.size());
+
+        final BucketEntityV1 b = buckets.stream().findFirst().get();
+        assertEquals(bucketEntityV1.getId(), b.getId());
+        assertEquals(bucketEntityV1.getName(), b.getName());
+        assertEquals(bucketEntityV1.getDescription(), b.getDescription());
+        assertEquals(bucketEntityV1.getCreated(), b.getCreated());
+
+        final List<FlowEntityV1> flows = service.getAllFlows();
+        assertEquals(1, flows.size());
+
+        final FlowEntityV1 f = flows.stream().findFirst().get();
+        assertEquals(flowEntityV1.getId(), f.getId());
+        assertEquals(flowEntityV1.getName(), f.getName());
+        assertEquals(flowEntityV1.getDescription(), f.getDescription());
+        assertEquals(flowEntityV1.getCreated(), f.getCreated());
+        assertEquals(flowEntityV1.getModified(), f.getModified());
+        assertEquals(flowEntityV1.getBucketId(), f.getBucketId());
+
+        final List<FlowSnapshotEntityV1> flowSnapshots = service.getAllFlowSnapshots();
+        assertEquals(1, flowSnapshots.size());
+
+        final FlowSnapshotEntityV1 fs = flowSnapshots.stream().findFirst().get();
+        assertEquals(flowSnapshotEntityV1.getFlowId(), fs.getFlowId());
+        assertEquals(flowSnapshotEntityV1.getVersion(), fs.getVersion());
+        assertEquals(flowSnapshotEntityV1.getComments(), fs.getComments());
+        assertEquals(flowSnapshotEntityV1.getCreatedBy(), fs.getCreatedBy());
+        assertEquals(flowSnapshotEntityV1.getCreated(), fs.getCreated());
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/6f26290d/nifi-registry-core/nifi-registry-framework/src/test/java/org/apache/nifi/registry/db/migration/TestLegacyEntityMapper.java
----------------------------------------------------------------------
diff --git a/nifi-registry-core/nifi-registry-framework/src/test/java/org/apache/nifi/registry/db/migration/TestLegacyEntityMapper.java b/nifi-registry-core/nifi-registry-framework/src/test/java/org/apache/nifi/registry/db/migration/TestLegacyEntityMapper.java
new file mode 100644
index 0000000..3de297f
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-framework/src/test/java/org/apache/nifi/registry/db/migration/TestLegacyEntityMapper.java
@@ -0,0 +1,79 @@
+/*
+ * 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.db.migration;
+
+import org.apache.nifi.registry.db.entity.BucketEntity;
+import org.apache.nifi.registry.db.entity.FlowEntity;
+import org.apache.nifi.registry.db.entity.FlowSnapshotEntity;
+import org.junit.Test;
+
+import java.util.Date;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+
+public class TestLegacyEntityMapper {
+
+    @Test
+    public void testMapLegacyEntities() {
+        final BucketEntityV1 bucketEntityV1 = new BucketEntityV1();
+        bucketEntityV1.setId("1");
+        bucketEntityV1.setName("Bucket1");
+        bucketEntityV1.setDescription("This is bucket 1");
+        bucketEntityV1.setCreated(new Date());
+
+        final BucketEntity bucketEntity = LegacyEntityMapper.createBucketEntity(bucketEntityV1);
+        assertNotNull(bucketEntity);
+        assertEquals(bucketEntityV1.getId(), bucketEntity.getId());
+        assertEquals(bucketEntityV1.getName(), bucketEntity.getName());
+        assertEquals(bucketEntityV1.getDescription(), bucketEntity.getDescription());
+        assertEquals(bucketEntityV1.getCreated(), bucketEntity.getCreated());
+
+        final FlowEntityV1 flowEntityV1 = new FlowEntityV1();
+        flowEntityV1.setId("1");
+        flowEntityV1.setBucketId(bucketEntityV1.getId());
+        flowEntityV1.setName("Flow1");
+        flowEntityV1.setDescription("This is flow1");
+        flowEntityV1.setCreated(new Date());
+        flowEntityV1.setModified(new Date());
+
+        final FlowEntity flowEntity = LegacyEntityMapper.createFlowEntity(flowEntityV1);
+        assertNotNull(flowEntity);
+        assertEquals(flowEntityV1.getId(), flowEntity.getId());
+        assertEquals(flowEntityV1.getBucketId(), flowEntity.getBucketId());
+        assertEquals(flowEntityV1.getName(), flowEntity.getName());
+        assertEquals(flowEntityV1.getDescription(), flowEntity.getDescription());
+        assertEquals(flowEntityV1.getCreated(), flowEntity.getCreated());
+        assertEquals(flowEntityV1.getModified(), flowEntity.getModified());
+
+        final FlowSnapshotEntityV1 flowSnapshotEntityV1 = new FlowSnapshotEntityV1();
+        flowSnapshotEntityV1.setFlowId(flowEntityV1.getId());
+        flowSnapshotEntityV1.setVersion(1);
+        flowSnapshotEntityV1.setComments("This is v1");
+        flowSnapshotEntityV1.setCreated(new Date());
+        flowSnapshotEntityV1.setCreatedBy("user1");
+
+        final FlowSnapshotEntity flowSnapshotEntity = LegacyEntityMapper.createFlowSnapshotEntity(flowSnapshotEntityV1);
+        assertNotNull(flowSnapshotEntity);
+        assertEquals(flowSnapshotEntityV1.getFlowId(), flowSnapshotEntity.getFlowId());
+        assertEquals(flowSnapshotEntityV1.getVersion(), flowSnapshotEntity.getVersion());
+        assertEquals(flowSnapshotEntityV1.getComments(), flowSnapshotEntity.getComments());
+        assertEquals(flowSnapshotEntityV1.getCreatedBy(), flowSnapshotEntity.getCreatedBy());
+        assertEquals(flowSnapshotEntityV1.getCreated(), flowSnapshotEntity.getCreated());
+    }
+
+}


[20/51] [partial] nifi-registry git commit: NIFIREG-201 Refactoring project structure to better isolate extensions

Posted by kd...@apache.org.
http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/6f26290d/nifi-registry-core/nifi-registry-security-api/src/main/java/org/apache/nifi/registry/security/authorization/ConfigurableAccessPolicyProvider.java
----------------------------------------------------------------------
diff --git a/nifi-registry-core/nifi-registry-security-api/src/main/java/org/apache/nifi/registry/security/authorization/ConfigurableAccessPolicyProvider.java b/nifi-registry-core/nifi-registry-security-api/src/main/java/org/apache/nifi/registry/security/authorization/ConfigurableAccessPolicyProvider.java
new file mode 100644
index 0000000..1f909a4
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-security-api/src/main/java/org/apache/nifi/registry/security/authorization/ConfigurableAccessPolicyProvider.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.security.authorization;
+
+import org.apache.nifi.registry.security.authorization.exception.AuthorizationAccessException;
+import org.apache.nifi.registry.security.authorization.exception.UninheritableAuthorizationsException;
+
+/**
+ * Provides support for configuring AccessPolicies.
+ *
+ * NOTE: Extensions will be called often and frequently. Because of this, if the underlying implementation needs to
+ * make remote calls or expensive calculations those should probably be done asynchronously and/or cache the results.
+ *
+ * Additionally, extensions need to be thread safe.
+ */
+public interface ConfigurableAccessPolicyProvider extends AccessPolicyProvider {
+
+    /**
+     * Returns a fingerprint representing the authorizations managed by this authorizer. The fingerprint will be
+     * used for comparison to determine if two policy-based authorizers represent a compatible set of policies.
+     *
+     * @return the fingerprint for this Authorizer
+     * @throws AuthorizationAccessException if there was an unexpected error performing the operation
+     */
+    String getFingerprint() throws AuthorizationAccessException;
+
+    /**
+     * Parses the fingerprint and adds any policies to the current AccessPolicyProvider.
+     *
+     * @param fingerprint the fingerprint that was obtained from calling getFingerprint() on another Authorizer.
+     * @throws AuthorizationAccessException if there was an unexpected error performing the operation
+     */
+    void inheritFingerprint(final String fingerprint) throws AuthorizationAccessException;
+
+    /**
+     * When the fingerprints are not equal, this method will check if the proposed fingerprint is inheritable.
+     * If the fingerprint is an exact match, this method will not be invoked as there is nothing to inherit.
+     *
+     * @throws AuthorizationAccessException if there was an unexpected error performing the operation
+     * @throws UninheritableAuthorizationsException if the proposed fingerprint was uninheritable
+     */
+    void checkInheritability(final String proposedFingerprint) throws AuthorizationAccessException, UninheritableAuthorizationsException;
+
+    /**
+     * Adds the given policy ensuring that multiple policies can not be added for the same resource and action.
+     *
+     * @param accessPolicy the policy to add
+     * @return the policy that was added
+     * @throws AuthorizationAccessException if there was an unexpected error performing the operation
+     */
+    AccessPolicy addAccessPolicy(AccessPolicy accessPolicy) throws AuthorizationAccessException;
+
+    /**
+     * Determines whether the specified access policy is configurable. Provides the opportunity for a ConfigurableAccessPolicyProvider to prevent
+     * editing of a specific access policy. By default, all known access policies are configurable.
+     *
+     * @param accessPolicy the access policy
+     * @return is configurable
+     */
+    default boolean isConfigurable(AccessPolicy accessPolicy) {
+        if (accessPolicy == null) {
+            throw new IllegalArgumentException("Access policy cannot be null");
+        }
+
+        return getAccessPolicy(accessPolicy.getIdentifier()) != null;
+    }
+
+    /**
+     * The policy represented by the provided instance will be updated based on the provided instance.
+     *
+     * @param accessPolicy an updated policy
+     * @return the updated policy, or null if no matching policy was found
+     * @throws AuthorizationAccessException if there was an unexpected error performing the operation
+     */
+    AccessPolicy updateAccessPolicy(AccessPolicy accessPolicy) throws AuthorizationAccessException;
+
+    /**
+     * Deletes the given policy.
+     *
+     * @param accessPolicy the policy to delete
+     * @return the deleted policy, or null if no matching policy was found
+     * @throws AuthorizationAccessException if there was an unexpected error performing the operation
+     */
+    AccessPolicy deleteAccessPolicy(AccessPolicy accessPolicy) throws AuthorizationAccessException;
+
+    /**
+     * Deletes the policy with the specified identifier.
+     *
+     * @param accessPolicyIdentifier the policy to delete
+     * @return the deleted policy, or null if no matching policy was found
+     * @throws AuthorizationAccessException if there was an unexpected error performing the operation
+     */
+    AccessPolicy deleteAccessPolicy(String accessPolicyIdentifier) throws AuthorizationAccessException;
+}

http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/6f26290d/nifi-registry-core/nifi-registry-security-api/src/main/java/org/apache/nifi/registry/security/authorization/ConfigurableUserGroupProvider.java
----------------------------------------------------------------------
diff --git a/nifi-registry-core/nifi-registry-security-api/src/main/java/org/apache/nifi/registry/security/authorization/ConfigurableUserGroupProvider.java b/nifi-registry-core/nifi-registry-security-api/src/main/java/org/apache/nifi/registry/security/authorization/ConfigurableUserGroupProvider.java
new file mode 100644
index 0000000..bd52128
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-security-api/src/main/java/org/apache/nifi/registry/security/authorization/ConfigurableUserGroupProvider.java
@@ -0,0 +1,163 @@
+/*
+ * 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.security.authorization;
+
+import org.apache.nifi.registry.security.authorization.exception.AuthorizationAccessException;
+import org.apache.nifi.registry.security.authorization.exception.UninheritableAuthorizationsException;
+
+/**
+ * Provides support for configuring Users and Groups.
+ *
+ * NOTE: Extensions will be called often and frequently. Because of this, if the underlying implementation needs to
+ * make remote calls or expensive calculations those should probably be done asynchronously and/or cache the results.
+ *
+ * Additionally, extensions need to be thread safe.
+ */
+public interface ConfigurableUserGroupProvider extends UserGroupProvider {
+
+    /**
+     * Returns a fingerprint representing the authorizations managed by this authorizer. The fingerprint will be
+     * used for comparison to determine if two policy-based authorizers represent a compatible set of users and/or groups.
+     *
+     * @return the fingerprint for this Authorizer
+     * @throws AuthorizationAccessException if there was an unexpected error performing the operation
+     */
+    String getFingerprint() throws AuthorizationAccessException;
+
+    /**
+     * Parses the fingerprint and adds any users and groups to the current Authorizer.
+     *
+     * @param fingerprint the fingerprint that was obtained from calling getFingerprint() on another Authorizer.
+     * @throws AuthorizationAccessException if there was an unexpected error performing the operation
+     */
+    void inheritFingerprint(final String fingerprint) throws AuthorizationAccessException;
+
+    /**
+     * When the fingerprints are not equal, this method will check if the proposed fingerprint is inheritable.
+     * If the fingerprint is an exact match, this method will not be invoked as there is nothing to inherit.
+     *
+     * @throws AuthorizationAccessException if there was an unexpected error performing the operation
+     * @throws UninheritableAuthorizationsException if the proposed fingerprint was uninheritable
+     */
+    void checkInheritability(final String proposedFingerprint) throws AuthorizationAccessException, UninheritableAuthorizationsException;
+
+    /**
+     * Adds the given user.
+     *
+     * @param user the user to add
+     * @return the user that was added
+     * @throws AuthorizationAccessException if there was an unexpected error performing the operation
+     * @throws IllegalStateException if there is already a user with the same identity
+     */
+    User addUser(User user) throws AuthorizationAccessException;
+
+    /**
+     * Determines whether the specified user is configurable. Provides the opportunity for a ConfigurableUserGroupProvider to prevent
+     * editing of a specific user. By default, all known users are configurable.
+     *
+     * @param user the user
+     * @return is configurable
+     */
+    default boolean isConfigurable(User user) {
+        if (user == null) {
+            throw new IllegalArgumentException("User cannot be null");
+        }
+
+        return getUser(user.getIdentifier()) != null;
+    }
+
+    /**
+     * The user represented by the provided instance will be updated based on the provided instance.
+     *
+     * @param user an updated user instance
+     * @return the updated user instance, or null if no matching user was found
+     * @throws AuthorizationAccessException if there was an unexpected error performing the operation
+     * @throws IllegalStateException if there is already a user with the same identity
+     */
+    User updateUser(final User user) throws AuthorizationAccessException;
+
+    /**
+     * Deletes the given user.
+     *
+     * @param user the user to delete
+     * @return the user that was deleted, or null if no matching user was found
+     * @throws AuthorizationAccessException if there was an unexpected error performing the operation
+     */
+    User deleteUser(User user) throws AuthorizationAccessException;
+
+    /**
+     * Deletes the user for the given ID.
+     *
+     * @param userIdentifier the user to delete
+     * @return the user that was deleted, or null if no matching user was found
+     * @throws AuthorizationAccessException if there was an unexpected error performing the operation
+     */
+    User deleteUser(String userIdentifier) throws AuthorizationAccessException;
+
+    /**
+     * Adds a new group.
+     *
+     * @param group the Group to add
+     * @return the added Group
+     * @throws AuthorizationAccessException if there was an unexpected error performing the operation
+     * @throws IllegalStateException if a group with the same name already exists
+     */
+    Group addGroup(Group group) throws AuthorizationAccessException;
+
+    /**
+     * Determines whether the specified group is configurable. Provides the opportunity for a ConfigurableUserGroupProvider to prevent
+     * editing of a specific group. By default, all known groups are configurable.
+     *
+     * @param group the group
+     * @return is configurable
+     */
+    default boolean isConfigurable(Group group) {
+        if (group == null) {
+            throw new IllegalArgumentException("Group cannot be null");
+        }
+
+        return getGroup(group.getIdentifier()) != null;
+    }
+
+    /**
+     * The group represented by the provided instance will be updated based on the provided instance.
+     *
+     * @param group an updated group instance
+     * @return the updated group instance, or null if no matching group was found
+     * @throws AuthorizationAccessException if there was an unexpected error performing the operation
+     * @throws IllegalStateException if there is already a group with the same name
+     */
+    Group updateGroup(Group group) throws AuthorizationAccessException;
+
+    /**
+     * Deletes the given group.
+     *
+     * @param group the group to delete
+     * @return the deleted group, or null if no matching group was found
+     * @throws AuthorizationAccessException if there was an unexpected error performing the operation
+     */
+    Group deleteGroup(Group group) throws AuthorizationAccessException;
+
+    /**
+     * Deletes the given group.
+     *
+     * @param groupIdentifier the group to delete
+     * @return the deleted group, or null if no matching group was found
+     * @throws AuthorizationAccessException if there was an unexpected error performing the operation
+     */
+    Group deleteGroup(String groupIdentifier) throws AuthorizationAccessException;
+}

http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/6f26290d/nifi-registry-core/nifi-registry-security-api/src/main/java/org/apache/nifi/registry/security/authorization/Group.java
----------------------------------------------------------------------
diff --git a/nifi-registry-core/nifi-registry-security-api/src/main/java/org/apache/nifi/registry/security/authorization/Group.java b/nifi-registry-core/nifi-registry-security-api/src/main/java/org/apache/nifi/registry/security/authorization/Group.java
new file mode 100644
index 0000000..0ec7764
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-security-api/src/main/java/org/apache/nifi/registry/security/authorization/Group.java
@@ -0,0 +1,263 @@
+/*
+ * 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.security.authorization;
+
+import java.nio.charset.StandardCharsets;
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.Objects;
+import java.util.Set;
+import java.util.UUID;
+
+/**
+ * A group that users can belong to.
+ */
+public class Group {
+
+    private final String identifier;
+
+    private final String name;
+
+    private final Set<String> users;
+
+    private Group(final Builder builder) {
+        this.identifier = builder.identifier;
+        this.name = builder.name;
+        this.users = Collections.unmodifiableSet(new HashSet<>(builder.users));
+
+        if (this.identifier == null || this.identifier.trim().isEmpty()) {
+            throw new IllegalArgumentException("Identifier can not be null or empty");
+        }
+
+        if (this.name == null || this.name.trim().isEmpty()) {
+            throw new IllegalArgumentException("Name can not be null or empty");
+        }
+    }
+
+    /**
+     * @return the identifier of the group
+     */
+    public String getIdentifier() {
+        return identifier;
+    }
+
+    /**
+     * @return the name of the group
+     */
+    public String getName() {
+        return name;
+    }
+
+    /**
+     * @return an unmodifiable set of user identifiers that belong to this group
+     */
+    public Set<String> getUsers() {
+        return users;
+    }
+
+    @Override
+    public boolean equals(Object obj) {
+        if (obj == null) {
+            return false;
+        }
+        if (getClass() != obj.getClass()) {
+            return false;
+        }
+
+        final Group other = (Group) obj;
+        return Objects.equals(this.identifier, other.identifier);
+    }
+
+    @Override
+    public int hashCode() {
+        return Objects.hashCode(this.identifier);
+    }
+
+    @Override
+    public String toString() {
+        return String.format("identifier[%s], name[%s]", getIdentifier(), getName());
+    }
+
+
+    /**
+     * Builder for creating Groups.
+     */
+    public static class Builder {
+
+        private String identifier;
+        private String name;
+        private Set<String> users = new HashSet<>();
+        private final boolean fromGroup;
+
+        public Builder() {
+            this.fromGroup = false;
+        }
+
+        /**
+         * Initializes the builder with the state of the provided group. When using this constructor
+         * the identifier field of the builder can not be changed and will result in an IllegalStateException
+         * if attempting to do so.
+         *
+         * @param other the existing access policy to initialize from
+         */
+        public Builder(final Group other) {
+            if (other == null) {
+                throw new IllegalArgumentException("Provided group can not be null");
+            }
+
+            this.identifier = other.getIdentifier();
+            this.name = other.getName();
+            this.users.clear();
+            this.users.addAll(other.getUsers());
+            this.fromGroup = true;
+        }
+
+        /**
+         * Sets the identifier of the builder.
+         *
+         * @param identifier the identifier
+         * @return the builder
+         * @throws IllegalStateException if this method is called when this builder was constructed from an existing Group
+         */
+        public Builder identifier(final String identifier) {
+            if (fromGroup) {
+                throw new IllegalStateException(
+                        "Identifier can not be changed when initialized from an existing group");
+            }
+
+            this.identifier = identifier;
+            return this;
+        }
+
+        /**
+         * Sets the identifier of the builder to a random UUID.
+         *
+         * @return the builder
+         * @throws IllegalStateException if this method is called when this builder was constructed from an existing Group
+         */
+        public Builder identifierGenerateRandom() {
+            if (fromGroup) {
+                throw new IllegalStateException(
+                        "Identifier can not be changed when initialized from an existing group");
+            }
+
+            this.identifier = UUID.randomUUID().toString();
+            return this;
+        }
+
+        /**
+         * Sets the identifier of the builder with a UUID generated from the specified seed string.
+         *
+         * @return the builder
+         * @throws IllegalStateException if this method is called when this builder was constructed from an existing Group
+         */
+        public Builder identifierGenerateFromSeed(final String seed) {
+            if (fromGroup) {
+                throw new IllegalStateException(
+                        "Identifier can not be changed when initialized from an existing group");
+            }
+            if (seed == null) {
+                throw new IllegalArgumentException("Cannot seed the group identifier with a null value.");
+            }
+
+            this.identifier = UUID.nameUUIDFromBytes(seed.getBytes(StandardCharsets.UTF_8)).toString();
+            return this;
+        }
+
+        /**
+         * Sets the name of the builder.
+         *
+         * @param name the name
+         * @return the builder
+         */
+        public Builder name(final String name) {
+            this.name = name;
+            return this;
+        }
+
+        /**
+         * Adds all users from the provided set to the builder's set of users.
+         *
+         * @param users a set of users to add
+         * @return the builder
+         */
+        public Builder addUsers(final Set<String> users) {
+            if (users != null) {
+                this.users.addAll(users);
+            }
+            return this;
+        }
+
+        /**
+         * Adds the given user to the builder's set of users.
+         *
+         * @param user the user to add
+         * @return the builder
+         */
+        public Builder addUser(final String user) {
+            if (user != null) {
+                this.users.add(user);
+            }
+            return this;
+        }
+
+        /**
+         * Removes the given user from the builder's set of users.
+         *
+         * @param user the user to remove
+         * @return the builder
+         */
+        public Builder removeUser(final String user) {
+            if (user != null) {
+                this.users.remove(user);
+            }
+            return this;
+        }
+
+        /**
+         * Removes all users from the provided set from the builder's set of users.
+         *
+         * @param users the users to remove
+         * @return the builder
+         */
+        public Builder removeUsers(final Set<String> users) {
+            if (users != null) {
+                this.users.removeAll(users);
+            }
+            return this;
+        }
+
+        /**
+         * Clears the builder's set of users so that users is non-null with size 0.
+         *
+         * @return the builder
+         */
+        public Builder clearUsers() {
+            this.users.clear();
+            return this;
+        }
+
+        /**
+         * @return a new Group constructed from the state of the builder
+         */
+        public Group build() {
+            return new Group(this);
+        }
+
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/6f26290d/nifi-registry-core/nifi-registry-security-api/src/main/java/org/apache/nifi/registry/security/authorization/ManagedAuthorizer.java
----------------------------------------------------------------------
diff --git a/nifi-registry-core/nifi-registry-security-api/src/main/java/org/apache/nifi/registry/security/authorization/ManagedAuthorizer.java b/nifi-registry-core/nifi-registry-security-api/src/main/java/org/apache/nifi/registry/security/authorization/ManagedAuthorizer.java
new file mode 100644
index 0000000..50b8094
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-security-api/src/main/java/org/apache/nifi/registry/security/authorization/ManagedAuthorizer.java
@@ -0,0 +1,59 @@
+/*
+ * 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.security.authorization;
+
+import org.apache.nifi.registry.security.authorization.exception.AuthorizationAccessException;
+import org.apache.nifi.registry.security.authorization.exception.UninheritableAuthorizationsException;
+
+public interface ManagedAuthorizer extends Authorizer {
+
+    /**
+     * Returns a fingerprint representing the authorizations managed by this authorizer. The fingerprint will be
+     * used for comparison to determine if two managed authorizers represent a compatible set of users,
+     * groups, and/or policies. Must be non null
+     *
+     * @return the fingerprint for this Authorizer
+     * @throws AuthorizationAccessException if there was an unexpected error performing the operation
+     */
+    String getFingerprint() throws AuthorizationAccessException;
+
+    /**
+     * Parses the fingerprint and adds any users, groups, and policies to the current Authorizer.
+     *
+     * @param fingerprint the fingerprint that was obtained from calling getFingerprint() on another Authorizer.
+     * @throws AuthorizationAccessException if there was an unexpected error performing the operation
+     */
+    void inheritFingerprint(final String fingerprint) throws AuthorizationAccessException;
+
+    /**
+     * When the fingerprints are not equal, this method will check if the proposed fingerprint is inheritable.
+     * If the fingerprint is an exact match, this method will not be invoked as there is nothing to inherit.
+     *
+     * @param proposedFingerprint the proposed fingerprint
+     * @throws AuthorizationAccessException if there was an unexpected error performing the operation
+     * @throws UninheritableAuthorizationsException if the proposed fingerprint was uninheritable
+     */
+    void checkInheritability(final String proposedFingerprint) throws AuthorizationAccessException, UninheritableAuthorizationsException;
+
+    /**
+     * Returns the AccessPolicy provider for this managed Authorizer. Must be non null
+     *
+     * @return the AccessPolicy provider
+     */
+    AccessPolicyProvider getAccessPolicyProvider();
+
+}

http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/6f26290d/nifi-registry-core/nifi-registry-security-api/src/main/java/org/apache/nifi/registry/security/authorization/RequestAction.java
----------------------------------------------------------------------
diff --git a/nifi-registry-core/nifi-registry-security-api/src/main/java/org/apache/nifi/registry/security/authorization/RequestAction.java b/nifi-registry-core/nifi-registry-security-api/src/main/java/org/apache/nifi/registry/security/authorization/RequestAction.java
new file mode 100644
index 0000000..def3de4
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-security-api/src/main/java/org/apache/nifi/registry/security/authorization/RequestAction.java
@@ -0,0 +1,56 @@
+/*
+ * 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.security.authorization;
+
+import java.util.StringJoiner;
+
+/**
+ * Actions a user/entity can take on a resource.
+ */
+public enum RequestAction {
+    READ("read"),
+    WRITE("write"),
+    DELETE("delete");
+
+    private String value;
+
+    RequestAction(String value) {
+        this.value = value;
+    }
+
+    @Override
+    public String toString() {
+        return value.toLowerCase();
+    }
+
+    public static RequestAction valueOfValue(final String action) {
+        if (RequestAction.READ.toString().equalsIgnoreCase(action)) {
+            return RequestAction.READ;
+        } else if (RequestAction.WRITE.toString().equalsIgnoreCase(action)) {
+            return RequestAction.WRITE;
+        } else if (RequestAction.DELETE.toString().equalsIgnoreCase(action)) {
+            return RequestAction.DELETE;
+        } else {
+            StringJoiner stringJoiner = new StringJoiner(", ");
+            for(RequestAction ra : RequestAction.values()) {
+                stringJoiner.add(ra.toString());
+            }
+            String allowableValues = stringJoiner.toString();
+            throw new IllegalArgumentException("Action '" + action + "' is invalid. Must be one of [" + allowableValues + "]");
+        }
+    }
+}

http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/6f26290d/nifi-registry-core/nifi-registry-security-api/src/main/java/org/apache/nifi/registry/security/authorization/Resource.java
----------------------------------------------------------------------
diff --git a/nifi-registry-core/nifi-registry-security-api/src/main/java/org/apache/nifi/registry/security/authorization/Resource.java b/nifi-registry-core/nifi-registry-security-api/src/main/java/org/apache/nifi/registry/security/authorization/Resource.java
new file mode 100644
index 0000000..eacdffe
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-security-api/src/main/java/org/apache/nifi/registry/security/authorization/Resource.java
@@ -0,0 +1,44 @@
+/*
+ * 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.security.authorization;
+
+/**
+ * Resource in an authorization request.
+ */
+public interface Resource {
+
+    /**
+     * The identifier for this resource.
+     *
+     * @return identifier for this resource
+     */
+    String getIdentifier();
+
+    /**
+     * The name of this resource. May be null.
+     *
+     * @return name of this resource
+     */
+    String getName();
+
+    /**
+     * The description of this resource that may be safely used in messages to the client.
+     *
+     * @return safe description
+     */
+    String getSafeDescription();
+}

http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/6f26290d/nifi-registry-core/nifi-registry-security-api/src/main/java/org/apache/nifi/registry/security/authorization/User.java
----------------------------------------------------------------------
diff --git a/nifi-registry-core/nifi-registry-security-api/src/main/java/org/apache/nifi/registry/security/authorization/User.java b/nifi-registry-core/nifi-registry-security-api/src/main/java/org/apache/nifi/registry/security/authorization/User.java
new file mode 100644
index 0000000..8879afe
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-security-api/src/main/java/org/apache/nifi/registry/security/authorization/User.java
@@ -0,0 +1,188 @@
+/*
+ * 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.security.authorization;
+
+import java.nio.charset.StandardCharsets;
+import java.util.Objects;
+import java.util.UUID;
+
+/**
+ * A user to create authorization policies for.
+ */
+public class User {
+
+    private final String identifier;
+
+    private final String identity;
+
+    private User(final Builder builder) {
+        this.identifier = builder.identifier;
+        this.identity = builder.identity;
+
+        if (identifier == null || identifier.trim().isEmpty()) {
+            throw new IllegalArgumentException("Identifier can not be null or empty");
+        }
+
+        if (identity == null || identity.trim().isEmpty()) {
+            throw new IllegalArgumentException("Identity can not be null or empty");
+        }
+
+    }
+
+    /**
+     * @return the identifier of the user
+     */
+    public String getIdentifier() {
+        return identifier;
+    }
+
+    /**
+     * @return the identity string of the user
+     */
+    public String getIdentity() {
+        return identity;
+    }
+
+    @Override
+    public boolean equals(Object obj) {
+        if (obj == null) {
+            return false;
+        }
+        if (getClass() != obj.getClass()) {
+            return false;
+        }
+
+        final User other = (User) obj;
+        return Objects.equals(this.identifier, other.identifier);
+    }
+
+    @Override
+    public int hashCode() {
+        return Objects.hashCode(this.identifier);
+    }
+
+    @Override
+    public String toString() {
+        return String.format("identifier[%s], identity[%s]", getIdentifier(), getIdentity());
+    }
+
+    /**
+     * Builder for Users.
+     */
+    public static class Builder {
+
+        private String identifier;
+        private String identity;
+        private final boolean fromUser;
+
+        /**
+         * Default constructor for building a new User.
+         */
+        public Builder() {
+            this.fromUser = false;
+        }
+
+        /**
+         * Initializes the builder with the state of the provided user. When using this constructor
+         * the identifier field of the builder can not be changed and will result in an IllegalStateException
+         * if attempting to do so.
+         *
+         * @param other the existing user to initialize from
+         */
+        public Builder(final User other) {
+            if (other == null) {
+                throw new IllegalArgumentException("Provided user can not be null");
+            }
+
+            this.identifier = other.getIdentifier();
+            this.identity = other.getIdentity();
+            this.fromUser = true;
+        }
+
+        /**
+         * Sets the identifier of the builder.
+         *
+         * @param identifier the identifier to set
+         * @return the builder
+         * @throws IllegalStateException if this method is called when this builder was constructed from an existing User
+         */
+        public Builder identifier(final String identifier) {
+            if (fromUser) {
+                throw new IllegalStateException(
+                        "Identifier can not be changed when initialized from an existing user");
+            }
+
+            this.identifier = identifier;
+            return this;
+        }
+
+        /**
+         * Sets the identifier of the builder to a random UUID.
+         *
+         * @return the builder
+         * @throws IllegalStateException if this method is called when this builder was constructed from an existing User
+         */
+        public Builder identifierGenerateRandom() {
+            if (fromUser) {
+                throw new IllegalStateException(
+                        "Identifier can not be changed when initialized from an existing user");
+            }
+
+            this.identifier = UUID.randomUUID().toString();
+            return this;
+        }
+
+        /**
+         * Sets the identifier of the builder with a UUID generated from the specified seed string.
+         *
+         * @return the builder
+         * @throws IllegalStateException if this method is called when this builder was constructed from an existing User
+         */
+        public Builder identifierGenerateFromSeed(final String seed) {
+            if (fromUser) {
+                throw new IllegalStateException(
+                        "Identifier can not be changed when initialized from an existing user");
+            }
+            if (seed == null) {
+                throw new IllegalArgumentException("Cannot seed the user identifier with a null value.");
+            }
+
+            this.identifier = UUID.nameUUIDFromBytes(seed.getBytes(StandardCharsets.UTF_8)).toString();
+            return this;
+        }
+
+        /**
+         * Sets the identity of the builder.
+         *
+         * @param identity the identity to set
+         * @return the builder
+         */
+        public Builder identity(final String identity) {
+            this.identity = identity;
+            return this;
+        }
+
+        /**
+         * @return a new User constructed from the state of the builder
+         */
+        public User build() {
+            return new User(this);
+        }
+
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/6f26290d/nifi-registry-core/nifi-registry-security-api/src/main/java/org/apache/nifi/registry/security/authorization/UserAndGroups.java
----------------------------------------------------------------------
diff --git a/nifi-registry-core/nifi-registry-security-api/src/main/java/org/apache/nifi/registry/security/authorization/UserAndGroups.java b/nifi-registry-core/nifi-registry-security-api/src/main/java/org/apache/nifi/registry/security/authorization/UserAndGroups.java
new file mode 100644
index 0000000..6776592
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-security-api/src/main/java/org/apache/nifi/registry/security/authorization/UserAndGroups.java
@@ -0,0 +1,55 @@
+/*
+ * 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.security.authorization;
+
+import java.util.Set;
+
+/**
+ * A holder object to provide atomic access to a user and their groups.
+ */
+public interface UserAndGroups {
+
+    /**
+     * A static, immutable, empty implementation.
+     */
+    UserAndGroups EMPTY = new UserAndGroups() {
+        @Override
+        public User getUser() {
+            return null;
+        }
+
+        @Override
+        public Set<Group> getGroups() {
+            return null;
+        }
+    };
+
+    /**
+     * Retrieves the user, or null if the user is unknown
+     *
+     * @return the user with the given identity
+     */
+    User getUser();
+
+    /**
+     * Retrieves the groups for the user, or null if the user is unknown or has no groups.
+     *
+     * @return the set of groups for the given user identity
+     */
+    Set<Group> getGroups();
+
+}

http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/6f26290d/nifi-registry-core/nifi-registry-security-api/src/main/java/org/apache/nifi/registry/security/authorization/UserContextKeys.java
----------------------------------------------------------------------
diff --git a/nifi-registry-core/nifi-registry-security-api/src/main/java/org/apache/nifi/registry/security/authorization/UserContextKeys.java b/nifi-registry-core/nifi-registry-security-api/src/main/java/org/apache/nifi/registry/security/authorization/UserContextKeys.java
new file mode 100644
index 0000000..8db6cfc
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-security-api/src/main/java/org/apache/nifi/registry/security/authorization/UserContextKeys.java
@@ -0,0 +1,26 @@
+/*
+ * 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.security.authorization;
+
+/**
+ * Constants for keys that can be passed in the AuthorizationRequest user context Map.
+ */
+public enum UserContextKeys {
+
+    CLIENT_ADDRESS;
+
+}

http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/6f26290d/nifi-registry-core/nifi-registry-security-api/src/main/java/org/apache/nifi/registry/security/authorization/UserGroupProvider.java
----------------------------------------------------------------------
diff --git a/nifi-registry-core/nifi-registry-security-api/src/main/java/org/apache/nifi/registry/security/authorization/UserGroupProvider.java b/nifi-registry-core/nifi-registry-security-api/src/main/java/org/apache/nifi/registry/security/authorization/UserGroupProvider.java
new file mode 100644
index 0000000..5505e7d
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-security-api/src/main/java/org/apache/nifi/registry/security/authorization/UserGroupProvider.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.security.authorization;
+
+import org.apache.nifi.registry.security.authorization.exception.AuthorizationAccessException;
+import org.apache.nifi.registry.security.exception.SecurityProviderCreationException;
+import org.apache.nifi.registry.security.exception.SecurityProviderDestructionException;
+
+import java.util.Set;
+
+/**
+ * Provides access to Users and Groups.
+ *
+ * NOTE: Extensions will be called often and frequently. Because of this, if the underlying implementation needs to
+ * make remote calls or expensive calculations those should probably be done asynchronously and/or cache the results.
+ *
+ * Additionally, extensions need to be thread safe.
+ */
+public interface UserGroupProvider {
+
+    /**
+     * Retrieves all users. Must be non null
+     *
+     * @return a list of users
+     * @throws AuthorizationAccessException if there was an unexpected error performing the operation
+     */
+    Set<User> getUsers() throws AuthorizationAccessException;
+
+    /**
+     * Retrieves the user with the given identifier.
+     *
+     * @param identifier the id of the user to retrieve
+     * @return the user with the given id, or null if no matching user was found
+     * @throws AuthorizationAccessException if there was an unexpected error performing the operation
+     */
+    User getUser(String identifier) throws AuthorizationAccessException;
+
+    /**
+     * Retrieves the user with the given identity.
+     *
+     * @param identity the identity of the user to retrieve
+     * @return the user with the given identity, or null if no matching user was found
+     * @throws AuthorizationAccessException if there was an unexpected error performing the operation
+     */
+    User getUserByIdentity(String identity) throws AuthorizationAccessException;
+
+    /**
+     * Retrieves all groups. Must be non null
+     *
+     * @return a list of groups
+     * @throws AuthorizationAccessException if there was an unexpected error performing the operation
+     */
+    Set<Group> getGroups() throws AuthorizationAccessException;
+
+    /**
+     * Retrieves a Group by id.
+     *
+     * @param identifier the identifier of the Group to retrieve
+     * @return the Group with the given identifier, or null if no matching group was found
+     * @throws AuthorizationAccessException if there was an unexpected error performing the operation
+     */
+    Group getGroup(String identifier) throws AuthorizationAccessException;
+
+    /**
+     * Gets a user and their groups. Must be non null. If the user is not known the UserAndGroups.getUser() and
+     * UserAndGroups.getGroups() should return null
+     *
+     * @return the UserAndGroups for the specified identity
+     * @throws AuthorizationAccessException if there was an unexpected error performing the operation
+     */
+    UserAndGroups getUserAndGroups(String identity) throws AuthorizationAccessException;
+
+    /**
+     * Called immediately after instance creation for implementers to perform additional setup
+     *
+     * @param initializationContext in which to initialize
+     */
+    void initialize(UserGroupProviderInitializationContext initializationContext) throws SecurityProviderCreationException;
+
+    /**
+     * Called to configure the Authorizer.
+     *
+     * @param configurationContext at the time of configuration
+     * @throws SecurityProviderCreationException for any issues configuring the provider
+     */
+    void onConfigured(AuthorizerConfigurationContext configurationContext) throws SecurityProviderCreationException;
+
+    /**
+     * Called immediately before instance destruction for implementers to release resources.
+     *
+     * @throws SecurityProviderDestructionException If pre-destruction fails.
+     */
+    void preDestruction() throws SecurityProviderDestructionException;
+}

http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/6f26290d/nifi-registry-core/nifi-registry-security-api/src/main/java/org/apache/nifi/registry/security/authorization/UserGroupProviderInitializationContext.java
----------------------------------------------------------------------
diff --git a/nifi-registry-core/nifi-registry-security-api/src/main/java/org/apache/nifi/registry/security/authorization/UserGroupProviderInitializationContext.java b/nifi-registry-core/nifi-registry-security-api/src/main/java/org/apache/nifi/registry/security/authorization/UserGroupProviderInitializationContext.java
new file mode 100644
index 0000000..d2c471e
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-security-api/src/main/java/org/apache/nifi/registry/security/authorization/UserGroupProviderInitializationContext.java
@@ -0,0 +1,37 @@
+/*
+ * 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.security.authorization;
+
+/**
+ * Initialization content for UserGroupProviders.
+ */
+public interface UserGroupProviderInitializationContext {
+
+    /**
+     * The identifier of the UserGroupProvider.
+     *
+     * @return  The identifier
+     */
+    String getIdentifier();
+
+    /**
+     * The lookup for accessing other configured UserGroupProviders.
+     *
+     * @return  The UserGroupProvider lookup
+     */
+    UserGroupProviderLookup getUserGroupProviderLookup();
+}

http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/6f26290d/nifi-registry-core/nifi-registry-security-api/src/main/java/org/apache/nifi/registry/security/authorization/UserGroupProviderLookup.java
----------------------------------------------------------------------
diff --git a/nifi-registry-core/nifi-registry-security-api/src/main/java/org/apache/nifi/registry/security/authorization/UserGroupProviderLookup.java b/nifi-registry-core/nifi-registry-security-api/src/main/java/org/apache/nifi/registry/security/authorization/UserGroupProviderLookup.java
new file mode 100644
index 0000000..df5e01c
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-security-api/src/main/java/org/apache/nifi/registry/security/authorization/UserGroupProviderLookup.java
@@ -0,0 +1,31 @@
+/*
+ * 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.security.authorization;
+
+/**
+ *
+ */
+public interface UserGroupProviderLookup {
+
+    /**
+     * Looks up the UserGroupProvider with the specified identifier
+     *
+     * @param identifier        The identifier of the UserGroupProvider
+     * @return                  The UserGroupProvider
+     */
+    UserGroupProvider getUserGroupProvider(String identifier);
+}

http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/6f26290d/nifi-registry-core/nifi-registry-security-api/src/main/java/org/apache/nifi/registry/security/authorization/annotation/AuthorizerContext.java
----------------------------------------------------------------------
diff --git a/nifi-registry-core/nifi-registry-security-api/src/main/java/org/apache/nifi/registry/security/authorization/annotation/AuthorizerContext.java b/nifi-registry-core/nifi-registry-security-api/src/main/java/org/apache/nifi/registry/security/authorization/annotation/AuthorizerContext.java
new file mode 100644
index 0000000..8d5136e
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-security-api/src/main/java/org/apache/nifi/registry/security/authorization/annotation/AuthorizerContext.java
@@ -0,0 +1,35 @@
+/*
+ * 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.security.authorization.annotation;
+
+import java.lang.annotation.Documented;
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Inherited;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+/**
+ *
+ *
+ */
+@Documented
+@Target({ElementType.FIELD, ElementType.METHOD})
+@Retention(RetentionPolicy.RUNTIME)
+@Inherited
+public @interface AuthorizerContext {
+}

http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/6f26290d/nifi-registry-core/nifi-registry-security-api/src/main/java/org/apache/nifi/registry/security/authorization/exception/AccessDeniedException.java
----------------------------------------------------------------------
diff --git a/nifi-registry-core/nifi-registry-security-api/src/main/java/org/apache/nifi/registry/security/authorization/exception/AccessDeniedException.java b/nifi-registry-core/nifi-registry-security-api/src/main/java/org/apache/nifi/registry/security/authorization/exception/AccessDeniedException.java
new file mode 100644
index 0000000..6ab629c
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-security-api/src/main/java/org/apache/nifi/registry/security/authorization/exception/AccessDeniedException.java
@@ -0,0 +1,39 @@
+/*
+ * 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.security.authorization.exception;
+
+/**
+ * Represents any error that might occur while authorizing user requests.
+ */
+public class AccessDeniedException extends RuntimeException {
+    private static final long serialVersionUID = -5683444815269084134L;
+
+    public AccessDeniedException(Throwable cause) {
+        super(cause);
+    }
+
+    public AccessDeniedException(String message, Throwable cause) {
+        super(message, cause);
+    }
+
+    public AccessDeniedException(String message) {
+        super(message);
+    }
+
+    public AccessDeniedException() {
+    }
+}

http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/6f26290d/nifi-registry-core/nifi-registry-security-api/src/main/java/org/apache/nifi/registry/security/authorization/exception/AuthorizationAccessException.java
----------------------------------------------------------------------
diff --git a/nifi-registry-core/nifi-registry-security-api/src/main/java/org/apache/nifi/registry/security/authorization/exception/AuthorizationAccessException.java b/nifi-registry-core/nifi-registry-security-api/src/main/java/org/apache/nifi/registry/security/authorization/exception/AuthorizationAccessException.java
new file mode 100644
index 0000000..7f33430
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-security-api/src/main/java/org/apache/nifi/registry/security/authorization/exception/AuthorizationAccessException.java
@@ -0,0 +1,32 @@
+/*
+ * 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.security.authorization.exception;
+
+/**
+ * Represents the case when an authorization decision could not be made because the Authorizer was unable to access the underlying data store.
+ */
+public class AuthorizationAccessException extends RuntimeException {
+
+    public AuthorizationAccessException(String message, Throwable cause) {
+        super(message, cause);
+    }
+
+    public AuthorizationAccessException(String message) {
+        super(message);
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/6f26290d/nifi-registry-core/nifi-registry-security-api/src/main/java/org/apache/nifi/registry/security/authorization/exception/UninheritableAuthorizationsException.java
----------------------------------------------------------------------
diff --git a/nifi-registry-core/nifi-registry-security-api/src/main/java/org/apache/nifi/registry/security/authorization/exception/UninheritableAuthorizationsException.java b/nifi-registry-core/nifi-registry-security-api/src/main/java/org/apache/nifi/registry/security/authorization/exception/UninheritableAuthorizationsException.java
new file mode 100644
index 0000000..b3ef068
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-security-api/src/main/java/org/apache/nifi/registry/security/authorization/exception/UninheritableAuthorizationsException.java
@@ -0,0 +1,28 @@
+/*
+ * 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.security.authorization.exception;
+
+/**
+ * Represents the case when the proposed authorizations are not inheritable.
+ */
+public class UninheritableAuthorizationsException extends RuntimeException {
+
+    public UninheritableAuthorizationsException(String message) {
+        super(message);
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/6f26290d/nifi-registry-core/nifi-registry-security-api/src/main/java/org/apache/nifi/registry/security/exception/SecurityProviderCreationException.java
----------------------------------------------------------------------
diff --git a/nifi-registry-core/nifi-registry-security-api/src/main/java/org/apache/nifi/registry/security/exception/SecurityProviderCreationException.java b/nifi-registry-core/nifi-registry-security-api/src/main/java/org/apache/nifi/registry/security/exception/SecurityProviderCreationException.java
new file mode 100644
index 0000000..01531d6
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-security-api/src/main/java/org/apache/nifi/registry/security/exception/SecurityProviderCreationException.java
@@ -0,0 +1,38 @@
+/*
+ * 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.security.exception;
+
+/**
+ * Represents the exceptional case when a security api provider fails instantiation.
+ */
+public class SecurityProviderCreationException extends RuntimeException {
+
+    public SecurityProviderCreationException() {
+    }
+
+    public SecurityProviderCreationException(String msg) {
+        super(msg);
+    }
+
+    public SecurityProviderCreationException(Throwable cause) {
+        super(cause);
+    }
+
+    public SecurityProviderCreationException(String msg, Throwable cause) {
+        super(msg, cause);
+    }
+}

http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/6f26290d/nifi-registry-core/nifi-registry-security-api/src/main/java/org/apache/nifi/registry/security/exception/SecurityProviderDestructionException.java
----------------------------------------------------------------------
diff --git a/nifi-registry-core/nifi-registry-security-api/src/main/java/org/apache/nifi/registry/security/exception/SecurityProviderDestructionException.java b/nifi-registry-core/nifi-registry-security-api/src/main/java/org/apache/nifi/registry/security/exception/SecurityProviderDestructionException.java
new file mode 100644
index 0000000..3370623
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-security-api/src/main/java/org/apache/nifi/registry/security/exception/SecurityProviderDestructionException.java
@@ -0,0 +1,38 @@
+/*
+ * 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.security.exception;
+
+/**
+ * Represents the exceptional case when a security api provider fails destruction.
+ */
+public class SecurityProviderDestructionException extends RuntimeException {
+
+    public SecurityProviderDestructionException() {
+    }
+
+    public SecurityProviderDestructionException(String msg) {
+        super(msg);
+    }
+
+    public SecurityProviderDestructionException(Throwable cause) {
+        super(cause);
+    }
+
+    public SecurityProviderDestructionException(String msg, Throwable cause) {
+        super(msg, cause);
+    }
+}

http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/6f26290d/nifi-registry-core/nifi-registry-security-utils/pom.xml
----------------------------------------------------------------------
diff --git a/nifi-registry-core/nifi-registry-security-utils/pom.xml b/nifi-registry-core/nifi-registry-security-utils/pom.xml
new file mode 100644
index 0000000..bcc704a
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-security-utils/pom.xml
@@ -0,0 +1,42 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 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. -->
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+    <modelVersion>4.0.0</modelVersion>
+    <parent>
+        <groupId>org.apache.nifi.registry</groupId>
+        <artifactId>nifi-registry-core</artifactId>
+        <version>0.3.0-SNAPSHOT</version>
+    </parent>
+    
+    <artifactId>nifi-registry-security-utils</artifactId>
+    <packaging>jar</packaging>
+
+    <!-- NOTE: Since this module is used by nifi-registry-client we should avoid any unnecessary dependencies -->
+
+    <dependencies>
+        <dependency>
+            <groupId>org.bouncycastle</groupId>
+            <artifactId>bcprov-jdk15on</artifactId>
+            <version>1.55</version>
+        </dependency>
+        <dependency>
+            <groupId>org.bouncycastle</groupId>
+            <artifactId>bcpkix-jdk15on</artifactId>
+            <version>1.55</version>
+        </dependency>
+        <dependency>
+            <groupId>org.apache.commons</groupId>
+            <artifactId>commons-lang3</artifactId>
+        </dependency>
+    </dependencies>
+
+</project>


[47/51] [partial] nifi-registry git commit: NIFIREG-201 Refactoring project structure to better isolate extensions

Posted by kd...@apache.org.
http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/6f26290d/nifi-registry-core/nifi-registry-client/src/main/java/org/apache/nifi/registry/client/FlowClient.java
----------------------------------------------------------------------
diff --git a/nifi-registry-core/nifi-registry-client/src/main/java/org/apache/nifi/registry/client/FlowClient.java b/nifi-registry-core/nifi-registry-client/src/main/java/org/apache/nifi/registry/client/FlowClient.java
new file mode 100644
index 0000000..6dd72e9
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-client/src/main/java/org/apache/nifi/registry/client/FlowClient.java
@@ -0,0 +1,119 @@
+/*
+ * 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.client;
+
+import org.apache.nifi.registry.diff.VersionedFlowDifference;
+import org.apache.nifi.registry.field.Fields;
+import org.apache.nifi.registry.flow.VersionedFlow;
+
+import java.io.IOException;
+import java.util.List;
+
+/**
+ * Client for interacting with flows.
+ */
+public interface FlowClient {
+
+    /**
+     * Create the given flow in the given bucket.
+     *
+     * @param flow the flow to create
+     * @return the created flow with the identifier populated
+     * @throws NiFiRegistryException if an error is encountered other than IOException
+     * @throws IOException if an I/O error is encountered
+     */
+    VersionedFlow create(VersionedFlow flow) throws NiFiRegistryException, IOException;
+
+    /**
+     * Gets the flow with the given id in the given bucket.
+     *
+     * The list of snapshot metadata will NOT be populated.
+     *
+     * @param bucketId a bucket id
+     * @param flowId a flow id
+     * @return the flow with the given id in the given bucket
+     * @throws NiFiRegistryException if an error is encountered other than IOException
+     * @throws IOException if an I/O error is encountered
+     */
+    VersionedFlow get(String bucketId, String flowId) throws NiFiRegistryException, IOException;
+
+    /**
+     * Gets the flow with the given id.
+     *
+     * @param flowId a flow id
+     * @return the flow with the given id
+     * @throws NiFiRegistryException if an error is encountered other than IOException
+     * @throws IOException if an I/O error is encountered
+     */
+    VersionedFlow get(String flowId) throws NiFiRegistryException, IOException;
+
+    /**
+     * Updates the given flow with in the given bucket.
+     *
+     * The identifier of the flow must be populated in the flow object, and only the name and description can be updated.
+     *
+     * @param bucketId a bucket id
+     * @param flow the flow with updates
+     * @return the updated flow
+     * @throws NiFiRegistryException if an error is encountered other than IOException
+     * @throws IOException if an I/O error is encountered
+     */
+    VersionedFlow update(String bucketId, VersionedFlow flow) throws NiFiRegistryException, IOException;
+
+    /**
+     *  Deletes the flow with the given id in the given bucket.
+     *
+     * @param bucketId a bucket id
+     * @param flowId the id of the flow to delete
+     * @return the deleted flow
+     * @throws NiFiRegistryException if an error is encountered other than IOException
+     * @throws IOException if an I/O error is encountered
+     */
+    VersionedFlow delete(String bucketId, String flowId) throws NiFiRegistryException, IOException;
+
+    /**
+     * Gets the field info for flows.
+     *
+     * @return field info for flows
+     * @throws NiFiRegistryException if an error is encountered other than IOException
+     * @throws IOException if an I/O error is encountered
+     */
+    Fields getFields() throws NiFiRegistryException, IOException;
+
+    /**
+     * Gets the flows for a given bucket.
+     *
+     * @param bucketId a bucket id
+     * @return the flows in the given bucket
+     * @throws NiFiRegistryException if an error is encountered other than IOException
+     * @throws IOException if an I/O error is encountered
+     */
+    List<VersionedFlow> getByBucket(String bucketId) throws NiFiRegistryException, IOException;
+
+    /**
+     *
+     * @param bucketId a bucket id
+     * @param flowId the flow that is under inspection
+     * @param versionA the first version to use in the comparison
+     * @param versionB the second flow to use in the comparison
+     * @return the list of differences between the 2 flow versions grouped by component
+     * @throws NiFiRegistryException if an error is encountered other than IOException
+     * @throws IOException if an I/O error is encountered
+     */
+    VersionedFlowDifference diff(final String bucketId, final String flowId,
+                                 final Integer versionA, final Integer versionB) throws NiFiRegistryException, IOException;
+}

http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/6f26290d/nifi-registry-core/nifi-registry-client/src/main/java/org/apache/nifi/registry/client/FlowSnapshotClient.java
----------------------------------------------------------------------
diff --git a/nifi-registry-core/nifi-registry-client/src/main/java/org/apache/nifi/registry/client/FlowSnapshotClient.java b/nifi-registry-core/nifi-registry-client/src/main/java/org/apache/nifi/registry/client/FlowSnapshotClient.java
new file mode 100644
index 0000000..edf7beb
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-client/src/main/java/org/apache/nifi/registry/client/FlowSnapshotClient.java
@@ -0,0 +1,133 @@
+/*
+ * 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.client;
+
+import org.apache.nifi.registry.flow.VersionedFlowSnapshot;
+import org.apache.nifi.registry.flow.VersionedFlowSnapshotMetadata;
+
+import java.io.IOException;
+import java.util.List;
+
+/**
+ * Client for interacting with snapshots.
+ */
+public interface FlowSnapshotClient {
+
+    /**
+     * Creates a new snapshot/version for the given flow.
+     *
+     * The snapshot object must have the version populated, and will receive an error if the submitted version is
+     * not the next one-up version.
+     *
+     * @param snapshot the new snapshot
+     * @return the created snapshot
+     * @throws NiFiRegistryException if an error is encountered other than IOException
+     * @throws IOException if an I/O error is encountered
+     */
+    VersionedFlowSnapshot create(VersionedFlowSnapshot snapshot) throws NiFiRegistryException, IOException;
+
+    /**
+     * Gets the snapshot for the given bucket, flow, and version.
+     *
+     * @param bucketId the bucket id
+     * @param flowId the flow id
+     * @param version the version
+     * @return the snapshot with the given version of the given flow in the given bucket
+     * @throws NiFiRegistryException if an error is encountered other than IOException
+     * @throws IOException if an I/O error is encountered
+     */
+    VersionedFlowSnapshot get(String bucketId, String flowId, int version) throws NiFiRegistryException, IOException;
+
+    /**
+     * Gets the snapshot for the given flow and version.
+     *
+     * @param flowId the flow id
+     * @param version the version
+     * @return the snapshot with the given version of the given flow
+     * @throws NiFiRegistryException if an error is encountered other than IOException
+     * @throws IOException if an I/O error is encountered
+     */
+    VersionedFlowSnapshot get(String flowId, int version) throws NiFiRegistryException, IOException;
+
+    /**
+     * Gets the latest snapshot for the given flow.
+     *
+     * @param bucketId the bucket id
+     * @param flowId the flow id
+     * @return the snapshot with the latest version for the given flow
+     * @throws NiFiRegistryException if an error is encountered other than IOException
+     * @throws IOException if an I/O error is encountered
+     */
+    VersionedFlowSnapshot getLatest(String bucketId, String flowId) throws NiFiRegistryException, IOException;
+
+    /**
+     * Gets the latest snapshot for the given flow.
+     *
+     * @param flowId the flow id
+     * @return the snapshot with the latest version for the given flow
+     * @throws NiFiRegistryException if an error is encountered other than IOException
+     * @throws IOException if an I/O error is encountered
+     */
+    VersionedFlowSnapshot getLatest(String flowId) throws NiFiRegistryException, IOException;
+
+    /**
+     * Gets the latest snapshot metadata for the given flow.
+     *
+     * @param bucketId the bucket id
+     * @param flowId the flow id
+     * @return the snapshot metadata for the latest version of the given flow
+     * @throws NiFiRegistryException if an error is encountered other than IOException
+     * @throws IOException if an I/O error is encountered
+     */
+    VersionedFlowSnapshotMetadata getLatestMetadata(String bucketId, String flowId) throws NiFiRegistryException, IOException;
+
+    /**
+     * Gets the latest snapshot metadata for the given flow.
+     *
+     * @param flowId the flow id
+     * @return the snapshot metadata for the latest version of the given flow
+     * @throws NiFiRegistryException if an error is encountered other than IOException
+     * @throws IOException if an I/O error is encountered
+     */
+    VersionedFlowSnapshotMetadata getLatestMetadata(String flowId) throws NiFiRegistryException, IOException;
+
+    /**
+     * Gets a list of the metadata for all snapshots of a given flow.
+     *
+     * The contents of each snapshot are not part of the response.
+     *
+     * @param bucketId the bucket id
+     * @param flowId the flow id
+     * @return the list of snapshot metadata
+     * @throws NiFiRegistryException if an error is encountered other than IOException
+     * @throws IOException if an I/O error is encountered
+     */
+    List<VersionedFlowSnapshotMetadata> getSnapshotMetadata(String bucketId, String flowId) throws NiFiRegistryException, IOException;
+
+    /**
+     * Gets a list of the metadata for all snapshots of a given flow.
+     *
+     * The contents of each snapshot are not part of the response.
+     *
+     * @param flowId the flow id
+     * @return the list of snapshot metadata
+     * @throws NiFiRegistryException if an error is encountered other than IOException
+     * @throws IOException if an I/O error is encountered
+     */
+    List<VersionedFlowSnapshotMetadata> getSnapshotMetadata(String flowId) throws NiFiRegistryException, IOException;
+
+}

http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/6f26290d/nifi-registry-core/nifi-registry-client/src/main/java/org/apache/nifi/registry/client/ItemsClient.java
----------------------------------------------------------------------
diff --git a/nifi-registry-core/nifi-registry-client/src/main/java/org/apache/nifi/registry/client/ItemsClient.java b/nifi-registry-core/nifi-registry-client/src/main/java/org/apache/nifi/registry/client/ItemsClient.java
new file mode 100644
index 0000000..96fa801
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-client/src/main/java/org/apache/nifi/registry/client/ItemsClient.java
@@ -0,0 +1,64 @@
+/*
+ * 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.client;
+
+import org.apache.nifi.registry.bucket.BucketItem;
+import org.apache.nifi.registry.field.Fields;
+
+import java.io.IOException;
+import java.util.List;
+
+/**
+ * Client for interacting with bucket items.
+ *
+ * Bucket items contain the common fields across anything stored in the registry.
+ *
+ * Each item contains a type field and a link to the URI of the specific item.
+ *
+ * i.e. The link field of a flow item would contain the URI to the specific flow.
+ */
+public interface ItemsClient {
+
+    /**
+     * Gets all bucket items in the registry.
+     *
+     * @return the list of all bucket items
+     * @throws NiFiRegistryException if an error is encountered other than IOException
+     * @throws IOException if an I/O error is encountered
+     */
+    List<BucketItem> getAll() throws NiFiRegistryException, IOException;
+
+    /**
+     * Gets all bucket items for the given bucket.
+     *
+     * @param bucketId the bucket id
+     * @return the list of items in the given bucket
+     * @throws NiFiRegistryException if an error is encountered other than IOException
+     * @throws IOException if an I/O error is encountered
+     */
+    List<BucketItem> getByBucket(String bucketId) throws NiFiRegistryException, IOException;
+
+    /**
+     * Gets the field info for bucket items.
+     *
+     * @return the list of field info
+     * @throws NiFiRegistryException if an error is encountered other than IOException
+     * @throws IOException if an I/O error is encountered
+     */
+    Fields getFields() throws NiFiRegistryException, IOException;
+
+}

http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/6f26290d/nifi-registry-core/nifi-registry-client/src/main/java/org/apache/nifi/registry/client/NiFiRegistryClient.java
----------------------------------------------------------------------
diff --git a/nifi-registry-core/nifi-registry-client/src/main/java/org/apache/nifi/registry/client/NiFiRegistryClient.java b/nifi-registry-core/nifi-registry-client/src/main/java/org/apache/nifi/registry/client/NiFiRegistryClient.java
new file mode 100644
index 0000000..07fb817
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-client/src/main/java/org/apache/nifi/registry/client/NiFiRegistryClient.java
@@ -0,0 +1,89 @@
+/*
+ * 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.client;
+
+import java.io.Closeable;
+
+/**
+ * A client for interacting with the REST API of a NiFi registry instance.
+ */
+public interface NiFiRegistryClient extends Closeable {
+
+    /**
+     * @return the client for interacting with buckets
+     */
+    BucketClient getBucketClient();
+
+    /**
+     * @return the client for interacting with buckets on behalf of the given proxied entities
+     */
+    BucketClient getBucketClient(String ... proxiedEntity);
+
+    /**
+     * @return the client for interacting with flows
+     */
+    FlowClient getFlowClient();
+
+    /**
+     * @return the client for interacting with flows on behalf of the given proxied entities
+     */
+    FlowClient getFlowClient(String ... proxiedEntity);
+
+    /**
+     * @return the client for interacting with flows/snapshots
+     */
+    FlowSnapshotClient getFlowSnapshotClient();
+
+    /**
+     * @return the client for interacting with flows/snapshots on behalf of the given proxied entities
+     */
+    FlowSnapshotClient getFlowSnapshotClient(String ... proxiedEntity);
+
+    /**
+     * @return the client for interacting with bucket items
+     */
+    ItemsClient getItemsClient();
+
+    /**
+     * @return the client for interacting with bucket items on behalf of the given proxied entities
+     */
+    ItemsClient getItemsClient(String ... proxiedEntity);
+
+    /**
+     * @return the client for obtaining information about the current user
+     */
+    UserClient getUserClient();
+
+    /**
+     * @return the client for obtaining information about the current user based on the given proxied entities
+     */
+    UserClient getUserClient(String ... proxiedEntity);
+
+    /**
+     * The builder interface that implementations should provide for obtaining the client.
+     */
+    interface Builder {
+
+        Builder config(NiFiRegistryClientConfig clientConfig);
+
+        NiFiRegistryClientConfig getConfig();
+
+        NiFiRegistryClient build();
+
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/6f26290d/nifi-registry-core/nifi-registry-client/src/main/java/org/apache/nifi/registry/client/NiFiRegistryClientConfig.java
----------------------------------------------------------------------
diff --git a/nifi-registry-core/nifi-registry-client/src/main/java/org/apache/nifi/registry/client/NiFiRegistryClientConfig.java b/nifi-registry-core/nifi-registry-client/src/main/java/org/apache/nifi/registry/client/NiFiRegistryClientConfig.java
new file mode 100644
index 0000000..de77b51
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-client/src/main/java/org/apache/nifi/registry/client/NiFiRegistryClientConfig.java
@@ -0,0 +1,257 @@
+/*
+ * 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.client;
+
+import org.apache.nifi.registry.security.util.KeyStoreUtils;
+import org.apache.nifi.registry.security.util.KeystoreType;
+
+import javax.net.ssl.HostnameVerifier;
+import javax.net.ssl.KeyManager;
+import javax.net.ssl.KeyManagerFactory;
+import javax.net.ssl.SSLContext;
+import javax.net.ssl.TrustManager;
+import javax.net.ssl.TrustManagerFactory;
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.InputStream;
+import java.security.KeyStore;
+import java.security.SecureRandom;
+
+/**
+ * Configuration for a NiFiRegistryClient.
+ */
+public class NiFiRegistryClientConfig {
+
+    private final String baseUrl;
+    private final SSLContext sslContext;
+    private final String keystoreFilename;
+    private final String keystorePass;
+    private final String keyPass;
+    private final KeystoreType keystoreType;
+    private final String truststoreFilename;
+    private final String truststorePass;
+    private final KeystoreType truststoreType;
+    private final HostnameVerifier hostnameVerifier;
+    private final Integer readTimeout;
+    private final Integer connectTimeout;
+
+
+    private NiFiRegistryClientConfig(final Builder builder) {
+        this.baseUrl = builder.baseUrl;
+        this.sslContext = builder.sslContext;
+        this.keystoreFilename = builder.keystoreFilename;
+        this.keystorePass = builder.keystorePass;
+        this.keyPass = builder.keyPass;
+        this.keystoreType = builder.keystoreType;
+        this.truststoreFilename = builder.truststoreFilename;
+        this.truststorePass = builder.truststorePass;
+        this.truststoreType = builder.truststoreType;
+        this.hostnameVerifier = builder.hostnameVerifier;
+        this.readTimeout = builder.readTimeout;
+        this.connectTimeout = builder.connectTimeout;
+    }
+
+    public String getBaseUrl() {
+        return baseUrl;
+    }
+
+    public SSLContext getSslContext() {
+        if (sslContext != null) {
+            return sslContext;
+        }
+
+        final KeyManagerFactory keyManagerFactory;
+        if (keystoreFilename != null && keystorePass != null && keystoreType != null) {
+            try {
+                // prepare the keystore
+                final KeyStore keyStore = KeyStoreUtils.getKeyStore(keystoreType.name());
+                try (final InputStream keyStoreStream = new FileInputStream(new File(keystoreFilename))) {
+                    keyStore.load(keyStoreStream, keystorePass.toCharArray());
+                }
+                keyManagerFactory = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm());
+
+                if (keyPass == null) {
+                    keyManagerFactory.init(keyStore, keystorePass.toCharArray());
+                } else {
+                    keyManagerFactory.init(keyStore, keyPass.toCharArray());
+                }
+            } catch (final Exception e) {
+                throw new IllegalStateException("Failed to load Keystore", e);
+            }
+        } else {
+            keyManagerFactory = null;
+        }
+
+        final TrustManagerFactory trustManagerFactory;
+        if (truststoreFilename != null && truststorePass != null && truststoreType != null) {
+            try {
+                // prepare the truststore
+                final KeyStore trustStore = KeyStoreUtils.getTrustStore(truststoreType.name());
+                try (final InputStream trustStoreStream = new FileInputStream(new File(truststoreFilename))) {
+                    trustStore.load(trustStoreStream, truststorePass.toCharArray());
+                }
+                trustManagerFactory = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());
+                trustManagerFactory.init(trustStore);
+            } catch (final Exception e) {
+                throw new IllegalStateException("Failed to load Truststore", e);
+            }
+        } else {
+            trustManagerFactory = null;
+        }
+
+        if (keyManagerFactory != null || trustManagerFactory != null) {
+            try {
+                // initialize the ssl context
+                KeyManager[] keyManagers = keyManagerFactory != null ? keyManagerFactory.getKeyManagers() : null;
+                TrustManager[] trustManagers = trustManagerFactory != null ? trustManagerFactory.getTrustManagers() : null;
+                final SSLContext sslContext = SSLContext.getInstance("TLS");
+                sslContext.init(keyManagers, trustManagers, new SecureRandom());
+                sslContext.getDefaultSSLParameters().setNeedClientAuth(true);
+
+                return sslContext;
+            } catch (final Exception e) {
+                throw new IllegalStateException("Created keystore and truststore but failed to initialize SSLContext", e);
+            }
+        } else {
+            return null;
+        }
+    }
+
+    public String getKeystoreFilename() {
+        return keystoreFilename;
+    }
+
+    public String getKeystorePass() {
+        return keystorePass;
+    }
+
+    public String getKeyPass() {
+        return keyPass;
+    }
+
+    public KeystoreType getKeystoreType() {
+        return keystoreType;
+    }
+
+    public String getTruststoreFilename() {
+        return truststoreFilename;
+    }
+
+    public String getTruststorePass() {
+        return truststorePass;
+    }
+
+    public KeystoreType getTruststoreType() {
+        return truststoreType;
+    }
+
+    public HostnameVerifier getHostnameVerifier() {
+        return hostnameVerifier;
+    }
+
+    public Integer getReadTimeout() {
+        return readTimeout;
+    }
+
+    public Integer getConnectTimeout() {
+        return connectTimeout;
+    }
+
+    /**
+     * Builder for client configuration.
+     */
+    public static class Builder {
+
+        private String baseUrl;
+        private SSLContext sslContext;
+        private String keystoreFilename;
+        private String keystorePass;
+        private String keyPass;
+        private KeystoreType keystoreType;
+        private String truststoreFilename;
+        private String truststorePass;
+        private KeystoreType truststoreType;
+        private HostnameVerifier hostnameVerifier;
+        private Integer readTimeout;
+        private Integer connectTimeout;
+
+        public Builder baseUrl(final String baseUrl) {
+            this.baseUrl = baseUrl;
+            return this;
+        }
+
+        public Builder sslContext(final SSLContext sslContext) {
+            this.sslContext = sslContext;
+            return this;
+        }
+
+        public Builder keystoreFilename(final String keystoreFilename) {
+            this.keystoreFilename = keystoreFilename;
+            return this;
+        }
+
+        public Builder keystorePassword(final String keystorePass) {
+            this.keystorePass = keystorePass;
+            return this;
+        }
+
+        public Builder keyPassword(final String keyPass) {
+            this.keyPass = keyPass;
+            return this;
+        }
+
+        public Builder keystoreType(final KeystoreType keystoreType) {
+            this.keystoreType = keystoreType;
+            return this;
+        }
+
+        public Builder truststoreFilename(final String truststoreFilename) {
+            this.truststoreFilename = truststoreFilename;
+            return this;
+        }
+
+        public Builder truststorePassword(final String truststorePass) {
+            this.truststorePass = truststorePass;
+            return this;
+        }
+
+        public Builder truststoreType(final KeystoreType truststoreType) {
+            this.truststoreType = truststoreType;
+            return this;
+        }
+
+        public Builder hostnameVerifier(final HostnameVerifier hostnameVerifier) {
+            this.hostnameVerifier = hostnameVerifier;
+            return this;
+        }
+
+        public Builder readTimeout(final Integer readTimeout) {
+            this.readTimeout = readTimeout;
+            return this;
+        }
+
+        public Builder connectTimeout(final Integer connectTimeout) {
+            this.connectTimeout = connectTimeout;
+            return this;
+        }
+
+        public NiFiRegistryClientConfig build() {
+            return new NiFiRegistryClientConfig(this);
+        }
+
+    }
+}

http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/6f26290d/nifi-registry-core/nifi-registry-client/src/main/java/org/apache/nifi/registry/client/NiFiRegistryException.java
----------------------------------------------------------------------
diff --git a/nifi-registry-core/nifi-registry-client/src/main/java/org/apache/nifi/registry/client/NiFiRegistryException.java b/nifi-registry-core/nifi-registry-client/src/main/java/org/apache/nifi/registry/client/NiFiRegistryException.java
new file mode 100644
index 0000000..273a032
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-client/src/main/java/org/apache/nifi/registry/client/NiFiRegistryException.java
@@ -0,0 +1,32 @@
+/*
+ * 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.client;
+
+/**
+ * Indicates an error interacting with the NiFi registry for a reason other than IOException.
+ */
+public class NiFiRegistryException extends Exception {
+
+    public NiFiRegistryException(final String message) {
+        super(message);
+    }
+
+    public NiFiRegistryException(final String message, final Throwable cause) {
+        super(message, cause);
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/6f26290d/nifi-registry-core/nifi-registry-client/src/main/java/org/apache/nifi/registry/client/UserClient.java
----------------------------------------------------------------------
diff --git a/nifi-registry-core/nifi-registry-client/src/main/java/org/apache/nifi/registry/client/UserClient.java b/nifi-registry-core/nifi-registry-client/src/main/java/org/apache/nifi/registry/client/UserClient.java
new file mode 100644
index 0000000..181f7af
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-client/src/main/java/org/apache/nifi/registry/client/UserClient.java
@@ -0,0 +1,42 @@
+/*
+ * 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.client;
+
+import org.apache.nifi.registry.authorization.CurrentUser;
+
+import java.io.IOException;
+
+public interface UserClient {
+
+    /**
+     * Obtains the access status of the current user.
+     *
+     * If the UserClient was obtained with proxied entities, then the access status should represent the status
+     * of the last identity in the chain.
+     *
+     * If the UserClient was obtained without proxied entities, then it would represent the identity of the certificate
+     * in the keystore used by the client.
+     *
+     * If the registry is not in secure mode, the anonymous identity is expected to be returned along with a flag indicating
+     * the user is anonymous.
+     *
+     * @return the access status of the current user
+     * @throws NiFiRegistryException if the proxying user is not a valid proxy or identity claim is otherwise invalid
+     */
+    CurrentUser getAccessStatus() throws NiFiRegistryException, IOException;
+
+}

http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/6f26290d/nifi-registry-core/nifi-registry-client/src/main/java/org/apache/nifi/registry/client/impl/AbstractJerseyClient.java
----------------------------------------------------------------------
diff --git a/nifi-registry-core/nifi-registry-client/src/main/java/org/apache/nifi/registry/client/impl/AbstractJerseyClient.java b/nifi-registry-core/nifi-registry-client/src/main/java/org/apache/nifi/registry/client/impl/AbstractJerseyClient.java
new file mode 100644
index 0000000..479699e
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-client/src/main/java/org/apache/nifi/registry/client/impl/AbstractJerseyClient.java
@@ -0,0 +1,120 @@
+/*
+ * 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.client.impl;
+
+import org.apache.nifi.registry.client.NiFiRegistryException;
+
+import javax.ws.rs.WebApplicationException;
+import javax.ws.rs.client.Invocation;
+import javax.ws.rs.client.WebTarget;
+import javax.ws.rs.core.Response;
+import java.io.IOException;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * Base class for the client operations to share exception handling.
+ *
+ * Sub-classes should always execute a request from getRequestBuilder(target) to ensure proper headers are sent.
+ */
+public class AbstractJerseyClient {
+
+    private final Map<String,String> headers;
+
+    public AbstractJerseyClient(final Map<String, String> headers) {
+        this.headers = headers == null ? Collections.emptyMap() : Collections.unmodifiableMap(new HashMap<>(headers));
+    }
+
+    protected Map<String,String> getHeaders() {
+        return headers;
+    }
+
+    /**
+     * Creates a new Invocation.Builder for the given WebTarget with the headers added to the builder.
+     *
+     * @param webTarget the target for the request
+     * @return the builder for the target with the headers added
+     */
+    protected Invocation.Builder getRequestBuilder(final WebTarget webTarget) {
+        final Invocation.Builder requestBuilder = webTarget.request();
+        headers.entrySet().stream().forEach(e -> requestBuilder.header(e.getKey(), e.getValue()));
+        return requestBuilder;
+    }
+
+    /**
+     * Executes the given action and returns the result.
+     *
+     * @param action the action to execute
+     * @param errorMessage the message to use if a NiFiRegistryException is thrown
+     * @param <T> the return type of the action
+     * @return the result of the action
+     * @throws NiFiRegistryException if any exception other than IOException is encountered
+     * @throws IOException if an I/O error occurs communicating with the registry
+     */
+    protected <T> T executeAction(final String errorMessage, final NiFiRegistryAction<T> action) throws NiFiRegistryException, IOException {
+        try {
+            return action.execute();
+        } catch (final Exception e) {
+            final Throwable ioeCause = getIOExceptionCause(e);
+
+            if (ioeCause == null) {
+                final StringBuilder errorMessageBuilder = new StringBuilder(errorMessage);
+
+                // see if we have a WebApplicationException, and if so add the response body to the error message
+                if (e instanceof WebApplicationException) {
+                    final Response response = ((WebApplicationException) e).getResponse();
+                    final String responseBody = response.readEntity(String.class);
+                    errorMessageBuilder.append(": ").append(responseBody);
+                }
+
+                throw new NiFiRegistryException(errorMessageBuilder.toString(), e);
+            } else {
+                throw (IOException) ioeCause;
+            }
+        }
+    }
+
+
+    /**
+     * An action to execute with the given return type.
+     *
+     * @param <T> the return type of the action
+     */
+    protected interface NiFiRegistryAction<T> {
+
+        T execute();
+
+    }
+
+    /**
+     * @param e an exception that was encountered interacting with the registry
+     * @return the IOException that caused this exception, or null if the an IOException did not cause this exception
+     */
+    protected Throwable getIOExceptionCause(final Throwable e) {
+        if (e == null) {
+            return null;
+        }
+
+        if (e instanceof IOException) {
+            return e;
+        }
+
+        return getIOExceptionCause(e.getCause());
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/6f26290d/nifi-registry-core/nifi-registry-client/src/main/java/org/apache/nifi/registry/client/impl/BucketItemDeserializer.java
----------------------------------------------------------------------
diff --git a/nifi-registry-core/nifi-registry-client/src/main/java/org/apache/nifi/registry/client/impl/BucketItemDeserializer.java b/nifi-registry-core/nifi-registry-client/src/main/java/org/apache/nifi/registry/client/impl/BucketItemDeserializer.java
new file mode 100644
index 0000000..5640d43
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-client/src/main/java/org/apache/nifi/registry/client/impl/BucketItemDeserializer.java
@@ -0,0 +1,76 @@
+/*
+ * 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.client.impl;
+
+import com.fasterxml.jackson.core.JsonParser;
+import com.fasterxml.jackson.core.JsonProcessingException;
+import com.fasterxml.jackson.databind.DeserializationContext;
+import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.deser.std.StdDeserializer;
+import org.apache.commons.lang3.StringUtils;
+import org.apache.nifi.registry.bucket.BucketItem;
+import org.apache.nifi.registry.bucket.BucketItemType;
+import org.apache.nifi.registry.flow.VersionedFlow;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Iterator;
+import java.util.List;
+
+public class BucketItemDeserializer extends StdDeserializer<BucketItem[]> {
+
+    public BucketItemDeserializer() {
+        super(BucketItem[].class);
+    }
+
+    @Override
+    public BucketItem[] deserialize(JsonParser jsonParser, DeserializationContext deserializationContext) throws IOException, JsonProcessingException {
+        final JsonNode arrayNode = jsonParser.getCodec().readTree(jsonParser);
+
+        final List<BucketItem> bucketItems = new ArrayList<>();
+
+        final Iterator<JsonNode> nodeIter = arrayNode.elements();
+        while (nodeIter.hasNext()) {
+            final JsonNode node = nodeIter.next();
+
+            final String type = node.get("type").asText();
+            if (StringUtils.isBlank(type)) {
+                throw new IllegalStateException("BucketItem type cannot be null or blank");
+            }
+
+            final BucketItemType bucketItemType;
+            try {
+                bucketItemType = BucketItemType.valueOf(type);
+            } catch (Exception e) {
+                throw new IllegalStateException("Unknown type for BucketItem: " + type, e);
+            }
+
+
+            switch (bucketItemType) {
+                case Flow:
+                    final VersionedFlow versionedFlow = jsonParser.getCodec().treeToValue(node, VersionedFlow.class);
+                    bucketItems.add(versionedFlow);
+                    break;
+                default:
+                    throw new IllegalStateException("Unknown type for BucketItem");
+            }
+        }
+
+        return bucketItems.toArray(new BucketItem[bucketItems.size()]);
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/6f26290d/nifi-registry-core/nifi-registry-client/src/main/java/org/apache/nifi/registry/client/impl/JerseyBucketClient.java
----------------------------------------------------------------------
diff --git a/nifi-registry-core/nifi-registry-client/src/main/java/org/apache/nifi/registry/client/impl/JerseyBucketClient.java b/nifi-registry-core/nifi-registry-client/src/main/java/org/apache/nifi/registry/client/impl/JerseyBucketClient.java
new file mode 100644
index 0000000..f84f8c6
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-client/src/main/java/org/apache/nifi/registry/client/impl/JerseyBucketClient.java
@@ -0,0 +1,140 @@
+/*
+ * 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.client.impl;
+
+import org.apache.commons.lang3.StringUtils;
+import org.apache.nifi.registry.bucket.Bucket;
+import org.apache.nifi.registry.client.BucketClient;
+import org.apache.nifi.registry.client.NiFiRegistryException;
+import org.apache.nifi.registry.field.Fields;
+
+import javax.ws.rs.client.Entity;
+import javax.ws.rs.client.WebTarget;
+import javax.ws.rs.core.MediaType;
+import java.io.IOException;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * Jersey implementation of BucketClient.
+ */
+public class JerseyBucketClient extends AbstractJerseyClient implements BucketClient {
+
+    private final WebTarget bucketsTarget;
+
+
+    public JerseyBucketClient(final WebTarget baseTarget) {
+        this(baseTarget, Collections.emptyMap());
+    }
+
+    public JerseyBucketClient(final WebTarget baseTarget, final Map<String,String> headers) {
+        super(headers);
+        this.bucketsTarget = baseTarget.path("/buckets");
+    }
+
+    @Override
+    public Bucket create(final Bucket bucket) throws NiFiRegistryException, IOException {
+        if (bucket == null) {
+            throw new IllegalArgumentException("Bucket cannot be null");
+        }
+
+        return executeAction("Error creating bucket", () -> {
+            return getRequestBuilder(bucketsTarget)
+                    .post(
+                            Entity.entity(bucket, MediaType.APPLICATION_JSON),
+                            Bucket.class
+                    );
+        });
+
+    }
+
+    @Override
+    public Bucket get(final String bucketId) throws NiFiRegistryException, IOException {
+        if (StringUtils.isBlank(bucketId)) {
+            throw new IllegalArgumentException("Bucket ID cannot be blank");
+        }
+
+        return executeAction("Error retrieving bucket", () -> {
+            final WebTarget target = bucketsTarget
+                    .path("/{bucketId}")
+                    .resolveTemplate("bucketId", bucketId);
+
+            return getRequestBuilder(target).get(Bucket.class);
+        });
+
+    }
+
+    @Override
+    public Bucket update(final Bucket bucket) throws NiFiRegistryException, IOException {
+        if (bucket == null) {
+            throw new IllegalArgumentException("Bucket cannot be null");
+        }
+
+        if (StringUtils.isBlank(bucket.getIdentifier())) {
+            throw new IllegalArgumentException("Bucket Identifier must be provided");
+        }
+
+        return executeAction("Error updating bucket", () -> {
+            final WebTarget target = bucketsTarget
+                    .path("/{bucketId}")
+                    .resolveTemplate("bucketId", bucket.getIdentifier());
+
+            return getRequestBuilder(target)
+                    .put(
+                            Entity.entity(bucket, MediaType.APPLICATION_JSON),
+                            Bucket.class
+                    );
+
+        });
+    }
+
+    @Override
+    public Bucket delete(final String bucketId) throws NiFiRegistryException, IOException {
+        if (StringUtils.isBlank(bucketId)) {
+            throw new IllegalArgumentException("Bucket ID cannot be blank");
+        }
+
+        return executeAction("Error deleting bucket", () -> {
+            final WebTarget target = bucketsTarget
+                    .path("/{bucketId}")
+                    .resolveTemplate("bucketId", bucketId);
+
+            return getRequestBuilder(target).delete(Bucket.class);
+        });
+    }
+
+    @Override
+    public Fields getFields() throws NiFiRegistryException, IOException {
+        return executeAction("Error retrieving bucket field info", () -> {
+            final WebTarget target = bucketsTarget
+                    .path("/fields");
+
+            return getRequestBuilder(target).get(Fields.class);
+        });
+    }
+
+    @Override
+    public List<Bucket> getAll() throws NiFiRegistryException, IOException {
+        return executeAction("Error retrieving all buckets", () -> {
+            final Bucket[] buckets = getRequestBuilder(bucketsTarget).get(Bucket[].class);
+            return buckets == null ? Collections.emptyList() : Arrays.asList(buckets);
+        });
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/6f26290d/nifi-registry-core/nifi-registry-client/src/main/java/org/apache/nifi/registry/client/impl/JerseyFlowClient.java
----------------------------------------------------------------------
diff --git a/nifi-registry-core/nifi-registry-client/src/main/java/org/apache/nifi/registry/client/impl/JerseyFlowClient.java b/nifi-registry-core/nifi-registry-client/src/main/java/org/apache/nifi/registry/client/impl/JerseyFlowClient.java
new file mode 100644
index 0000000..486a20a
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-client/src/main/java/org/apache/nifi/registry/client/impl/JerseyFlowClient.java
@@ -0,0 +1,205 @@
+/*
+ * 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.client.impl;
+
+import org.apache.commons.lang3.StringUtils;
+import org.apache.nifi.registry.client.FlowClient;
+import org.apache.nifi.registry.client.NiFiRegistryException;
+import org.apache.nifi.registry.diff.VersionedFlowDifference;
+import org.apache.nifi.registry.field.Fields;
+import org.apache.nifi.registry.flow.VersionedFlow;
+
+import javax.ws.rs.client.Entity;
+import javax.ws.rs.client.WebTarget;
+import javax.ws.rs.core.MediaType;
+import java.io.IOException;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * Jersey implementation of FlowClient.
+ */
+public class JerseyFlowClient extends AbstractJerseyClient  implements FlowClient {
+
+    private final WebTarget flowsTarget;
+    private final WebTarget bucketFlowsTarget;
+
+    public JerseyFlowClient(final WebTarget baseTarget) {
+        this(baseTarget, Collections.emptyMap());
+    }
+
+    public JerseyFlowClient(final WebTarget baseTarget, final Map<String,String> headers) {
+        super(headers);
+        this.flowsTarget = baseTarget.path("/flows");
+        this.bucketFlowsTarget = baseTarget.path("/buckets/{bucketId}/flows");
+    }
+
+    @Override
+    public VersionedFlow create(final VersionedFlow flow) throws NiFiRegistryException, IOException {
+        if (flow == null) {
+            throw new IllegalArgumentException("VersionedFlow cannot be null");
+        }
+
+        final String bucketId = flow.getBucketIdentifier();
+        if (StringUtils.isBlank(bucketId)) {
+            throw new IllegalArgumentException("Bucket Identifier cannot be blank");
+        }
+
+        return executeAction("Error creating flow", () -> {
+            final WebTarget target = bucketFlowsTarget
+                    .resolveTemplate("bucketId", bucketId);
+
+            return getRequestBuilder(target)
+                    .post(
+                            Entity.entity(flow, MediaType.APPLICATION_JSON),
+                            VersionedFlow.class
+                    );
+        });
+    }
+
+    @Override
+    public VersionedFlow get(final String bucketId, final String flowId) throws NiFiRegistryException, IOException {
+        if (StringUtils.isBlank(bucketId)) {
+            throw new IllegalArgumentException("Bucket Identifier cannot be blank");
+        }
+
+        if (StringUtils.isBlank(flowId)) {
+            throw new IllegalArgumentException("Flow Identifier cannot be blank");
+        }
+
+        return executeAction("Error retrieving flow", () -> {
+            final WebTarget target = bucketFlowsTarget
+                    .path("/{flowId}")
+                    .resolveTemplate("bucketId", bucketId)
+                    .resolveTemplate("flowId", flowId);
+
+            return  getRequestBuilder(target).get(VersionedFlow.class);
+        });
+    }
+
+    @Override
+    public VersionedFlow get(final String flowId) throws NiFiRegistryException, IOException {
+        if (StringUtils.isBlank(flowId)) {
+            throw new IllegalArgumentException("Flow Identifier cannot be blank");
+        }
+
+        // this uses the flowsTarget because its calling /flows/{flowId} without knowing a bucketId
+        return executeAction("Error retrieving flow", () -> {
+            final WebTarget target = flowsTarget
+                    .path("/{flowId}")
+                    .resolveTemplate("flowId", flowId);
+
+            return  getRequestBuilder(target).get(VersionedFlow.class);
+        });
+    }
+
+    @Override
+    public VersionedFlow update(final String bucketId, final VersionedFlow flow) throws NiFiRegistryException, IOException {
+        if (StringUtils.isBlank(bucketId)) {
+            throw new IllegalArgumentException("Bucket Identifier cannot be blank");
+        }
+
+        if (flow == null) {
+            throw new IllegalArgumentException("VersionedFlow cannot be null");
+        }
+
+        if (StringUtils.isBlank(flow.getIdentifier())) {
+            throw new IllegalArgumentException("VersionedFlow identifier must be provided");
+        }
+
+        return executeAction("Error updating flow", () -> {
+            final WebTarget target = bucketFlowsTarget
+                    .path("/{flowId}")
+                    .resolveTemplate("bucketId", bucketId)
+                    .resolveTemplate("flowId", flow.getIdentifier());
+
+            return  getRequestBuilder(target)
+                    .put(
+                            Entity.entity(flow, MediaType.APPLICATION_JSON),
+                            VersionedFlow.class
+                    );
+        });
+    }
+
+    @Override
+    public VersionedFlow delete(final String bucketId, final String flowId) throws NiFiRegistryException, IOException {
+        if (StringUtils.isBlank(bucketId)) {
+            throw new IllegalArgumentException("Bucket Identifier cannot be blank");
+        }
+
+        if (StringUtils.isBlank(flowId)) {
+            throw new IllegalArgumentException("Flow Identifier cannot be blank");
+        }
+
+        return executeAction("Error deleting flow", () -> {
+            final WebTarget target = bucketFlowsTarget
+                    .path("/{flowId}")
+                    .resolveTemplate("bucketId", bucketId)
+                    .resolveTemplate("flowId", flowId);
+
+            return getRequestBuilder(target).delete(VersionedFlow.class);
+        });
+    }
+
+    @Override
+    public Fields getFields() throws NiFiRegistryException, IOException {
+        return executeAction("Error retrieving fields info for flows", () -> {
+            final WebTarget target = flowsTarget.path("/fields");
+            return getRequestBuilder(target).get(Fields.class);
+        });
+    }
+
+    @Override
+    public List<VersionedFlow> getByBucket(final String bucketId) throws NiFiRegistryException, IOException {
+        if (StringUtils.isBlank(bucketId)) {
+            throw new IllegalArgumentException("Bucket Identifier cannot be blank");
+        }
+
+        return executeAction("Error getting flows for bucket", () -> {
+            WebTarget target = bucketFlowsTarget;
+            target = target.resolveTemplate("bucketId", bucketId);
+
+            final VersionedFlow[] versionedFlows = getRequestBuilder(target).get(VersionedFlow[].class);
+            return  versionedFlows == null ? Collections.emptyList() : Arrays.asList(versionedFlows);
+        });
+    }
+
+    @Override
+    public VersionedFlowDifference diff(final String bucketId, final String flowId,
+                                        final Integer versionA, final Integer versionB) throws NiFiRegistryException, IOException {
+        if (StringUtils.isBlank(bucketId)) {
+            throw new IllegalArgumentException("Bucket Identifier cannot be blank");
+        }
+
+        if (StringUtils.isBlank(flowId)) {
+            throw new IllegalArgumentException("Flow Identifier cannot be blank");
+        }
+
+        return executeAction("Error retrieving flow", () -> {
+            final WebTarget target = bucketFlowsTarget
+                    .path("/{flowId}/diff/{versionA}/{versionB}")
+                    .resolveTemplate("bucketId", bucketId)
+                    .resolveTemplate("flowId", flowId)
+                    .resolveTemplate("versionA", versionA)
+                    .resolveTemplate("versionB", versionB);
+
+            return  getRequestBuilder(target).get(VersionedFlowDifference.class);
+        });
+    }
+}

http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/6f26290d/nifi-registry-core/nifi-registry-client/src/main/java/org/apache/nifi/registry/client/impl/JerseyFlowSnapshotClient.java
----------------------------------------------------------------------
diff --git a/nifi-registry-core/nifi-registry-client/src/main/java/org/apache/nifi/registry/client/impl/JerseyFlowSnapshotClient.java b/nifi-registry-core/nifi-registry-client/src/main/java/org/apache/nifi/registry/client/impl/JerseyFlowSnapshotClient.java
new file mode 100644
index 0000000..befe389
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-client/src/main/java/org/apache/nifi/registry/client/impl/JerseyFlowSnapshotClient.java
@@ -0,0 +1,246 @@
+/*
+ * 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.client.impl;
+
+import org.apache.commons.lang3.StringUtils;
+import org.apache.nifi.registry.client.FlowSnapshotClient;
+import org.apache.nifi.registry.client.NiFiRegistryException;
+import org.apache.nifi.registry.flow.VersionedFlowSnapshot;
+import org.apache.nifi.registry.flow.VersionedFlowSnapshotMetadata;
+
+import javax.ws.rs.client.Entity;
+import javax.ws.rs.client.WebTarget;
+import javax.ws.rs.core.MediaType;
+import java.io.IOException;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * Jersey implementation of FlowSnapshotClient.
+ */
+public class JerseyFlowSnapshotClient extends AbstractJerseyClient implements FlowSnapshotClient {
+
+    final WebTarget bucketFlowSnapshotTarget;
+    final WebTarget flowsFlowSnapshotTarget;
+
+    public JerseyFlowSnapshotClient(final WebTarget baseTarget) {
+        this(baseTarget, Collections.emptyMap());
+    }
+
+    public JerseyFlowSnapshotClient(final WebTarget baseTarget, final Map<String,String> headers) {
+        super(headers);
+        this.bucketFlowSnapshotTarget = baseTarget.path("/buckets/{bucketId}/flows/{flowId}/versions");
+        this.flowsFlowSnapshotTarget = baseTarget.path("/flows/{flowId}/versions");
+    }
+
+    @Override
+    public VersionedFlowSnapshot create(final VersionedFlowSnapshot snapshot)
+            throws NiFiRegistryException, IOException {
+        if (snapshot.getSnapshotMetadata() == null) {
+            throw new IllegalArgumentException("Snapshot Metadata cannot be null");
+        }
+
+        final String bucketId = snapshot.getSnapshotMetadata().getBucketIdentifier();
+        if (StringUtils.isBlank(bucketId)) {
+            throw new IllegalArgumentException("Bucket Identifier cannot be blank");
+        }
+
+        final String flowId = snapshot.getSnapshotMetadata().getFlowIdentifier();
+        if (StringUtils.isBlank(flowId)) {
+            throw new IllegalArgumentException("Flow Identifier cannot be blank");
+        }
+
+        return executeAction("Error creating snapshot", () -> {
+            final WebTarget target = bucketFlowSnapshotTarget
+                    .resolveTemplate("bucketId", bucketId)
+                    .resolveTemplate("flowId", flowId);
+
+            return  getRequestBuilder(target)
+                    .post(
+                            Entity.entity(snapshot, MediaType.APPLICATION_JSON),
+                            VersionedFlowSnapshot.class
+                    );
+        });
+    }
+
+    @Override
+    public VersionedFlowSnapshot get(final String bucketId, final String flowId, final int version)
+            throws NiFiRegistryException, IOException {
+        if (StringUtils.isBlank(bucketId)) {
+            throw new IllegalArgumentException("Bucket Identifier cannot be blank");
+        }
+
+        if (StringUtils.isBlank(flowId)) {
+            throw new IllegalArgumentException("Flow Identifier cannot be blank");
+        }
+
+        if (version < 1) {
+            throw new IllegalArgumentException("Version must be greater than 1");
+        }
+
+        return executeAction("Error retrieving flow snapshot", () -> {
+            final WebTarget target = bucketFlowSnapshotTarget
+                    .path("/{version}")
+                    .resolveTemplate("bucketId", bucketId)
+                    .resolveTemplate("flowId", flowId)
+                    .resolveTemplate("version", version);
+
+            return getRequestBuilder(target).get(VersionedFlowSnapshot.class);
+        });
+    }
+
+    @Override
+    public VersionedFlowSnapshot get(final String flowId, final int version)
+            throws NiFiRegistryException, IOException {
+
+        if (StringUtils.isBlank(flowId)) {
+            throw new IllegalArgumentException("Flow Identifier cannot be blank");
+        }
+
+        if (version < 1) {
+            throw new IllegalArgumentException("Version must be greater than 1");
+        }
+
+        return executeAction("Error retrieving flow snapshot", () -> {
+            final WebTarget target = flowsFlowSnapshotTarget
+                    .path("/{version}")
+                    .resolveTemplate("flowId", flowId)
+                    .resolveTemplate("version", version);
+
+            return getRequestBuilder(target).get(VersionedFlowSnapshot.class);
+        });
+    }
+
+    @Override
+    public VersionedFlowSnapshot getLatest(final String bucketId, final String flowId)
+            throws NiFiRegistryException, IOException {
+        if (StringUtils.isBlank(bucketId)) {
+            throw new IllegalArgumentException("Bucket Identifier cannot be blank");
+        }
+
+        if (StringUtils.isBlank(flowId)) {
+            throw new IllegalArgumentException("Flow Identifier cannot be blank");
+        }
+
+        return executeAction("Error retrieving latest snapshot", () -> {
+            final WebTarget target = bucketFlowSnapshotTarget
+                    .path("/latest")
+                    .resolveTemplate("bucketId", bucketId)
+                    .resolveTemplate("flowId", flowId);
+
+            return getRequestBuilder(target).get(VersionedFlowSnapshot.class);
+        });
+    }
+
+    @Override
+    public VersionedFlowSnapshot getLatest(final String flowId)
+            throws NiFiRegistryException, IOException {
+        if (StringUtils.isBlank(flowId)) {
+            throw new IllegalArgumentException("Flow Identifier cannot be blank");
+        }
+
+        return executeAction("Error retrieving latest snapshot", () -> {
+            final WebTarget target = flowsFlowSnapshotTarget
+                    .path("/latest")
+                    .resolveTemplate("flowId", flowId);
+
+            return getRequestBuilder(target).get(VersionedFlowSnapshot.class);
+        });
+    }
+
+    @Override
+    public VersionedFlowSnapshotMetadata getLatestMetadata(final String bucketId, final String flowId) throws NiFiRegistryException, IOException {
+        if (StringUtils.isBlank(bucketId)) {
+            throw new IllegalArgumentException("Bucket Identifier cannot be blank");
+        }
+
+        if (StringUtils.isBlank(flowId)) {
+            throw new IllegalArgumentException("Flow Identifier cannot be blank");
+        }
+
+        return executeAction("Error retrieving latest snapshot metadata", () -> {
+            final WebTarget target = bucketFlowSnapshotTarget
+                    .path("/latest/metadata")
+                    .resolveTemplate("bucketId", bucketId)
+                    .resolveTemplate("flowId", flowId);
+
+            return getRequestBuilder(target).get(VersionedFlowSnapshotMetadata.class);
+        });
+    }
+
+    @Override
+    public VersionedFlowSnapshotMetadata getLatestMetadata(final String flowId) throws NiFiRegistryException, IOException {
+        if (StringUtils.isBlank(flowId)) {
+            throw new IllegalArgumentException("Flow Identifier cannot be blank");
+        }
+
+        return executeAction("Error retrieving latest snapshot metadata", () -> {
+            final WebTarget target = flowsFlowSnapshotTarget
+                    .path("/latest/metadata")
+                    .resolveTemplate("flowId", flowId);
+
+            return getRequestBuilder(target).get(VersionedFlowSnapshotMetadata.class);
+        });
+    }
+
+    @Override
+    @SuppressWarnings("unchecked")
+    public List<VersionedFlowSnapshotMetadata> getSnapshotMetadata(final String bucketId, final String flowId)
+            throws NiFiRegistryException, IOException {
+        if (StringUtils.isBlank(bucketId)) {
+            throw new IllegalArgumentException("Bucket Identifier cannot be blank");
+        }
+
+        if (StringUtils.isBlank(flowId)) {
+            throw new IllegalArgumentException("Flow Identifier cannot be blank");
+        }
+
+        return executeAction("Error retrieving snapshot metadata", () -> {
+            final WebTarget target = bucketFlowSnapshotTarget
+                    .resolveTemplate("bucketId", bucketId)
+                    .resolveTemplate("flowId", flowId);
+
+            final VersionedFlowSnapshotMetadata[] snapshots = getRequestBuilder(target)
+                    .get(VersionedFlowSnapshotMetadata[].class);
+
+            return snapshots == null ? Collections.emptyList() : Arrays.asList(snapshots);
+        });
+    }
+
+    @Override
+    @SuppressWarnings("unchecked")
+    public List<VersionedFlowSnapshotMetadata> getSnapshotMetadata(final String flowId)
+            throws NiFiRegistryException, IOException {
+
+        if (StringUtils.isBlank(flowId)) {
+            throw new IllegalArgumentException("Flow Identifier cannot be blank");
+        }
+
+        return executeAction("Error retrieving snapshot metadata", () -> {
+            final WebTarget target = flowsFlowSnapshotTarget
+                    .resolveTemplate("flowId", flowId);
+
+            final VersionedFlowSnapshotMetadata[] snapshots = getRequestBuilder(target)
+                    .get(VersionedFlowSnapshotMetadata[].class);
+
+            return snapshots == null ? Collections.emptyList() : Arrays.asList(snapshots);
+        });
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/6f26290d/nifi-registry-core/nifi-registry-client/src/main/java/org/apache/nifi/registry/client/impl/JerseyItemsClient.java
----------------------------------------------------------------------
diff --git a/nifi-registry-core/nifi-registry-client/src/main/java/org/apache/nifi/registry/client/impl/JerseyItemsClient.java b/nifi-registry-core/nifi-registry-client/src/main/java/org/apache/nifi/registry/client/impl/JerseyItemsClient.java
new file mode 100644
index 0000000..6b01fc4
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-client/src/main/java/org/apache/nifi/registry/client/impl/JerseyItemsClient.java
@@ -0,0 +1,85 @@
+/*
+ * 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.client.impl;
+
+import org.apache.commons.lang3.StringUtils;
+import org.apache.nifi.registry.bucket.BucketItem;
+import org.apache.nifi.registry.client.ItemsClient;
+import org.apache.nifi.registry.client.NiFiRegistryException;
+import org.apache.nifi.registry.field.Fields;
+
+import javax.ws.rs.client.WebTarget;
+import java.io.IOException;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * Jersey implementation of ItemsClient.
+ */
+public class JerseyItemsClient extends AbstractJerseyClient implements ItemsClient {
+
+    private final WebTarget itemsTarget;
+
+    public JerseyItemsClient(final WebTarget baseTarget) {
+        this(baseTarget, Collections.emptyMap());
+    }
+
+    public JerseyItemsClient(final WebTarget baseTarget, final Map<String,String> headers) {
+        super(headers);
+        this.itemsTarget = baseTarget.path("/items");
+    }
+
+
+
+    @Override
+    public List<BucketItem> getAll() throws NiFiRegistryException, IOException {
+        return executeAction("", () -> {
+            WebTarget target = itemsTarget;
+            final BucketItem[] bucketItems = getRequestBuilder(target).get(BucketItem[].class);
+            return bucketItems == null ? Collections.emptyList() : Arrays.asList(bucketItems);
+        });
+    }
+
+    @Override
+    public List<BucketItem> getByBucket(final String bucketId)
+            throws NiFiRegistryException, IOException {
+        if (StringUtils.isBlank(bucketId)) {
+            throw new IllegalArgumentException("Bucket Identifier cannot be blank");
+        }
+
+        return executeAction("", () -> {
+            WebTarget target = itemsTarget
+                    .path("/{bucketId}")
+                    .resolveTemplate("bucketId", bucketId);
+
+            final BucketItem[] bucketItems = getRequestBuilder(target).get(BucketItem[].class);
+            return bucketItems == null ? Collections.emptyList() : Arrays.asList(bucketItems);
+        });
+    }
+
+    @Override
+    public Fields getFields() throws NiFiRegistryException, IOException {
+        return executeAction("", () -> {
+            final WebTarget target = itemsTarget.path("/fields");
+            return getRequestBuilder(target).get(Fields.class);
+
+        });
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/6f26290d/nifi-registry-core/nifi-registry-client/src/main/java/org/apache/nifi/registry/client/impl/JerseyNiFiRegistryClient.java
----------------------------------------------------------------------
diff --git a/nifi-registry-core/nifi-registry-client/src/main/java/org/apache/nifi/registry/client/impl/JerseyNiFiRegistryClient.java b/nifi-registry-core/nifi-registry-client/src/main/java/org/apache/nifi/registry/client/impl/JerseyNiFiRegistryClient.java
new file mode 100644
index 0000000..329a47a
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-client/src/main/java/org/apache/nifi/registry/client/impl/JerseyNiFiRegistryClient.java
@@ -0,0 +1,247 @@
+/*
+ * 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.client.impl;
+
+import com.fasterxml.jackson.annotation.JsonInclude;
+import com.fasterxml.jackson.databind.DeserializationFeature;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.databind.module.SimpleModule;
+import com.fasterxml.jackson.module.jaxb.JaxbAnnotationIntrospector;
+import org.apache.commons.lang3.StringUtils;
+import org.apache.nifi.registry.bucket.BucketItem;
+import org.apache.nifi.registry.client.BucketClient;
+import org.apache.nifi.registry.client.FlowClient;
+import org.apache.nifi.registry.client.FlowSnapshotClient;
+import org.apache.nifi.registry.client.ItemsClient;
+import org.apache.nifi.registry.client.NiFiRegistryClient;
+import org.apache.nifi.registry.client.NiFiRegistryClientConfig;
+import org.apache.nifi.registry.client.UserClient;
+import org.apache.nifi.registry.security.util.ProxiedEntitiesUtils;
+import org.glassfish.jersey.client.ClientConfig;
+import org.glassfish.jersey.client.ClientProperties;
+import org.glassfish.jersey.jackson.internal.jackson.jaxrs.json.JacksonJaxbJsonProvider;
+
+import javax.net.ssl.HostnameVerifier;
+import javax.net.ssl.SSLContext;
+import javax.ws.rs.client.Client;
+import javax.ws.rs.client.ClientBuilder;
+import javax.ws.rs.client.WebTarget;
+import java.io.IOException;
+import java.net.URI;
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.stream.Collectors;
+
+/**
+ * A NiFiRegistryClient that uses Jersey Client.
+ */
+public class JerseyNiFiRegistryClient implements NiFiRegistryClient {
+
+    static final String NIFI_REGISTRY_CONTEXT = "nifi-registry-api";
+    static final int DEFAULT_CONNECT_TIMEOUT = 10000;
+    static final int DEFAULT_READ_TIMEOUT = 10000;
+
+    private final Client client;
+    private final WebTarget baseTarget;
+
+    private final BucketClient bucketClient;
+    private final FlowClient flowClient;
+    private final FlowSnapshotClient flowSnapshotClient;
+    private final ItemsClient itemsClient;
+
+    private JerseyNiFiRegistryClient(final NiFiRegistryClient.Builder builder) {
+        final NiFiRegistryClientConfig registryClientConfig = builder.getConfig();
+        if (registryClientConfig == null) {
+            throw new IllegalArgumentException("NiFiRegistryClientConfig cannot be null");
+        }
+
+        String baseUrl = registryClientConfig.getBaseUrl();
+        if (StringUtils.isBlank(baseUrl)) {
+            throw new IllegalArgumentException("Base URL cannot be blank");
+        }
+
+        if (baseUrl.endsWith("/")) {
+            baseUrl = baseUrl.substring(0, baseUrl.length() - 1);
+        }
+
+        if (!baseUrl.endsWith(NIFI_REGISTRY_CONTEXT)) {
+            baseUrl = baseUrl + "/" + NIFI_REGISTRY_CONTEXT;
+        }
+
+        try {
+            new URI(baseUrl);
+        } catch (final Exception e) {
+            throw new IllegalArgumentException("Invalid base URL: " + e.getMessage(), e);
+        }
+
+        final SSLContext sslContext = registryClientConfig.getSslContext();
+        final HostnameVerifier hostnameVerifier = registryClientConfig.getHostnameVerifier();
+
+        final ClientBuilder clientBuilder = ClientBuilder.newBuilder();
+        if (sslContext != null) {
+            clientBuilder.sslContext(sslContext);
+        }
+        if (hostnameVerifier != null) {
+            clientBuilder.hostnameVerifier(hostnameVerifier);
+        }
+
+        final int connectTimeout = registryClientConfig.getConnectTimeout() == null ? DEFAULT_CONNECT_TIMEOUT : registryClientConfig.getConnectTimeout();
+        final int readTimeout = registryClientConfig.getReadTimeout() == null ? DEFAULT_READ_TIMEOUT : registryClientConfig.getReadTimeout();
+
+        final ClientConfig clientConfig = new ClientConfig();
+        clientConfig.property(ClientProperties.CONNECT_TIMEOUT, connectTimeout);
+        clientConfig.property(ClientProperties.READ_TIMEOUT, readTimeout);
+        clientConfig.register(jacksonJaxbJsonProvider());
+        clientBuilder.withConfig(clientConfig);
+        this.client = clientBuilder.build();
+
+        this.baseTarget = client.target(baseUrl);
+        this.bucketClient = new JerseyBucketClient(baseTarget);
+        this.flowClient = new JerseyFlowClient(baseTarget);
+        this.flowSnapshotClient = new JerseyFlowSnapshotClient(baseTarget);
+        this.itemsClient = new JerseyItemsClient(baseTarget);
+    }
+
+    @Override
+    public BucketClient getBucketClient() {
+        return this.bucketClient;
+    }
+
+    @Override
+    public FlowClient getFlowClient() {
+        return this.flowClient;
+    }
+
+    @Override
+    public FlowSnapshotClient getFlowSnapshotClient() {
+        return this.flowSnapshotClient;
+    }
+
+    @Override
+    public ItemsClient getItemsClient() {
+        return this.itemsClient;
+    }
+
+    @Override
+    public BucketClient getBucketClient(String... proxiedEntity) {
+        final Map<String,String> headers = getHeaders(proxiedEntity);
+        return new JerseyBucketClient(baseTarget, headers);
+    }
+
+    @Override
+    public FlowClient getFlowClient(String... proxiedEntity) {
+        final Map<String,String> headers = getHeaders(proxiedEntity);
+        return new JerseyFlowClient(baseTarget, headers);
+    }
+
+    @Override
+    public FlowSnapshotClient getFlowSnapshotClient(String... proxiedEntity) {
+        final Map<String,String> headers = getHeaders(proxiedEntity);
+        return new JerseyFlowSnapshotClient(baseTarget, headers);
+    }
+
+    @Override
+    public ItemsClient getItemsClient(String... proxiedEntity) {
+        final Map<String,String> headers = getHeaders(proxiedEntity);
+        return new JerseyItemsClient(baseTarget, headers);
+    }
+
+    @Override
+    public UserClient getUserClient() {
+        return new JerseyUserClient(baseTarget);
+    }
+
+    @Override
+    public UserClient getUserClient(String... proxiedEntity) {
+        final Map<String,String> headers = getHeaders(proxiedEntity);
+        return new JerseyUserClient(baseTarget, headers);
+    }
+
+    private Map<String,String> getHeaders(String[] proxiedEntities) {
+        final String proxiedEntitiesValue = getProxiedEntitesValue(proxiedEntities);
+
+        final Map<String,String> headers = new HashMap<>();
+        if (proxiedEntitiesValue != null) {
+            headers.put(ProxiedEntitiesUtils.PROXY_ENTITIES_CHAIN, proxiedEntitiesValue);
+        }
+        return headers;
+    }
+
+    private String getProxiedEntitesValue(String[] proxiedEntities) {
+        if (proxiedEntities == null) {
+            return null;
+        }
+
+        final List<String> proxiedEntityChain = Arrays.stream(proxiedEntities).map(ProxiedEntitiesUtils::formatProxyDn).collect(Collectors.toList());
+        return StringUtils.join(proxiedEntityChain, "");
+    }
+
+    @Override
+    public void close() throws IOException {
+        if (this.client != null) {
+            try {
+                this.client.close();
+            } catch (Exception e) {
+
+            }
+        }
+    }
+
+    /**
+     * Builder for creating a JerseyNiFiRegistryClient.
+     */
+    public static class Builder implements NiFiRegistryClient.Builder {
+
+        private NiFiRegistryClientConfig clientConfig;
+
+        @Override
+        public Builder config(final NiFiRegistryClientConfig clientConfig) {
+            this.clientConfig = clientConfig;
+            return this;
+        }
+
+        @Override
+        public NiFiRegistryClientConfig getConfig() {
+            return clientConfig;
+        }
+
+        @Override
+        public NiFiRegistryClient build() {
+            return new JerseyNiFiRegistryClient(this);
+        }
+
+    }
+
+    private static JacksonJaxbJsonProvider jacksonJaxbJsonProvider() {
+        JacksonJaxbJsonProvider jacksonJaxbJsonProvider = new JacksonJaxbJsonProvider();
+
+        ObjectMapper mapper = new ObjectMapper();
+        mapper.setPropertyInclusion(JsonInclude.Value.construct(JsonInclude.Include.NON_NULL, JsonInclude.Include.NON_NULL));
+        mapper.setAnnotationIntrospector(new JaxbAnnotationIntrospector(mapper.getTypeFactory()));
+        // Ignore unknown properties so that deployed client remain compatible with future versions of NiFi Registry that add new fields
+        mapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
+
+        SimpleModule module = new SimpleModule();
+        module.addDeserializer(BucketItem[].class, new BucketItemDeserializer());
+        mapper.registerModule(module);
+
+        jacksonJaxbJsonProvider.setMapper(mapper);
+        return jacksonJaxbJsonProvider;
+    }
+}

http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/6f26290d/nifi-registry-core/nifi-registry-client/src/main/java/org/apache/nifi/registry/client/impl/JerseyUserClient.java
----------------------------------------------------------------------
diff --git a/nifi-registry-core/nifi-registry-client/src/main/java/org/apache/nifi/registry/client/impl/JerseyUserClient.java b/nifi-registry-core/nifi-registry-client/src/main/java/org/apache/nifi/registry/client/impl/JerseyUserClient.java
new file mode 100644
index 0000000..7625f35
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-client/src/main/java/org/apache/nifi/registry/client/impl/JerseyUserClient.java
@@ -0,0 +1,47 @@
+/*
+ * 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.client.impl;
+
+import org.apache.nifi.registry.client.NiFiRegistryException;
+import org.apache.nifi.registry.client.UserClient;
+import org.apache.nifi.registry.authorization.CurrentUser;
+
+import javax.ws.rs.client.WebTarget;
+import java.io.IOException;
+import java.util.Collections;
+import java.util.Map;
+
+public class JerseyUserClient extends AbstractJerseyClient implements UserClient {
+
+    private final WebTarget accessTarget;
+
+    public JerseyUserClient(final WebTarget baseTarget) {
+        this(baseTarget, Collections.emptyMap());
+    }
+
+    public JerseyUserClient(final WebTarget baseTarget, final Map<String,String> headers) {
+        super(headers);
+        this.accessTarget = baseTarget.path("/access");
+    }
+
+    @Override
+    public CurrentUser getAccessStatus() throws NiFiRegistryException, IOException {
+        return executeAction("Error retrieving access status for the current user", () -> {
+            return getRequestBuilder(accessTarget).get(CurrentUser.class);
+        });
+    }
+}

http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/6f26290d/nifi-registry-core/nifi-registry-data-model/pom.xml
----------------------------------------------------------------------
diff --git a/nifi-registry-core/nifi-registry-data-model/pom.xml b/nifi-registry-core/nifi-registry-data-model/pom.xml
new file mode 100644
index 0000000..3b63ddc
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-data-model/pom.xml
@@ -0,0 +1,41 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 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. -->
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+    <modelVersion>4.0.0</modelVersion>
+    <parent>
+        <groupId>org.apache.nifi.registry</groupId>
+        <artifactId>nifi-registry-core</artifactId>
+        <version>0.3.0-SNAPSHOT</version>
+    </parent>
+    
+    <artifactId>nifi-registry-data-model</artifactId>
+    <packaging>jar</packaging>
+    
+    <dependencies>
+        <dependency>
+            <groupId>io.swagger</groupId>
+            <artifactId>swagger-annotations</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>javax.validation</groupId>
+            <artifactId>validation-api</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>javax.ws.rs</groupId>
+            <artifactId>javax.ws.rs-api</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>org.apache.commons</groupId>
+            <artifactId>commons-lang3</artifactId>
+        </dependency>
+    </dependencies>
+</project>


[27/51] [partial] nifi-registry git commit: NIFIREG-201 Refactoring project structure to better isolate extensions

Posted by kd...@apache.org.
http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/6f26290d/nifi-registry-core/nifi-registry-framework/src/test/java/org/apache/nifi/registry/service/TestRegistryService.java
----------------------------------------------------------------------
diff --git a/nifi-registry-core/nifi-registry-framework/src/test/java/org/apache/nifi/registry/service/TestRegistryService.java b/nifi-registry-core/nifi-registry-framework/src/test/java/org/apache/nifi/registry/service/TestRegistryService.java
new file mode 100644
index 0000000..95e2d1a
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-framework/src/test/java/org/apache/nifi/registry/service/TestRegistryService.java
@@ -0,0 +1,1236 @@
+/*
+ * 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.service;
+
+import org.apache.nifi.registry.bucket.Bucket;
+import org.apache.nifi.registry.db.entity.BucketEntity;
+import org.apache.nifi.registry.db.entity.FlowEntity;
+import org.apache.nifi.registry.db.entity.FlowSnapshotEntity;
+import org.apache.nifi.registry.diff.ComponentDifference;
+import org.apache.nifi.registry.diff.ComponentDifferenceGroup;
+import org.apache.nifi.registry.diff.VersionedFlowDifference;
+import org.apache.nifi.registry.exception.ResourceNotFoundException;
+import org.apache.nifi.registry.flow.FlowPersistenceProvider;
+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.flow.VersionedProcessGroup;
+import org.apache.nifi.registry.flow.VersionedProcessor;
+import org.apache.nifi.registry.serialization.Serializer;
+import org.apache.nifi.registry.serialization.VersionedProcessGroupSerializer;
+import org.junit.Assert;
+import org.junit.Before;
+import org.junit.Test;
+import org.mockito.invocation.InvocationOnMock;
+import org.mockito.stubbing.Answer;
+
+import javax.validation.ConstraintViolationException;
+import javax.validation.Validation;
+import javax.validation.Validator;
+import javax.validation.ValidatorFactory;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.Date;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Optional;
+import java.util.Set;
+import java.util.SortedSet;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertTrue;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.ArgumentMatchers.anyString;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.doAnswer;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+public class TestRegistryService {
+
+    private MetadataService metadataService;
+    private FlowPersistenceProvider flowPersistenceProvider;
+    private Serializer<VersionedProcessGroup> snapshotSerializer;
+    private Validator validator;
+
+    private RegistryService registryService;
+
+    @Before
+    public void setup() {
+        metadataService = mock(MetadataService.class);
+        flowPersistenceProvider = mock(FlowPersistenceProvider.class);
+        snapshotSerializer = mock(VersionedProcessGroupSerializer.class);
+
+        final ValidatorFactory validatorFactory = Validation.buildDefaultValidatorFactory();
+        validator = validatorFactory.getValidator();
+
+        registryService = new RegistryService(metadataService, flowPersistenceProvider, snapshotSerializer, validator);
+    }
+
+    // ---------------------- Test Bucket methods ---------------------------------------------
+
+    @Test
+    public void testCreateBucketValid() {
+        final Bucket bucket = new Bucket();
+        bucket.setName("My Bucket");
+        bucket.setDescription("This is my bucket.");
+
+        when(metadataService.getBucketsByName(bucket.getName())).thenReturn(Collections.emptyList());
+
+        doAnswer(createBucketAnswer()).when(metadataService).createBucket(any(BucketEntity.class));
+
+        final Bucket createdBucket = registryService.createBucket(bucket);
+        assertNotNull(createdBucket);
+        assertNotNull(createdBucket.getIdentifier());
+        assertNotNull(createdBucket.getCreatedTimestamp());
+
+        assertEquals(bucket.getName(), createdBucket.getName());
+        assertEquals(bucket.getDescription(), createdBucket.getDescription());
+    }
+
+    @Test(expected = IllegalStateException.class)
+    public void testCreateBucketWithSameName() {
+        final Bucket bucket = new Bucket();
+        bucket.setName("My Bucket");
+        bucket.setDescription("This is my bucket.");
+
+        final BucketEntity existingBucket = new BucketEntity();
+        existingBucket.setId("b1");
+        existingBucket.setName("My Bucket");
+        existingBucket.setCreated(new Date());
+
+        when(metadataService.getBucketsByName(bucket.getName())).thenReturn(Collections.singletonList(existingBucket));
+
+        // should throw exception since a bucket with the same name exists
+        registryService.createBucket(bucket);
+    }
+
+    @Test(expected = ConstraintViolationException.class)
+    public void testCreateBucketWithMissingName() {
+        final Bucket bucket = new Bucket();
+        when(metadataService.getBucketsByName(bucket.getName())).thenReturn(Collections.emptyList());
+        registryService.createBucket(bucket);
+    }
+
+    @Test
+    public void testGetExistingBucket() {
+        final BucketEntity existingBucket = new BucketEntity();
+        existingBucket.setId("b1");
+        existingBucket.setName("My Bucket");
+        existingBucket.setDescription("This is my bucket");
+        existingBucket.setCreated(new Date());
+
+        when(metadataService.getBucketById(existingBucket.getId())).thenReturn(existingBucket);
+
+        final Bucket bucket = registryService.getBucket(existingBucket.getId());
+        assertNotNull(bucket);
+        assertEquals(existingBucket.getId(), bucket.getIdentifier());
+        assertEquals(existingBucket.getName(), bucket.getName());
+        assertEquals(existingBucket.getDescription(), bucket.getDescription());
+        assertEquals(existingBucket.getCreated().getTime(), bucket.getCreatedTimestamp());
+    }
+
+    @Test(expected = ResourceNotFoundException.class)
+    public void testGetBucketDoesNotExist() {
+        when(metadataService.getBucketById(any(String.class))).thenReturn(null);
+        registryService.getBucket("does-not-exist");
+    }
+
+    @Test(expected = IllegalArgumentException.class)
+    public void testUpdateBucketWithoutId() {
+        final Bucket bucket = new Bucket();
+        bucket.setName("My Bucket");
+        bucket.setDescription("This is my bucket.");
+        registryService.updateBucket(bucket);
+    }
+
+    @Test(expected = ResourceNotFoundException.class)
+    public void testUpdateBucketDoesNotExist() {
+        final Bucket bucket = new Bucket();
+        bucket.setIdentifier("b1");
+        bucket.setName("My Bucket");
+        bucket.setDescription("This is my bucket.");
+        registryService.updateBucket(bucket);
+
+        when(metadataService.getBucketById(any(String.class))).thenReturn(null);
+        registryService.updateBucket(bucket);
+    }
+
+    @Test(expected = IllegalStateException.class)
+    public void testUpdateBucketWithSameNameAsExistingBucket() {
+        final BucketEntity bucketToUpdate = new BucketEntity();
+        bucketToUpdate.setId("b1");
+        bucketToUpdate.setName("My Bucket");
+        bucketToUpdate.setDescription("This is my bucket");
+        bucketToUpdate.setCreated(new Date());
+
+        when(metadataService.getBucketById(bucketToUpdate.getId())).thenReturn(bucketToUpdate);
+
+        final BucketEntity otherBucket = new BucketEntity();
+        otherBucket.setId("b2");
+        otherBucket.setName("My Bucket #2");
+        otherBucket.setDescription("This is my bucket");
+        otherBucket.setCreated(new Date());
+
+        when(metadataService.getBucketsByName(otherBucket.getName())).thenReturn(Collections.singletonList(otherBucket));
+
+        // should fail because other bucket has the same name
+        final Bucket updatedBucket = new Bucket();
+        updatedBucket.setIdentifier(bucketToUpdate.getId());
+        updatedBucket.setName("My Bucket #2");
+        updatedBucket.setDescription(bucketToUpdate.getDescription());
+
+        registryService.updateBucket(updatedBucket);
+    }
+
+    @Test
+    public void testUpdateBucket() {
+        final BucketEntity bucketToUpdate = new BucketEntity();
+        bucketToUpdate.setId("b1");
+        bucketToUpdate.setName("My Bucket");
+        bucketToUpdate.setDescription("This is my bucket");
+        bucketToUpdate.setCreated(new Date());
+
+        when(metadataService.getBucketById(bucketToUpdate.getId())).thenReturn(bucketToUpdate);
+
+        doAnswer(updateBucketAnswer()).when(metadataService).updateBucket(any(BucketEntity.class));
+
+        final Bucket updatedBucket = new Bucket();
+        updatedBucket.setIdentifier(bucketToUpdate.getId());
+        updatedBucket.setName("Updated Name");
+        updatedBucket.setDescription("Updated Description");
+
+        final Bucket result = registryService.updateBucket(updatedBucket);
+        assertNotNull(result);
+        assertEquals(updatedBucket.getName(), result.getName());
+        assertEquals(updatedBucket.getDescription(), result.getDescription());
+    }
+
+    @Test
+    public void testUpdateBucketPartial() {
+        final BucketEntity bucketToUpdate = new BucketEntity();
+        bucketToUpdate.setId("b1");
+        bucketToUpdate.setName("My Bucket");
+        bucketToUpdate.setDescription("This is my bucket");
+        bucketToUpdate.setCreated(new Date());
+
+        when(metadataService.getBucketById(bucketToUpdate.getId())).thenReturn(bucketToUpdate);
+
+        doAnswer(updateBucketAnswer()).when(metadataService).updateBucket(any(BucketEntity.class));
+
+        final Bucket updatedBucket = new Bucket();
+        updatedBucket.setIdentifier(bucketToUpdate.getId());
+        updatedBucket.setName("Updated Name");
+        updatedBucket.setDescription(null);
+
+        // name should be updated but description should not be changed
+        final Bucket result = registryService.updateBucket(updatedBucket);
+        assertNotNull(result);
+        assertEquals(updatedBucket.getName(), result.getName());
+        assertEquals(bucketToUpdate.getDescription(), result.getDescription());
+    }
+
+    @Test(expected = ResourceNotFoundException.class)
+    public void testDeleteBucketDoesNotExist() {
+        final String bucketId = "b1";
+        when(metadataService.getBucketById(bucketId)).thenReturn(null);
+        registryService.deleteBucket(bucketId);
+    }
+
+    @Test
+    public void testDeleteBucketWithFlows() {
+        final BucketEntity bucketToDelete = new BucketEntity();
+        bucketToDelete.setId("b1");
+        bucketToDelete.setName("My Bucket");
+        bucketToDelete.setDescription("This is my bucket");
+        bucketToDelete.setCreated(new Date());
+
+        final FlowEntity flowToDelete = new FlowEntity();
+        flowToDelete.setId("flow1");
+        flowToDelete.setName("Flow 1");
+        flowToDelete.setDescription("This is flow 1");
+        flowToDelete.setCreated(new Date());
+
+        final List<FlowEntity> flows = new ArrayList<>();
+        flows.add(flowToDelete);
+
+        when(metadataService.getBucketById(bucketToDelete.getId())).thenReturn(bucketToDelete);
+
+        when(metadataService.getFlowsByBucket(bucketToDelete.getId())).thenReturn(flows);
+
+        final Bucket deletedBucket = registryService.deleteBucket(bucketToDelete.getId());
+        assertNotNull(deletedBucket);
+        assertEquals(bucketToDelete.getId(), deletedBucket.getIdentifier());
+
+        verify(flowPersistenceProvider, times(1))
+                .deleteAllFlowContent(eq(bucketToDelete.getId()), eq(flowToDelete.getId()));
+    }
+
+    // ---------------------- Test VersionedFlow methods ---------------------------------------------
+
+    @Test(expected = ConstraintViolationException.class)
+    public void testCreateFlowInvalid() {
+        final VersionedFlow versionedFlow = new VersionedFlow();
+        registryService.createFlow("b1", versionedFlow);
+    }
+
+    @Test(expected = ResourceNotFoundException.class)
+    public void testCreateFlowBucketDoesNotExist() {
+
+        when(metadataService.getBucketById(any(String.class))).thenReturn(null);
+
+        final VersionedFlow versionedFlow = new VersionedFlow();
+        versionedFlow.setName("My Flow");
+        versionedFlow.setBucketIdentifier("b1");
+
+        registryService.createFlow(versionedFlow.getBucketIdentifier(), versionedFlow);
+    }
+
+    @Test(expected = IllegalStateException.class)
+    public void testCreateFlowWithSameName() {
+        final BucketEntity existingBucket = new BucketEntity();
+        existingBucket.setId("b1");
+        existingBucket.setName("My Bucket");
+        existingBucket.setDescription("This is my bucket");
+        existingBucket.setCreated(new Date());
+
+        when(metadataService.getBucketById(existingBucket.getId())).thenReturn(existingBucket);
+
+        // setup a flow with the same name that already exists
+
+        final FlowEntity flowWithSameName = new FlowEntity();
+        flowWithSameName.setId("flow1");
+        flowWithSameName.setName("Flow 1");
+        flowWithSameName.setDescription("This is flow 1");
+        flowWithSameName.setCreated(new Date());
+        flowWithSameName.setModified(new Date());
+
+        when(metadataService.getFlowsByName(existingBucket.getId(), flowWithSameName.getName())).thenReturn(Collections.singletonList(flowWithSameName));
+
+        final VersionedFlow versionedFlow = new VersionedFlow();
+        versionedFlow.setName(flowWithSameName.getName());
+        versionedFlow.setBucketIdentifier("b1");
+
+        registryService.createFlow(versionedFlow.getBucketIdentifier(), versionedFlow);
+    }
+
+    @Test
+    public void testCreateFlowValid() {
+        final BucketEntity existingBucket = new BucketEntity();
+        existingBucket.setId("b1");
+        existingBucket.setName("My Bucket");
+        existingBucket.setDescription("This is my bucket");
+        existingBucket.setCreated(new Date());
+
+        when(metadataService.getBucketById(existingBucket.getId())).thenReturn(existingBucket);
+
+        final VersionedFlow versionedFlow = new VersionedFlow();
+        versionedFlow.setName("My Flow");
+        versionedFlow.setBucketIdentifier("b1");
+
+        doAnswer(createFlowAnswer()).when(metadataService).createFlow(any(FlowEntity.class));
+
+        final VersionedFlow createdFlow = registryService.createFlow(versionedFlow.getBucketIdentifier(), versionedFlow);
+        assertNotNull(createdFlow);
+        assertNotNull(createdFlow.getIdentifier());
+        assertTrue(createdFlow.getCreatedTimestamp() > 0);
+        assertTrue(createdFlow.getModifiedTimestamp() > 0);
+        assertEquals(versionedFlow.getName(), createdFlow.getName());
+        assertEquals(versionedFlow.getBucketIdentifier(), createdFlow.getBucketIdentifier());
+        assertEquals(versionedFlow.getDescription(), createdFlow.getDescription());
+    }
+
+    @Test(expected = ResourceNotFoundException.class)
+    public void testGetFlowDoesNotExist() {
+        when(metadataService.getFlowById(any(String.class))).thenReturn(null);
+        registryService.getFlow("bucket1","flow1");
+    }
+
+    @Test(expected = ResourceNotFoundException.class)
+    public void testGetFlowDirectDoesNotExist() {
+        when(metadataService.getFlowById(any(String.class))).thenReturn(null);
+        registryService.getFlow("flow1");
+    }
+
+    @Test
+    public void testGetFlowExists() {
+        final BucketEntity existingBucket = new BucketEntity();
+        existingBucket.setId("b1");
+        existingBucket.setName("My Bucket");
+        existingBucket.setDescription("This is my bucket");
+        existingBucket.setCreated(new Date());
+
+        when(metadataService.getBucketById(existingBucket.getId())).thenReturn(existingBucket);
+
+        final FlowEntity flowEntity = new FlowEntity();
+        flowEntity.setId("flow1");
+        flowEntity.setName("My Flow");
+        flowEntity.setDescription("This is my flow.");
+        flowEntity.setCreated(new Date());
+        flowEntity.setModified(new Date());
+        flowEntity.setBucketId(existingBucket.getId());
+
+        when(metadataService.getFlowByIdWithSnapshotCounts(flowEntity.getId())).thenReturn(flowEntity);
+
+        final VersionedFlow versionedFlow = registryService.getFlow(existingBucket.getId(), flowEntity.getId());
+        assertNotNull(versionedFlow);
+        assertEquals(flowEntity.getId(), versionedFlow.getIdentifier());
+        assertEquals(flowEntity.getName(), versionedFlow.getName());
+        assertEquals(flowEntity.getDescription(), versionedFlow.getDescription());
+        assertEquals(flowEntity.getBucketId(), versionedFlow.getBucketIdentifier());
+        assertEquals(existingBucket.getName(), versionedFlow.getBucketName());
+        assertEquals(flowEntity.getCreated().getTime(), versionedFlow.getCreatedTimestamp());
+        assertEquals(flowEntity.getModified().getTime(), versionedFlow.getModifiedTimestamp());
+    }
+
+    @Test
+    public void testGetFlowDirectExists() {
+        final BucketEntity existingBucket = new BucketEntity();
+        existingBucket.setId("b1");
+        existingBucket.setName("My Bucket");
+        existingBucket.setDescription("This is my bucket");
+        existingBucket.setCreated(new Date());
+
+        final FlowEntity flowEntity = new FlowEntity();
+        flowEntity.setId("flow1");
+        flowEntity.setName("My Flow");
+        flowEntity.setDescription("This is my flow.");
+        flowEntity.setCreated(new Date());
+        flowEntity.setModified(new Date());
+        flowEntity.setBucketId(existingBucket.getId());
+
+        when(metadataService.getFlowByIdWithSnapshotCounts(flowEntity.getId())).thenReturn(flowEntity);
+        when(metadataService.getBucketById(existingBucket.getId())).thenReturn(existingBucket);
+
+        final VersionedFlow versionedFlow = registryService.getFlow(flowEntity.getId());
+        assertNotNull(versionedFlow);
+        assertEquals(flowEntity.getId(), versionedFlow.getIdentifier());
+        assertEquals(flowEntity.getName(), versionedFlow.getName());
+        assertEquals(flowEntity.getDescription(), versionedFlow.getDescription());
+        assertEquals(flowEntity.getBucketId(), versionedFlow.getBucketIdentifier());
+        assertEquals(existingBucket.getName(), versionedFlow.getBucketName());
+        assertEquals(flowEntity.getCreated().getTime(), versionedFlow.getCreatedTimestamp());
+        assertEquals(flowEntity.getModified().getTime(), versionedFlow.getModifiedTimestamp());
+    }
+
+    @Test(expected = ResourceNotFoundException.class)
+    public void testGetFlowsByBucketDoesNotExist() {
+        when(metadataService.getBucketById(any(String.class))).thenReturn(null);
+        registryService.getFlows("b1");
+    }
+
+    @Test
+    public void testGetFlowsByBucketExists() {
+        final BucketEntity existingBucket = new BucketEntity();
+        existingBucket.setId("b1");
+        existingBucket.setName("My Bucket");
+        existingBucket.setDescription("This is my bucket");
+        existingBucket.setCreated(new Date());
+
+        when(metadataService.getBucketById(existingBucket.getId())).thenReturn(existingBucket);
+
+        final FlowEntity flowEntity1 = new FlowEntity();
+        flowEntity1.setId("flow1");
+        flowEntity1.setName("My Flow");
+        flowEntity1.setDescription("This is my flow.");
+        flowEntity1.setCreated(new Date());
+        flowEntity1.setModified(new Date());
+        flowEntity1.setBucketId(existingBucket.getId());
+
+        final FlowEntity flowEntity2 = new FlowEntity();
+        flowEntity2.setId("flow2");
+        flowEntity2.setName("My Flow 2");
+        flowEntity2.setDescription("This is my flow 2.");
+        flowEntity2.setCreated(new Date());
+        flowEntity2.setModified(new Date());
+        flowEntity2.setBucketId(existingBucket.getId());
+
+        final List<FlowEntity> flows = new ArrayList<>();
+        flows.add(flowEntity1);
+        flows.add(flowEntity2);
+
+        when(metadataService.getFlowsByBucket(eq(existingBucket.getId()))).thenReturn(flows);
+
+        final List<VersionedFlow> allFlows = registryService.getFlows(existingBucket.getId());
+        assertNotNull(allFlows);
+        assertEquals(2, allFlows.size());
+    }
+
+    @Test(expected = IllegalArgumentException.class)
+    public void testUpdateFlowWithoutId() {
+        final VersionedFlow versionedFlow = new VersionedFlow();
+        registryService.updateFlow(versionedFlow);
+    }
+
+    @Test(expected = ResourceNotFoundException.class)
+    public void testUpdateFlowDoesNotExist() {
+        final VersionedFlow versionedFlow = new VersionedFlow();
+        versionedFlow.setBucketIdentifier("b1");
+        versionedFlow.setIdentifier("flow1");
+
+        when(metadataService.getFlowById(versionedFlow.getIdentifier())).thenReturn(null);
+
+        registryService.updateFlow(versionedFlow);
+    }
+
+    @Test(expected = IllegalStateException.class)
+    public void testUpdateFlowWithSameNameAsExistingFlow() {
+        final BucketEntity existingBucket = new BucketEntity();
+        existingBucket.setId("b1");
+        existingBucket.setName("My Bucket");
+        existingBucket.setDescription("This is my bucket");
+        existingBucket.setCreated(new Date());
+
+        when(metadataService.getBucketById(existingBucket.getId())).thenReturn(existingBucket);
+
+        final FlowEntity flowToUpdate = new FlowEntity();
+        flowToUpdate.setId("flow1");
+        flowToUpdate.setName("My Flow");
+        flowToUpdate.setDescription("This is my flow.");
+        flowToUpdate.setCreated(new Date());
+        flowToUpdate.setModified(new Date());
+        flowToUpdate.setBucketId(existingBucket.getId());
+
+        when(metadataService.getFlowByIdWithSnapshotCounts(flowToUpdate.getId())).thenReturn(flowToUpdate);
+
+        final FlowEntity otherFlow = new FlowEntity();
+        otherFlow.setId("flow2");
+        otherFlow.setName("My Flow 2");
+        otherFlow.setDescription("This is my flow 2.");
+        otherFlow.setCreated(new Date());
+        otherFlow.setModified(new Date());
+        otherFlow.setBucketId(existingBucket.getId());
+
+        when(metadataService.getFlowsByName(existingBucket.getId(), otherFlow.getName())).thenReturn(Collections.singletonList(otherFlow));
+
+        final VersionedFlow versionedFlow = new VersionedFlow();
+        versionedFlow.setIdentifier(flowToUpdate.getId());
+        versionedFlow.setBucketIdentifier(existingBucket.getId());
+        versionedFlow.setName(otherFlow.getName());
+
+        registryService.updateFlow(versionedFlow);
+    }
+
+    @Test
+    public void testUpdateFlow() throws InterruptedException {
+        final BucketEntity existingBucket = new BucketEntity();
+        existingBucket.setId("b1");
+        existingBucket.setName("My Bucket");
+        existingBucket.setDescription("This is my bucket");
+        existingBucket.setCreated(new Date());
+
+        final FlowEntity flowToUpdate = new FlowEntity();
+        flowToUpdate.setId("flow1");
+        flowToUpdate.setName("My Flow");
+        flowToUpdate.setDescription("This is my flow.");
+        flowToUpdate.setCreated(new Date());
+        flowToUpdate.setModified(new Date());
+        flowToUpdate.setBucketId(existingBucket.getId());
+
+        when(metadataService.getBucketById(existingBucket.getId())).thenReturn(existingBucket);
+        when(metadataService.getFlowByIdWithSnapshotCounts(flowToUpdate.getId())).thenReturn(flowToUpdate);
+        when(metadataService.getFlowsByName(flowToUpdate.getName())).thenReturn(Collections.singletonList(flowToUpdate));
+
+        doAnswer(updateFlowAnswer()).when(metadataService).updateFlow(any(FlowEntity.class));
+
+        final VersionedFlow versionedFlow = new VersionedFlow();
+        versionedFlow.setBucketIdentifier(flowToUpdate.getBucketId());
+        versionedFlow.setIdentifier(flowToUpdate.getId());
+        versionedFlow.setName("New Flow Name");
+        versionedFlow.setDescription("This is a new description");
+
+        Thread.sleep(10);
+
+        final VersionedFlow updatedFlow = registryService.updateFlow(versionedFlow);
+        assertNotNull(updatedFlow);
+        assertEquals(versionedFlow.getIdentifier(), updatedFlow.getIdentifier());
+
+        // name and description should be updated
+        assertEquals(versionedFlow.getName(), updatedFlow.getName());
+        assertEquals(versionedFlow.getDescription(), updatedFlow.getDescription());
+
+        // other fields should not be updated
+        assertEquals(flowToUpdate.getBucketId(), updatedFlow.getBucketIdentifier());
+        assertEquals(flowToUpdate.getCreated().getTime(), updatedFlow.getCreatedTimestamp());
+    }
+
+    @Test(expected = ResourceNotFoundException.class)
+    public void testDeleteFlowDoesNotExist() {
+        when(metadataService.getFlowById(any(String.class))).thenReturn(null);
+        registryService.deleteFlow("b1", "flow1");
+    }
+
+    @Test
+    public void testDeleteFlowWithSnapshots() {
+        final BucketEntity existingBucket = new BucketEntity();
+        existingBucket.setId("b1");
+        existingBucket.setName("My Bucket");
+        existingBucket.setDescription("This is my bucket");
+        existingBucket.setCreated(new Date());
+
+        final FlowEntity flowToDelete = new FlowEntity();
+        flowToDelete.setId("flow1");
+        flowToDelete.setName("My Flow");
+        flowToDelete.setDescription("This is my flow.");
+        flowToDelete.setCreated(new Date());
+        flowToDelete.setModified(new Date());
+        flowToDelete.setBucketId(existingBucket.getId());
+
+        when(metadataService.getBucketById(existingBucket.getId())).thenReturn(existingBucket);
+        when(metadataService.getFlowById(flowToDelete.getId())).thenReturn(flowToDelete);
+        when(metadataService.getFlowsByName(flowToDelete.getName())).thenReturn(Collections.singletonList(flowToDelete));
+
+        final VersionedFlow deletedFlow = registryService.deleteFlow(existingBucket.getId(), flowToDelete.getId());
+        assertNotNull(deletedFlow);
+        assertEquals(flowToDelete.getId(), deletedFlow.getIdentifier());
+
+        verify(flowPersistenceProvider, times(1))
+                .deleteAllFlowContent(flowToDelete.getBucketId(), flowToDelete.getId());
+
+        verify(metadataService, times(1)).deleteFlow(flowToDelete);
+    }
+
+    // ---------------------- Test VersionedFlowSnapshot methods ---------------------------------------------
+
+    private VersionedFlowSnapshot createSnapshot() {
+        final VersionedFlowSnapshotMetadata snapshotMetadata = new VersionedFlowSnapshotMetadata();
+        snapshotMetadata.setFlowIdentifier("flow1");
+        snapshotMetadata.setVersion(1);
+        snapshotMetadata.setComments("This is the first snapshot");
+        snapshotMetadata.setBucketIdentifier("b1");
+        snapshotMetadata.setAuthor("user1");
+
+        final VersionedProcessGroup processGroup = new VersionedProcessGroup();
+        processGroup.setIdentifier("pg1");
+        processGroup.setName("My Process Group");
+
+        final VersionedFlowSnapshot snapshot = new VersionedFlowSnapshot();
+        snapshot.setSnapshotMetadata(snapshotMetadata);
+        snapshot.setFlowContents(processGroup);
+
+        return snapshot;
+    }
+
+    @Test(expected = ConstraintViolationException.class)
+    public void testCreateSnapshotInvalidMetadata() {
+        final VersionedFlowSnapshot snapshot = createSnapshot();
+        snapshot.getSnapshotMetadata().setFlowIdentifier(null);
+        registryService.createFlowSnapshot(snapshot);
+    }
+
+    @Test(expected = ConstraintViolationException.class)
+    public void testCreateSnapshotInvalidFlowContents() {
+        final VersionedFlowSnapshot snapshot = createSnapshot();
+        snapshot.setFlowContents(null);
+        registryService.createFlowSnapshot(snapshot);
+    }
+
+    @Test(expected = ConstraintViolationException.class)
+    public void testCreateSnapshotNullMetadata() {
+        final VersionedFlowSnapshot snapshot = createSnapshot();
+        snapshot.setSnapshotMetadata(null);
+        registryService.createFlowSnapshot(snapshot);
+    }
+
+    @Test(expected = ConstraintViolationException.class)
+    public void testCreateSnapshotNullFlowContents() {
+        final VersionedFlowSnapshot snapshot = createSnapshot();
+        snapshot.setFlowContents(null);
+        registryService.createFlowSnapshot(snapshot);
+    }
+
+    @Test(expected = ResourceNotFoundException.class)
+    public void testCreateSnapshotBucketDoesNotExist() {
+        when(metadataService.getBucketById(any(String.class))).thenReturn(null);
+
+        final VersionedFlowSnapshot snapshot = createSnapshot();
+        registryService.createFlowSnapshot(snapshot);
+    }
+
+    @Test(expected = ResourceNotFoundException.class)
+    public void testCreateSnapshotFlowDoesNotExist() {
+        final VersionedFlowSnapshot snapshot = createSnapshot();
+
+        final BucketEntity existingBucket = new BucketEntity();
+        existingBucket.setId("b1");
+        existingBucket.setName("My Bucket");
+        existingBucket.setDescription("This is my bucket");
+        existingBucket.setCreated(new Date());
+
+        when(metadataService.getBucketById(existingBucket.getId())).thenReturn(existingBucket);
+
+        when(metadataService.getFlowById(snapshot.getSnapshotMetadata().getFlowIdentifier())).thenReturn(null);
+
+        registryService.createFlowSnapshot(snapshot);
+    }
+
+    @Test(expected = IllegalStateException.class)
+    public void testCreateSnapshotVersionAlreadyExists() {
+        final VersionedFlowSnapshot snapshot = createSnapshot();
+
+        final BucketEntity existingBucket = new BucketEntity();
+        existingBucket.setId("b1");
+        existingBucket.setName("My Bucket");
+        existingBucket.setDescription("This is my bucket");
+        existingBucket.setCreated(new Date());
+
+        when(metadataService.getBucketById(existingBucket.getId())).thenReturn(existingBucket);
+
+        // return a flow with the existing snapshot when getFlowById is called
+        final FlowEntity existingFlow = new FlowEntity();
+        existingFlow.setId("flow1");
+        existingFlow.setName("My Flow");
+        existingFlow.setDescription("This is my flow.");
+        existingFlow.setCreated(new Date());
+        existingFlow.setModified(new Date());
+        existingFlow.setBucketId(existingBucket.getId());
+
+        when(metadataService.getFlowById(existingFlow.getId())).thenReturn(existingFlow);
+
+        // make a snapshot that has the same version as the one being created
+        final FlowSnapshotEntity existingSnapshot = new FlowSnapshotEntity();
+        existingSnapshot.setFlowId(snapshot.getSnapshotMetadata().getFlowIdentifier());
+        existingSnapshot.setVersion(snapshot.getSnapshotMetadata().getVersion());
+        existingSnapshot.setComments("This is an existing snapshot");
+        existingSnapshot.setCreated(new Date());
+        existingSnapshot.setCreatedBy("test-user");
+
+        final List<FlowSnapshotEntity> existingSnapshots = Arrays.asList(existingSnapshot);
+        when(metadataService.getSnapshots(existingFlow.getId())).thenReturn(existingSnapshots);
+
+        registryService.createFlowSnapshot(snapshot);
+    }
+
+    @Test(expected = IllegalStateException.class)
+    public void testCreateSnapshotVersionNotNextVersion() {
+        final VersionedFlowSnapshot snapshot = createSnapshot();
+
+        final BucketEntity existingBucket = new BucketEntity();
+        existingBucket.setId("b1");
+        existingBucket.setName("My Bucket");
+        existingBucket.setDescription("This is my bucket");
+        existingBucket.setCreated(new Date());
+
+        when(metadataService.getBucketById(existingBucket.getId())).thenReturn(existingBucket);
+
+        // return a flow with the existing snapshot when getFlowById is called
+        final FlowEntity existingFlow = new FlowEntity();
+        existingFlow.setId("flow1");
+        existingFlow.setName("My Flow");
+        existingFlow.setDescription("This is my flow.");
+        existingFlow.setCreated(new Date());
+        existingFlow.setModified(new Date());
+        existingFlow.setBucketId(existingBucket.getId());
+
+        // make a snapshot that has the same version as the one being created
+        final FlowSnapshotEntity existingSnapshot = new FlowSnapshotEntity();
+        existingSnapshot.setFlowId(snapshot.getSnapshotMetadata().getFlowIdentifier());
+        existingSnapshot.setVersion(snapshot.getSnapshotMetadata().getVersion());
+        existingSnapshot.setComments("This is an existing snapshot");
+        existingSnapshot.setCreated(new Date());
+        existingSnapshot.setCreatedBy("test-user");
+
+        when(metadataService.getFlowById(existingFlow.getId())).thenReturn(existingFlow);
+
+        // set the version to something that is not the next one-up version
+        snapshot.getSnapshotMetadata().setVersion(100);
+        registryService.createFlowSnapshot(snapshot);
+    }
+
+    @Test
+    public void testCreateFirstSnapshot() {
+        final VersionedFlowSnapshot snapshot = createSnapshot();
+
+        final BucketEntity existingBucket = new BucketEntity();
+        existingBucket.setId("b1");
+        existingBucket.setName("My Bucket");
+        existingBucket.setDescription("This is my bucket");
+        existingBucket.setCreated(new Date());
+
+        when(metadataService.getBucketById(existingBucket.getId())).thenReturn(existingBucket);
+
+        // return a flow with the existing snapshot when getFlowById is called
+        final FlowEntity existingFlow = new FlowEntity();
+        existingFlow.setId("flow1");
+        existingFlow.setName("My Flow");
+        existingFlow.setDescription("This is my flow.");
+        existingFlow.setCreated(new Date());
+        existingFlow.setModified(new Date());
+        existingFlow.setBucketId(existingBucket.getId());
+
+        when(metadataService.getFlowById(existingFlow.getId())).thenReturn(existingFlow);
+        when(metadataService.getFlowByIdWithSnapshotCounts(existingFlow.getId())).thenReturn(existingFlow);
+
+        final VersionedFlowSnapshot createdSnapshot = registryService.createFlowSnapshot(snapshot);
+        assertNotNull(createdSnapshot);
+        assertNotNull(createdSnapshot.getSnapshotMetadata());
+        assertNotNull(createdSnapshot.getFlow());
+        assertNotNull(createdSnapshot.getBucket());
+
+        verify(snapshotSerializer, times(1)).serialize(eq(snapshot.getFlowContents()), any(OutputStream.class));
+        verify(flowPersistenceProvider, times(1)).saveFlowContent(any(), any());
+        verify(metadataService, times(1)).createFlowSnapshot(any(FlowSnapshotEntity.class));
+    }
+
+    @Test(expected = IllegalStateException.class)
+    public void testCreateFirstSnapshotWithBadVersion() {
+        final VersionedFlowSnapshot snapshot = createSnapshot();
+
+        final BucketEntity existingBucket = new BucketEntity();
+        existingBucket.setId("b1");
+        existingBucket.setName("My Bucket");
+        existingBucket.setDescription("This is my bucket");
+        existingBucket.setCreated(new Date());
+
+        when(metadataService.getBucketById(existingBucket.getId())).thenReturn(existingBucket);
+
+        // return a flow with the existing snapshot when getFlowById is called
+        final FlowEntity existingFlow = new FlowEntity();
+        existingFlow.setId("flow1");
+        existingFlow.setName("My Flow");
+        existingFlow.setDescription("This is my flow.");
+        existingFlow.setCreated(new Date());
+        existingFlow.setModified(new Date());
+        existingFlow.setBucketId(existingBucket.getId());
+
+        when(metadataService.getFlowById(existingFlow.getId())).thenReturn(existingFlow);
+
+        // set the first version to something other than 1
+        snapshot.getSnapshotMetadata().setVersion(100);
+        registryService.createFlowSnapshot(snapshot);
+    }
+
+    @Test
+    public void testGetFlowSnapshots() {
+        final BucketEntity existingBucket = new BucketEntity();
+        existingBucket.setId("b1");
+        existingBucket.setName("My Bucket");
+        existingBucket.setDescription("This is my bucket");
+        existingBucket.setCreated(new Date());
+
+        when(metadataService.getBucketById(existingBucket.getId())).thenReturn(existingBucket);
+
+        // return a flow with the existing snapshot when getFlowById is called
+        final FlowEntity existingFlow = new FlowEntity();
+        existingFlow.setId("flow1");
+        existingFlow.setName("My Flow");
+        existingFlow.setDescription("This is my flow.");
+        existingFlow.setCreated(new Date());
+        existingFlow.setModified(new Date());
+        existingFlow.setBucketId(existingBucket.getId());
+
+        when(metadataService.getFlowById(existingFlow.getId())).thenReturn(existingFlow);
+
+        final FlowSnapshotEntity existingSnapshot1 = new FlowSnapshotEntity();
+        existingSnapshot1.setVersion(1);
+        existingSnapshot1.setFlowId(existingFlow.getId());
+        existingSnapshot1.setCreatedBy("user1");
+        existingSnapshot1.setCreated(new Date());
+        existingSnapshot1.setComments("This is snapshot 1");
+
+        final FlowSnapshotEntity existingSnapshot2 = new FlowSnapshotEntity();
+        existingSnapshot2.setVersion(2);
+        existingSnapshot2.setFlowId(existingFlow.getId());
+        existingSnapshot2.setCreatedBy("user2");
+        existingSnapshot2.setCreated(new Date());
+        existingSnapshot2.setComments("This is snapshot 2");
+
+        final List<FlowSnapshotEntity> snapshots = new ArrayList<>();
+        snapshots.add(existingSnapshot1);
+        snapshots.add(existingSnapshot2);
+
+        when(metadataService.getSnapshots(existingFlow.getId())).thenReturn(snapshots);
+
+        final SortedSet<VersionedFlowSnapshotMetadata> retrievedSnapshots = registryService.getFlowSnapshots(existingBucket.getId(), existingFlow.getId());
+        assertNotNull(retrievedSnapshots);
+        assertEquals(2, retrievedSnapshots.size());
+        // check that sorted set order is reversed
+        assertEquals(2, retrievedSnapshots.first().getVersion());
+        assertEquals(1, retrievedSnapshots.last().getVersion());
+    }
+
+    @Test
+    public void testGetFlowSnapshotsWhenNoSnapshots() {
+        final BucketEntity existingBucket = new BucketEntity();
+        existingBucket.setId("b1");
+        existingBucket.setName("My Bucket");
+        existingBucket.setDescription("This is my bucket");
+        existingBucket.setCreated(new Date());
+
+        when(metadataService.getBucketById(existingBucket.getId())).thenReturn(existingBucket);
+
+        // return a flow with the existing snapshot when getFlowById is called
+        final FlowEntity existingFlow = new FlowEntity();
+        existingFlow.setId("flow1");
+        existingFlow.setName("My Flow");
+        existingFlow.setDescription("This is my flow.");
+        existingFlow.setCreated(new Date());
+        existingFlow.setModified(new Date());
+        existingFlow.setBucketId(existingBucket.getId());
+
+        final Set<FlowSnapshotEntity> snapshots = new HashSet<>();
+
+        when(metadataService.getFlowById(existingFlow.getId())).thenReturn(existingFlow);
+
+        final SortedSet<VersionedFlowSnapshotMetadata> retrievedSnapshots = registryService.getFlowSnapshots(existingBucket.getId(), existingFlow.getId());
+        assertNotNull(retrievedSnapshots);
+        assertEquals(0, retrievedSnapshots.size());
+    }
+
+    @Test
+    public void testGetLatestSnapshotMetadataWhenVersionsExist() {
+        final BucketEntity existingBucket = new BucketEntity();
+        existingBucket.setId("b1");
+        existingBucket.setName("My Bucket");
+        existingBucket.setDescription("This is my bucket");
+        existingBucket.setCreated(new Date());
+
+        when(metadataService.getBucketById(existingBucket.getId())).thenReturn(existingBucket);
+
+        // return a flow with the existing snapshot when getFlowById is called
+        final FlowEntity existingFlow = new FlowEntity();
+        existingFlow.setId("flow1");
+        existingFlow.setName("My Flow");
+        existingFlow.setDescription("This is my flow.");
+        existingFlow.setCreated(new Date());
+        existingFlow.setModified(new Date());
+        existingFlow.setBucketId(existingBucket.getId());
+
+        when(metadataService.getFlowById(existingFlow.getId())).thenReturn(existingFlow);
+
+        final FlowSnapshotEntity existingSnapshot1 = new FlowSnapshotEntity();
+        existingSnapshot1.setVersion(1);
+        existingSnapshot1.setFlowId(existingFlow.getId());
+        existingSnapshot1.setCreatedBy("user1");
+        existingSnapshot1.setCreated(new Date());
+        existingSnapshot1.setComments("This is snapshot 1");
+
+        when(metadataService.getLatestSnapshot(existingFlow.getId())).thenReturn(existingSnapshot1);
+
+        VersionedFlowSnapshotMetadata latestMetadata = registryService.getLatestFlowSnapshotMetadata(existingBucket.getId(), existingFlow.getId());
+        assertNotNull(latestMetadata);
+        assertEquals(1, latestMetadata.getVersion());
+    }
+
+    @Test
+    public void testGetLatestSnapshotMetadataWhenNoVersionsExist() {
+        final BucketEntity existingBucket = new BucketEntity();
+        existingBucket.setId("b1");
+        existingBucket.setName("My Bucket");
+        existingBucket.setDescription("This is my bucket");
+        existingBucket.setCreated(new Date());
+
+        when(metadataService.getBucketById(existingBucket.getId())).thenReturn(existingBucket);
+
+        // return a flow with the existing snapshot when getFlowById is called
+        final FlowEntity existingFlow = new FlowEntity();
+        existingFlow.setId("flow1");
+        existingFlow.setName("My Flow");
+        existingFlow.setDescription("This is my flow.");
+        existingFlow.setCreated(new Date());
+        existingFlow.setModified(new Date());
+        existingFlow.setBucketId(existingBucket.getId());
+
+        when(metadataService.getFlowById(existingFlow.getId())).thenReturn(existingFlow);
+
+        final FlowSnapshotEntity existingSnapshot1 = new FlowSnapshotEntity();
+        existingSnapshot1.setVersion(1);
+        existingSnapshot1.setFlowId(existingFlow.getId());
+        existingSnapshot1.setCreatedBy("user1");
+        existingSnapshot1.setCreated(new Date());
+        existingSnapshot1.setComments("This is snapshot 1");
+
+        when(metadataService.getLatestSnapshot(existingFlow.getId())).thenReturn(null);
+
+        try {
+            registryService.getLatestFlowSnapshotMetadata(existingBucket.getId(), existingFlow.getId());
+            Assert.fail("Should have thrown exception");
+        } catch (ResourceNotFoundException e) {
+            assertEquals("The specified flow ID has no versions", e.getMessage());
+        }
+    }
+
+    @Test(expected = ResourceNotFoundException.class)
+    public void testGetSnapshotDoesNotExistInMetadataProvider() {
+        final String bucketId = "b1";
+        final String flowId = "flow1";
+        final Integer version = 1;
+        when(metadataService.getFlowSnapshot(flowId, version)).thenReturn(null);
+        registryService.getFlowSnapshot(bucketId, flowId, version);
+    }
+
+    @Test(expected = IllegalStateException.class)
+    public void testGetSnapshotDoesNotExistInPersistenceProvider() {
+        final BucketEntity existingBucket = createBucketEntity("b1");
+        final FlowEntity existingFlow = createFlowEntity(existingBucket.getId());
+        final FlowSnapshotEntity existingSnapshot = createFlowSnapshotEntity(existingFlow.getId());
+
+        existingFlow.setSnapshotCount(10);
+
+        when(metadataService.getBucketById(existingBucket.getId()))
+                .thenReturn(existingBucket);
+
+        when(metadataService.getFlowByIdWithSnapshotCounts(existingFlow.getId()))
+                .thenReturn(existingFlow);
+
+        when(metadataService.getFlowSnapshot(existingFlow.getId(), existingSnapshot.getVersion()))
+                .thenReturn(existingSnapshot);
+
+        when(flowPersistenceProvider.getFlowContent(
+                existingBucket.getId(),
+                existingSnapshot.getFlowId(),
+                existingSnapshot.getVersion()
+        )).thenReturn(null);
+
+        registryService.getFlowSnapshot(existingBucket.getId(), existingSnapshot.getFlowId(), existingSnapshot.getVersion());
+    }
+
+    @Test
+    public void testGetSnapshotExists() {
+        final BucketEntity existingBucket = createBucketEntity("b1");
+        final FlowEntity existingFlow = createFlowEntity(existingBucket.getId());
+        final FlowSnapshotEntity existingSnapshot = createFlowSnapshotEntity(existingFlow.getId());
+
+        existingFlow.setSnapshotCount(10);
+
+        when(metadataService.getBucketById(existingBucket.getId()))
+                .thenReturn(existingBucket);
+
+        when(metadataService.getFlowByIdWithSnapshotCounts(existingFlow.getId()))
+                .thenReturn(existingFlow);
+
+        when(metadataService.getFlowSnapshot(existingFlow.getId(), existingSnapshot.getVersion()))
+                .thenReturn(existingSnapshot);
+
+        // return a non-null, non-zero-length array so something gets passed to the serializer
+        when(flowPersistenceProvider.getFlowContent(
+                existingBucket.getId(),
+                existingSnapshot.getFlowId(),
+                existingSnapshot.getVersion()
+        )).thenReturn(new byte[10]);
+
+        final VersionedFlowSnapshot snapshotToDeserialize = createSnapshot();
+        when(snapshotSerializer.deserialize(any(InputStream.class))).thenReturn(snapshotToDeserialize.getFlowContents());
+
+        final VersionedFlowSnapshot returnedSnapshot = registryService.getFlowSnapshot(
+                existingBucket.getId(), existingSnapshot.getFlowId(), existingSnapshot.getVersion());
+        assertNotNull(returnedSnapshot);
+        assertNotNull(returnedSnapshot.getSnapshotMetadata());
+
+        final VersionedFlowSnapshotMetadata snapshotMetadata = returnedSnapshot.getSnapshotMetadata();
+        assertEquals(existingSnapshot.getVersion().intValue(), snapshotMetadata.getVersion());
+        assertEquals(existingBucket.getId(), snapshotMetadata.getBucketIdentifier());
+        assertEquals(existingSnapshot.getFlowId(), snapshotMetadata.getFlowIdentifier());
+        assertEquals(existingSnapshot.getCreated(), new Date(snapshotMetadata.getTimestamp()));
+        assertEquals(existingSnapshot.getCreatedBy(), snapshotMetadata.getAuthor());
+        assertEquals(existingSnapshot.getComments(), snapshotMetadata.getComments());
+
+        final VersionedFlow versionedFlow = returnedSnapshot.getFlow();
+        assertNotNull(versionedFlow);
+        assertNotNull(versionedFlow.getVersionCount());
+        assertTrue(versionedFlow.getVersionCount() > 0);
+
+        final Bucket bucket = returnedSnapshot.getBucket();
+        assertNotNull(bucket);
+    }
+
+    @Test(expected = ResourceNotFoundException.class)
+    public void testDeleteSnapshotDoesNotExist() {
+        final String bucketId = "b1";
+        final String flowId = "flow1";
+        final Integer version = 1;
+        when(metadataService.getFlowSnapshot(flowId, version)).thenReturn(null);
+        registryService.deleteFlowSnapshot(bucketId, flowId, version);
+    }
+
+    @Test
+    public void testDeleteSnapshotExists() {
+        final BucketEntity existingBucket = createBucketEntity("b1");
+        final FlowEntity existingFlow = createFlowEntity(existingBucket.getId());
+        final FlowSnapshotEntity existingSnapshot = createFlowSnapshotEntity(existingFlow.getId());
+
+        when(metadataService.getBucketById(existingBucket.getId()))
+                .thenReturn(existingBucket);
+
+        when(metadataService.getFlowById(existingFlow.getId()))
+                .thenReturn(existingFlow);
+
+        when(metadataService.getFlowSnapshot(existingSnapshot.getFlowId(), existingSnapshot.getVersion()))
+                .thenReturn(existingSnapshot);
+
+        final VersionedFlowSnapshotMetadata deletedSnapshot = registryService.deleteFlowSnapshot(
+                existingBucket.getId(), existingSnapshot.getFlowId(), existingSnapshot.getVersion());
+
+        assertNotNull(deletedSnapshot);
+        assertEquals(existingSnapshot.getFlowId(), deletedSnapshot.getFlowIdentifier());
+
+        verify(flowPersistenceProvider, times(1)).deleteFlowContent(
+                existingBucket.getId(),
+                existingSnapshot.getFlowId(),
+                existingSnapshot.getVersion()
+        );
+
+        verify(metadataService, times(1)).deleteFlowSnapshot(existingSnapshot);
+    }
+
+    private FlowSnapshotEntity createFlowSnapshotEntity(final String flowId) {
+        final FlowSnapshotEntity existingSnapshot = new FlowSnapshotEntity();
+        existingSnapshot.setVersion(1);
+        existingSnapshot.setFlowId(flowId);
+        existingSnapshot.setComments("This is an existing snapshot");
+        existingSnapshot.setCreated(new Date());
+        existingSnapshot.setCreatedBy("test-user");
+        return existingSnapshot;
+    }
+
+    private FlowEntity createFlowEntity(final String bucketId) {
+        final FlowEntity existingFlow = new FlowEntity();
+        existingFlow.setId("flow1");
+        existingFlow.setName("My Flow");
+        existingFlow.setDescription("This is my flow.");
+        existingFlow.setCreated(new Date());
+        existingFlow.setModified(new Date());
+        existingFlow.setBucketId(bucketId);
+        return existingFlow;
+    }
+
+    private BucketEntity createBucketEntity(final String bucketId) {
+        final BucketEntity existingBucket = new BucketEntity();
+        existingBucket.setId(bucketId);
+        existingBucket.setName("My Bucket");
+        existingBucket.setDescription("This is my bucket");
+        existingBucket.setCreated(new Date());
+        return existingBucket;
+    }
+
+    // -----------------Test Flow Diff Service Method---------------------
+    @Test
+    public void testGetDiffReturnsRemovedComponentChanges() {
+        when(flowPersistenceProvider.getFlowContent(
+                anyString(), anyString(), anyInt()
+        )).thenReturn(new byte[10], new byte[10]);
+
+        final VersionedProcessGroup pgA = createVersionedProcessGroupA();
+        final VersionedProcessGroup pgB = createVersionedProcessGroupB();
+        when(snapshotSerializer.deserialize(any())).thenReturn(pgA, pgB);
+
+        final VersionedFlowDifference diff = registryService.getFlowDiff(
+                "bucketIdentifier", "flowIdentifier", 1, 2);
+
+        assertNotNull(diff);
+        Optional<ComponentDifferenceGroup> removedComponent = diff.getComponentDifferenceGroups().stream()
+                .filter(p->p.getComponentId().equals("ID-pg1")).findFirst();
+
+        assertTrue(removedComponent.isPresent());
+        assertTrue(removedComponent.get().getDifferences().iterator().next().getDifferenceType().equals("COMPONENT_REMOVED"));
+    }
+
+    @Test
+    public void testGetDiffReturnsChangesInChronologicalOrder() {
+        when(flowPersistenceProvider.getFlowContent(
+                anyString(), anyString(), anyInt()
+        )).thenReturn(new byte[10], new byte[10]);
+
+        final VersionedProcessGroup pgA = createVersionedProcessGroupA();
+        final VersionedProcessGroup pgB = createVersionedProcessGroupB();
+        when(snapshotSerializer.deserialize(any())).thenReturn(pgA, pgB);
+
+        // getFlowDiff orders the changes in ascending order of version number regardless of param order
+        final VersionedFlowDifference diff = registryService.getFlowDiff(
+                "bucketIdentifier", "flowIdentifier", 2,1);
+
+        assertNotNull(diff);
+        Optional<ComponentDifferenceGroup> nameChangedComponent = diff.getComponentDifferenceGroups().stream()
+                .filter(p->p.getComponentId().equals("ProcessorFirstV1")).findFirst();
+
+        assertTrue(nameChangedComponent.isPresent());
+
+        ComponentDifference nameChangeDifference = nameChangedComponent.get().getDifferences().stream()
+                .filter(d-> d.getDifferenceType().equals("NAME_CHANGED")).findFirst().get();
+
+        assertEquals("ProcessorFirstV1", nameChangeDifference.getValueA());
+        assertEquals("ProcessorFirstV2", nameChangeDifference.getValueB());
+    }
+
+    private VersionedProcessGroup createVersionedProcessGroupA() {
+        VersionedProcessGroup root = new VersionedProcessGroup();
+        root.setProcessGroups(new HashSet<>(Arrays.asList(createProcessGroup("ID-pg1"), createProcessGroup("ID-pg2"))));
+        // Add processors
+        root.setProcessors(new HashSet<>(Arrays.asList(createVersionedProcessor("ProcessorFirstV1"), createVersionedProcessor("ProcessorSecondV1"))));
+        return root;
+    }
+
+    private VersionedProcessGroup createProcessGroup(String identifier){
+        VersionedProcessGroup processGroup = new VersionedProcessGroup();
+        processGroup.setIdentifier(identifier);
+        return processGroup;
+    }
+    private VersionedProcessGroup createVersionedProcessGroupB() {
+        VersionedProcessGroup updated = createVersionedProcessGroupA();
+        // remove a process group
+        updated.getProcessGroups().removeIf(pg->pg.getIdentifier().equals("ID-pg1"));
+        // change the name of a processor
+        updated.getProcessors().stream().forEach(p->p.setPenaltyDuration(p.getName().equals("ProcessorFirstV1") ? "1" : "2"));
+        updated.getProcessors().stream().forEach(p->p.setName(p.getName().equals("ProcessorFirstV1") ? "ProcessorFirstV2" : p.getName()));
+        return updated;
+    }
+
+    private VersionedProcessor createVersionedProcessor(String name){
+        VersionedProcessor processor = new VersionedProcessor();
+        processor.setName(name);
+        processor.setIdentifier(name);
+        processor.setProperties(new HashMap<>());
+        return processor;
+    }
+    // -------------------------------------------------------------------
+
+    private Answer<BucketEntity> createBucketAnswer() {
+        return (InvocationOnMock invocation) -> {
+            BucketEntity bucketEntity = (BucketEntity) invocation.getArguments()[0];
+            return bucketEntity;
+        };
+    }
+
+    private Answer<BucketEntity> updateBucketAnswer() {
+        return (InvocationOnMock invocation) -> {
+            BucketEntity bucketEntity = (BucketEntity) invocation.getArguments()[0];
+            return bucketEntity;
+        };
+    }
+
+    private Answer<FlowEntity> createFlowAnswer() {
+        return (InvocationOnMock invocation) -> {
+            final FlowEntity flowEntity = (FlowEntity) invocation.getArguments()[0];
+            return flowEntity;
+        };
+    }
+
+    private Answer<FlowEntity> updateFlowAnswer() {
+        return (InvocationOnMock invocation) -> {
+            final FlowEntity flowEntity = (FlowEntity) invocation.getArguments()[0];
+            return flowEntity;
+        };
+    }
+}

http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/6f26290d/nifi-registry-core/nifi-registry-framework/src/test/resources/application.properties
----------------------------------------------------------------------
diff --git a/nifi-registry-core/nifi-registry-framework/src/test/resources/application.properties b/nifi-registry-core/nifi-registry-framework/src/test/resources/application.properties
new file mode 100644
index 0000000..1e0d7c9
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-framework/src/test/resources/application.properties
@@ -0,0 +1,25 @@
+# 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.
+
+# Properties for Spring Boot tests
+
+# Properties for Spring Boot integration tests
+# Documentation for commoon Spring Boot application properties can be found at:
+# https://docs.spring.io/spring-boot/docs/current/reference/html/common-application-properties.html
+
+# These verbose log levels can be enabled locally for dev testing, but disable them in the repo to minimize travis logs.
+#logging.level.org.springframework.core.io.support: DEBUG
+#logging.level.org.springframework.context.annotation: DEBUG
+#logging.level.org.springframework.web: DEBUG

http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/6f26290d/nifi-registry-core/nifi-registry-framework/src/test/resources/db/migration/V999999.1__test-setup.sql
----------------------------------------------------------------------
diff --git a/nifi-registry-core/nifi-registry-framework/src/test/resources/db/migration/V999999.1__test-setup.sql b/nifi-registry-core/nifi-registry-framework/src/test/resources/db/migration/V999999.1__test-setup.sql
new file mode 100644
index 0000000..8f3cfa8
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-framework/src/test/resources/db/migration/V999999.1__test-setup.sql
@@ -0,0 +1,70 @@
+-- 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.
+
+-- test data for buckets
+
+insert into bucket (id, name, description, created)
+  values ('1', 'Bucket 1', 'This is test bucket 1', parsedatetime('2017-09-11 12:51:00.000 UTC', 'yyyy-MM-dd hh:mm:ss.SSS z'));
+
+insert into bucket (id, name, description, created)
+  values ('2', 'Bucket 2', 'This is test bucket 2', parsedatetime('2017-09-11 12:52:00.000 UTC', 'yyyy-MM-dd hh:mm:ss.SSS z'));
+
+insert into bucket (id, name, description, created)
+  values ('3', 'Bucket 3', 'This is test bucket 3', parsedatetime('2017-09-11 12:53:00.000 UTC', 'yyyy-MM-dd hh:mm:ss.SSS z'));
+
+insert into bucket (id, name, description, created)
+  values ('4', 'Bucket 4', 'This is test bucket 4', parsedatetime('2017-09-11 12:54:00.000 UTC', 'yyyy-MM-dd hh:mm:ss.SSS z'));
+
+insert into bucket (id, name, description, created)
+  values ('5', 'Bucket 5', 'This is test bucket 5', parsedatetime('2017-09-11 12:55:00.000 UTC', 'yyyy-MM-dd hh:mm:ss.SSS z'));
+
+insert into bucket (id, name, description, created)
+  values ('6', 'Bucket 6', 'This is test bucket 6', parsedatetime('2017-09-11 12:56:00.000 UTC', 'yyyy-MM-dd hh:mm:ss.SSS z'));
+
+
+-- test data for flows
+
+insert into bucket_item (id, name, description, created, modified, item_type, bucket_id)
+  values ('1', 'Flow 1', 'This is flow 1 bucket 1', parsedatetime('2017-09-11 12:56:00.000 UTC', 'yyyy-MM-dd hh:mm:ss.SSS z'), parsedatetime('2017-09-11 12:56:00.000 UTC', 'yyyy-MM-dd hh:mm:ss.SSS z'), 'FLOW', '1');
+
+insert into flow (id) values ('1');
+
+insert into bucket_item (id, name, description, created, modified, item_type, bucket_id)
+  values ('2', 'Flow 2', 'This is flow 2 bucket 1', parsedatetime('2017-09-11 12:56:00.000 UTC', 'yyyy-MM-dd hh:mm:ss.SSS z'), parsedatetime('2017-09-11 12:56:00.000 UTC', 'yyyy-MM-dd hh:mm:ss.SSS z'), 'FLOW', '1');
+
+insert into flow (id) values ('2');
+
+insert into bucket_item (id, name, description, created, modified, item_type, bucket_id)
+  values ('3', 'Flow 1', 'This is flow 1 bucket 2', parsedatetime('2017-09-11 12:56:00.000 UTC', 'yyyy-MM-dd hh:mm:ss.SSS z'), parsedatetime('2017-09-11 12:56:00.000 UTC', 'yyyy-MM-dd hh:mm:ss.SSS z'), 'FLOW', '2');
+
+insert into flow (id) values ('3');
+
+
+-- test data for flow snapshots
+
+insert into flow_snapshot (flow_id, version, created, created_by, comments)
+  values ('1', 1, parsedatetime('2017-09-11 12:57:00.000 UTC', 'yyyy-MM-dd hh:mm:ss.SSS z'), 'user1', 'This is flow 1 snapshot 1');
+
+insert into flow_snapshot (flow_id, version, created, created_by, comments)
+  values ('1', 2, parsedatetime('2017-09-11 12:58:00.000 UTC', 'yyyy-MM-dd hh:mm:ss.SSS z'), 'user1', 'This is flow 1 snapshot 2');
+
+insert into flow_snapshot (flow_id, version, created, created_by, comments)
+  values ('1', 3, parsedatetime('2017-09-11 12:59:00.000 UTC', 'yyyy-MM-dd hh:mm:ss.SSS z'), 'user1', 'This is flow 1 snapshot 3');
+
+
+-- test data for signing keys
+
+insert into signing_key (id, tenant_identity, key_value)
+  values ('1', 'unit_test_tenant_identity', '0123456789abcdef');
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/6f26290d/nifi-registry-core/nifi-registry-framework/src/test/resources/nifi-example.ldif
----------------------------------------------------------------------
diff --git a/nifi-registry-core/nifi-registry-framework/src/test/resources/nifi-example.ldif b/nifi-registry-core/nifi-registry-framework/src/test/resources/nifi-example.ldif
new file mode 100644
index 0000000..c91feac
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-framework/src/test/resources/nifi-example.ldif
@@ -0,0 +1,166 @@
+## ---------------------------------------------------------------------------
+## 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.
+## ---------------------------------------------------------------------------
+
+version: 1
+
+dn: o=nifi
+objectclass: extensibleObject
+objectclass: top
+objectclass: domain
+dc: nifi
+o: nifi
+
+dn: ou=users,o=nifi
+objectClass: organizationalUnit
+objectClass: top
+ou: users
+
+dn: ou=users-2,o=nifi
+objectClass: organizationalUnit
+objectClass: top
+ou: users-2
+
+dn: cn=User 1,ou=users,o=nifi
+objectClass: organizationalPerson
+objectClass: person
+objectClass: inetOrgPerson
+objectClass: top
+cn: User 1
+sn: User1
+uid: user1
+
+dn: cn=User 2,ou=users,o=nifi
+objectClass: organizationalPerson
+objectClass: person
+objectClass: inetOrgPerson
+objectClass: top
+cn: User 2
+sn: User2
+uid: user2
+
+dn: cn=User 3,ou=users,o=nifi
+objectClass: organizationalPerson
+objectClass: person
+objectClass: inetOrgPerson
+objectClass: top
+cn: User 3
+sn: User3
+uid: user3
+
+## since the embedded ldap does not support memberof, we are using description to simulate
+
+dn: cn=User 4,ou=users,o=nifi
+objectClass: organizationalPerson
+objectClass: person
+objectClass: inetOrgPerson
+objectClass: top
+cn: User 4
+sn: User4
+description: cn=team1,ou=groups,o=nifi
+uid: user4
+
+dn: cn=User 5,ou=users,o=nifi
+objectClass: organizationalPerson
+objectClass: person
+objectClass: inetOrgPerson
+objectClass: top
+cn: User 5
+sn: User5
+description: cn=team1,ou=groups,o=nifi
+uid: user5
+
+dn: cn=User 6,ou=users,o=nifi
+objectClass: organizationalPerson
+objectClass: person
+objectClass: inetOrgPerson
+objectClass: top
+cn: User 6
+sn: User6
+description: cn=team2,ou=groups,o=nifi
+uid: user6
+
+dn: cn=User 7,ou=users,o=nifi
+objectClass: organizationalPerson
+objectClass: person
+objectClass: inetOrgPerson
+objectClass: top
+cn: User 7
+sn: User7
+description: cn=team2,ou=groups,o=nifi
+uid: user7
+
+dn: cn=User 8,ou=users,o=nifi
+objectClass: organizationalPerson
+objectClass: person
+objectClass: inetOrgPerson
+objectClass: top
+cn: User 8
+sn: User8
+uid: user8
+
+dn: cn=User 9,ou=users-2,o=nifi
+objectClass: organizationalPerson
+objectClass: person
+objectClass: inetOrgPerson
+objectClass: top
+cn: User 9
+sn: User9
+description: team3
+uid: user9
+
+dn: ou=groups,o=nifi
+objectClass: organizationalUnit
+objectClass: top
+ou: groups
+
+dn: ou=groups-2,o=nifi
+objectClass: organizationalUnit
+objectClass: top
+ou: groups
+
+dn: cn=admins,ou=groups,o=nifi
+objectClass: groupOfNames
+objectClass: top
+cn: admins
+member: cn=User 1,ou=users,o=nifi
+member: cn=User 3,ou=users,o=nifi
+
+dn: cn=read-only,ou=groups,o=nifi
+objectClass: groupOfNames
+objectClass: top
+cn: read-only
+member: cn=User 2,ou=users,o=nifi
+
+dn: cn=team1,ou=groups,o=nifi
+objectClass: groupOfNames
+objectClass: top
+cn: team1
+member: cn=User 1,ou=users,o=nifi
+
+dn: cn=team2,ou=groups,o=nifi
+objectClass: groupOfNames
+objectClass: top
+cn: team2
+member: cn=User 1,ou=users,o=nifi
+
+## since the embedded ldap requires member to be fqdn, we are simulating using room and description
+
+dn: cn=team3,ou=groups-2,o=nifi
+objectClass: room
+objectClass: top
+cn: team3
+description: user9

http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/6f26290d/nifi-registry-core/nifi-registry-framework/src/test/resources/provider/hook/bad-script-provider.xml
----------------------------------------------------------------------
diff --git a/nifi-registry-core/nifi-registry-framework/src/test/resources/provider/hook/bad-script-provider.xml b/nifi-registry-core/nifi-registry-framework/src/test/resources/provider/hook/bad-script-provider.xml
new file mode 100644
index 0000000..568e756
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-framework/src/test/resources/provider/hook/bad-script-provider.xml
@@ -0,0 +1,30 @@
+<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
+<!--
+  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.
+-->
+<providers>
+
+    <flowPersistenceProvider>
+        <class>org.apache.nifi.registry.provider.MockFlowPersistenceProvider</class>
+        <property name="Flow Property 1">flow foo</property>
+        <property name="Flow Property 2">flow bar</property>
+    </flowPersistenceProvider>
+
+    <eventHookProvider>
+    	<class>org.apache.nifi.registry.provider.hook.ScriptEventHookProvider</class>
+    	<property name="Script Path"></property>
+    	<property name="Working Directory"></property>
+    </eventHookProvider>
+
+</providers>
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/6f26290d/nifi-registry-core/nifi-registry-framework/src/test/resources/provider/providers-class-not-found.xml
----------------------------------------------------------------------
diff --git a/nifi-registry-core/nifi-registry-framework/src/test/resources/provider/providers-class-not-found.xml b/nifi-registry-core/nifi-registry-framework/src/test/resources/provider/providers-class-not-found.xml
new file mode 100644
index 0000000..9adba54
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-framework/src/test/resources/provider/providers-class-not-found.xml
@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
+<!--
+  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.
+-->
+<providers>
+
+    <flowPersistenceProvider>
+        <class>org.apache.nifi.registry.provider.FlowProviderXXX</class>
+        <property name="Flow Property 1">foo</property>
+        <property name="Flow Property 2">bar</property>
+    </flowPersistenceProvider>
+
+</providers>
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/6f26290d/nifi-registry-core/nifi-registry-framework/src/test/resources/provider/providers-good.xml
----------------------------------------------------------------------
diff --git a/nifi-registry-core/nifi-registry-framework/src/test/resources/provider/providers-good.xml b/nifi-registry-core/nifi-registry-framework/src/test/resources/provider/providers-good.xml
new file mode 100644
index 0000000..32414e5
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-framework/src/test/resources/provider/providers-good.xml
@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
+<!--
+  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.
+-->
+<providers>
+
+    <flowPersistenceProvider>
+        <class>org.apache.nifi.registry.provider.MockFlowPersistenceProvider</class>
+        <property name="Flow Property 1">flow foo</property>
+        <property name="Flow Property 2">flow bar</property>
+    </flowPersistenceProvider>
+
+</providers>
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/6f26290d/nifi-registry-core/nifi-registry-framework/src/test/resources/security/authorizers-bad-ap-provider-ids.xml
----------------------------------------------------------------------
diff --git a/nifi-registry-core/nifi-registry-framework/src/test/resources/security/authorizers-bad-ap-provider-ids.xml b/nifi-registry-core/nifi-registry-framework/src/test/resources/security/authorizers-bad-ap-provider-ids.xml
new file mode 100644
index 0000000..dcbc36a
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-framework/src/test/resources/security/authorizers-bad-ap-provider-ids.xml
@@ -0,0 +1,47 @@
+<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
+<!--
+  ~ 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.
+  -->
+<authorizers>
+
+    <userGroupProvider>
+        <identifier>file-user-group-provider</identifier>
+        <class>org.apache.nifi.registry.security.authorization.file.FileUserGroupProvider</class>
+        <property name="Users File">./target/test-classes/security/users.xml</property>
+    </userGroupProvider>
+
+    <accessPolicyProvider>
+        <identifier>file-access-policy-provider</identifier>
+        <class>org.apache.nifi.registry.security.authorization.file.FileAccessPolicyProvider</class>
+        <property name="User Group Provider">file-user-group-provider</property>
+        <property name="Authorizations File">./target/test-classes/security/authorizations1.xml</property>
+    </accessPolicyProvider>
+
+    <accessPolicyProvider>
+        <!-- this is incorrect: identifiers must be unique across all accessPolicyProviders -->
+        <identifier>file-access-policy-provider</identifier>
+        <class>org.apache.nifi.registry.security.authorization.file.FileAccessPolicyProvider</class>
+        <property name="User Group Provider">file-user-group-provider</property>
+        <property name="Authorizations File">./target/test-classes/security/authorizations2.xml</property>
+    </accessPolicyProvider>
+
+    <authorizer>
+        <identifier>managed-authorizer</identifier>
+        <class>org.apache.nifi.registry.security.authorization.StandardManagedAuthorizer</class>
+        <property name="Access Policy Provider">file-access-policy-provider</property>
+    </authorizer>
+
+</authorizers>
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/6f26290d/nifi-registry-core/nifi-registry-framework/src/test/resources/security/authorizers-bad-authorizer-ids.xml
----------------------------------------------------------------------
diff --git a/nifi-registry-core/nifi-registry-framework/src/test/resources/security/authorizers-bad-authorizer-ids.xml b/nifi-registry-core/nifi-registry-framework/src/test/resources/security/authorizers-bad-authorizer-ids.xml
new file mode 100644
index 0000000..d31de91
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-framework/src/test/resources/security/authorizers-bad-authorizer-ids.xml
@@ -0,0 +1,46 @@
+<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
+<!--
+  ~ 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.
+  -->
+<authorizers>
+
+    <userGroupProvider>
+        <identifier>file-user-group-provider</identifier>
+        <class>org.apache.nifi.registry.security.authorization.file.FileUserGroupProvider</class>
+        <property name="Users File">./target/test-classes/security/users.xml</property>
+    </userGroupProvider>
+
+    <accessPolicyProvider>
+        <identifier>file-access-policy-provider</identifier>
+        <class>org.apache.nifi.registry.security.authorization.file.FileAccessPolicyProvider</class>
+        <property name="User Group Provider">file-user-group-provider</property>
+        <property name="Authorizations File">./target/test-classes/security/authorizations.xml</property>
+    </accessPolicyProvider>
+
+    <authorizer>
+        <identifier>managed-authorizer</identifier>
+        <class>org.apache.nifi.registry.security.authorization.StandardManagedAuthorizer</class>
+        <property name="Access Policy Provider">file-access-policy-provider</property>
+    </authorizer>
+
+    <authorizer>
+        <!-- this is incorrect: identifiers must be unique across all authorizers -->
+        <identifier>managed-authorizer</identifier>
+        <class>org.apache.nifi.registry.security.authorization.StandardManagedAuthorizer</class>
+        <property name="Access Policy Provider">file-access-policy-provider</property>
+    </authorizer>
+
+</authorizers>
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/6f26290d/nifi-registry-core/nifi-registry-framework/src/test/resources/security/authorizers-bad-composite.xml
----------------------------------------------------------------------
diff --git a/nifi-registry-core/nifi-registry-framework/src/test/resources/security/authorizers-bad-composite.xml b/nifi-registry-core/nifi-registry-framework/src/test/resources/security/authorizers-bad-composite.xml
new file mode 100644
index 0000000..1aa96f8
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-framework/src/test/resources/security/authorizers-bad-composite.xml
@@ -0,0 +1,54 @@
+<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
+<!--
+  ~ 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.
+  -->
+<authorizers>
+
+    <userGroupProvider>
+        <identifier>file-user-group-provider-1</identifier>
+        <class>org.apache.nifi.registry.security.authorization.file.FileUserGroupProvider</class>
+        <property name="Users File">./target/test-classes/security/users.xml</property>
+    </userGroupProvider>
+
+    <userGroupProvider>
+        <identifier>file-user-group-provider-2</identifier>
+        <class>org.apache.nifi.registry.security.authorization.file.FileUserGroupProvider</class>
+        <property name="Users File">./target/test-classes/security/read-only-users.xml</property>
+    </userGroupProvider>
+
+    <userGroupProvider>
+        <identifier>composite-user-group-provider</identifier>
+        <class>org.apache.nifi.registry.security.authorization.CompositeUserGroupProvider</class>
+        <property name="User Group Provider 1">file-user-group-provider-1</property>
+        <property name="User Group Provider 2">file-user-group-provider-2</property>
+        <!-- this is incorrect: the list of providers must not contain duplicates -->
+        <property name="User Group Provider 3">file-user-group-provider-2</property>
+    </userGroupProvider>
+
+    <accessPolicyProvider>
+        <identifier>file-access-policy-provider</identifier>
+        <class>org.apache.nifi.registry.security.authorization.file.FileAccessPolicyProvider</class>
+        <property name="User Group Provider">composite-user-group-provider</property>
+        <property name="Authorizations File">./target/test-classes/security/authorizations.xml</property>
+    </accessPolicyProvider>
+    
+    <authorizer>
+        <identifier>managed-authorizer</identifier>
+        <class>org.apache.nifi.registry.security.authorization.StandardManagedAuthorizer</class>
+        <property name="Access Policy Provider">file-access-policy-provider</property>
+    </authorizer>
+
+</authorizers>
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/6f26290d/nifi-registry-core/nifi-registry-framework/src/test/resources/security/authorizers-bad-configurable-composite.xml
----------------------------------------------------------------------
diff --git a/nifi-registry-core/nifi-registry-framework/src/test/resources/security/authorizers-bad-configurable-composite.xml b/nifi-registry-core/nifi-registry-framework/src/test/resources/security/authorizers-bad-configurable-composite.xml
new file mode 100644
index 0000000..f019345
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-framework/src/test/resources/security/authorizers-bad-configurable-composite.xml
@@ -0,0 +1,54 @@
+<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
+<!--
+  ~ 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.
+  -->
+<authorizers>
+
+    <userGroupProvider>
+        <identifier>file-user-group-provider-1</identifier>
+        <class>org.apache.nifi.registry.security.authorization.file.FileUserGroupProvider</class>
+        <property name="Users File">./target/test-classes/security/users.xml</property>
+    </userGroupProvider>
+
+    <userGroupProvider>
+        <identifier>file-user-group-provider-2</identifier>
+        <class>org.apache.nifi.registry.security.authorization.file.FileUserGroupProvider</class>
+        <property name="Users File">./target/test-classes/security/read-only-users.xml</property>
+    </userGroupProvider>
+
+    <userGroupProvider>
+        <identifier>composite-configurable-user-group-provider</identifier>
+        <class>org.apache.nifi.registry.security.authorization.CompositeConfigurableUserGroupProvider</class>
+        <property name="Configurable User Group Provider">file-user-group-provider-1</property>
+        <!-- this is incorrect: the configurable provider should not be listed in the list of non-configurable providers -->
+        <property name="User Group Provider 1">file-user-group-provider-1</property>
+        <property name="User Group Provider 2">file-user-group-provider-2</property>
+    </userGroupProvider>
+
+    <accessPolicyProvider>
+        <identifier>file-access-policy-provider</identifier>
+        <class>org.apache.nifi.registry.security.authorization.file.FileAccessPolicyProvider</class>
+        <property name="User Group Provider">composite-configurable-user-group-provider</property>
+        <property name="Authorizations File">./target/test-classes/security/authorizations.xml</property>
+    </accessPolicyProvider>
+
+    <authorizer>
+        <identifier>managed-authorizer</identifier>
+        <class>org.apache.nifi.registry.security.authorization.StandardManagedAuthorizer</class>
+        <property name="Access Policy Provider">file-access-policy-provider</property>
+    </authorizer>
+
+</authorizers>
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/6f26290d/nifi-registry-core/nifi-registry-framework/src/test/resources/security/authorizers-bad-ug-provider-ids.xml
----------------------------------------------------------------------
diff --git a/nifi-registry-core/nifi-registry-framework/src/test/resources/security/authorizers-bad-ug-provider-ids.xml b/nifi-registry-core/nifi-registry-framework/src/test/resources/security/authorizers-bad-ug-provider-ids.xml
new file mode 100644
index 0000000..a28a36d
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-framework/src/test/resources/security/authorizers-bad-ug-provider-ids.xml
@@ -0,0 +1,46 @@
+<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
+<!--
+  ~ 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.
+  -->
+<authorizers>
+
+    <userGroupProvider>
+        <identifier>file-user-group-provider</identifier>
+        <class>org.apache.nifi.registry.security.authorization.file.FileUserGroupProvider</class>
+        <property name="Users File">./target/test-classes/security/users1.xml</property>
+    </userGroupProvider>
+
+    <userGroupProvider>
+        <!-- this is incorrect: identifiers must be unique across all userGroupProviders -->
+        <identifier>file-user-group-provider</identifier>
+        <class>org.apache.nifi.registry.security.authorization.file.FileUserGroupProvider</class>
+        <property name="Users File">./target/test-classes/security/users2.xml</property>
+    </userGroupProvider>
+
+    <accessPolicyProvider>
+        <identifier>file-access-policy-provider</identifier>
+        <class>org.apache.nifi.registry.security.authorization.file.FileAccessPolicyProvider</class>
+        <property name="User Group Provider">file-user-group-provider</property>
+        <property name="Authorizations File">./target/test-classes/security/authorizations.xml</property>
+    </accessPolicyProvider>
+    
+    <authorizer>
+        <identifier>managed-authorizer</identifier>
+        <class>org.apache.nifi.registry.security.authorization.StandardManagedAuthorizer</class>
+        <property name="Access Policy Provider">file-access-policy-provider</property>
+    </authorizer>
+
+</authorizers>
\ No newline at end of file


[35/51] [partial] nifi-registry git commit: NIFIREG-201 Refactoring project structure to better isolate extensions

Posted by kd...@apache.org.
http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/6f26290d/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/security/authorization/StandardManagedAuthorizer.java
----------------------------------------------------------------------
diff --git a/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/security/authorization/StandardManagedAuthorizer.java b/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/security/authorization/StandardManagedAuthorizer.java
new file mode 100644
index 0000000..58bcf55
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/security/authorization/StandardManagedAuthorizer.java
@@ -0,0 +1,264 @@
+/*
+ * 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.security.authorization;
+
+import org.apache.commons.lang3.StringUtils;
+import org.apache.nifi.registry.security.authorization.exception.AuthorizationAccessException;
+import org.apache.nifi.registry.security.authorization.exception.UninheritableAuthorizationsException;
+import org.apache.nifi.registry.security.exception.SecurityProviderCreationException;
+import org.apache.nifi.registry.security.exception.SecurityProviderDestructionException;
+import org.apache.nifi.registry.util.PropertyValue;
+import org.w3c.dom.Document;
+import org.w3c.dom.Element;
+import org.w3c.dom.Node;
+import org.w3c.dom.NodeList;
+import org.xml.sax.SAXException;
+
+import javax.xml.parsers.DocumentBuilder;
+import javax.xml.parsers.DocumentBuilderFactory;
+import javax.xml.parsers.ParserConfigurationException;
+import javax.xml.stream.XMLOutputFactory;
+import javax.xml.stream.XMLStreamException;
+import javax.xml.stream.XMLStreamWriter;
+import java.io.ByteArrayInputStream;
+import java.io.IOException;
+import java.io.StringWriter;
+import java.nio.charset.StandardCharsets;
+import java.util.Set;
+
+public class StandardManagedAuthorizer implements ManagedAuthorizer {
+
+    private static final DocumentBuilderFactory DOCUMENT_BUILDER_FACTORY = DocumentBuilderFactory.newInstance();
+    private static final XMLOutputFactory XML_OUTPUT_FACTORY = XMLOutputFactory.newInstance();
+
+    private static final String USER_GROUP_PROVIDER_ELEMENT = "userGroupProvider";
+    private static final String ACCESS_POLICY_PROVIDER_ELEMENT = "accessPolicyProvider";
+
+    private AccessPolicyProviderLookup accessPolicyProviderLookup;
+    private AccessPolicyProvider accessPolicyProvider;
+    private UserGroupProvider userGroupProvider;
+
+    public StandardManagedAuthorizer() {}
+
+    // exposed for testing to inject mocks
+    public StandardManagedAuthorizer(AccessPolicyProvider accessPolicyProvider, UserGroupProvider userGroupProvider) {
+        this.accessPolicyProvider = accessPolicyProvider;
+        this.userGroupProvider = userGroupProvider;
+    }
+
+    @Override
+    public void initialize(AuthorizerInitializationContext initializationContext) throws SecurityProviderCreationException {
+        accessPolicyProviderLookup = initializationContext.getAccessPolicyProviderLookup();
+    }
+
+    @Override
+    public void onConfigured(AuthorizerConfigurationContext configurationContext) throws SecurityProviderCreationException {
+        final PropertyValue accessPolicyProviderKey = configurationContext.getProperty("Access Policy Provider");
+        if (!accessPolicyProviderKey.isSet()) {
+            throw new SecurityProviderCreationException("The Access Policy Provider must be set.");
+        }
+
+        accessPolicyProvider = accessPolicyProviderLookup.getAccessPolicyProvider(accessPolicyProviderKey.getValue());
+
+        // ensure the desired access policy provider was found
+        if (accessPolicyProvider == null) {
+            throw new SecurityProviderCreationException(String.format("Unable to locate configured Access Policy Provider: %s", accessPolicyProviderKey));
+        }
+
+        userGroupProvider = accessPolicyProvider.getUserGroupProvider();
+
+        // ensure the desired access policy provider has a user group provider
+        if (userGroupProvider == null) {
+            throw new SecurityProviderCreationException(String.format("Configured Access Policy Provider %s does not contain a User Group Provider", accessPolicyProviderKey));
+        }
+    }
+
+    @Override
+    public AuthorizationResult authorize(AuthorizationRequest request) throws AuthorizationAccessException {
+        final String resourceIdentifier = request.getResource().getIdentifier();
+        final AccessPolicy policy = accessPolicyProvider.getAccessPolicy(resourceIdentifier, request.getAction());
+        if (policy == null) {
+            return AuthorizationResult.resourceNotFound();
+        }
+
+        final UserAndGroups userAndGroups = userGroupProvider.getUserAndGroups(request.getIdentity());
+
+        final User user = userAndGroups.getUser();
+        if (user == null) {
+            return AuthorizationResult.denied(String.format("Unknown user with identity '%s'.", request.getIdentity()));
+        }
+
+        final Set<Group> userGroups = userAndGroups.getGroups();
+        if (policy.getUsers().contains(user.getIdentifier()) || containsGroup(userGroups, policy)) {
+            return AuthorizationResult.approved();
+        }
+
+        return AuthorizationResult.denied(request.getExplanationSupplier().get());
+    }
+
+    /**
+     * Determines if the policy contains one of the user's groups.
+     *
+     * @param userGroups the set of the user's groups
+     * @param policy the policy
+     * @return true if one of the Groups in userGroups is contained in the policy
+     */
+    private boolean containsGroup(final Set<Group> userGroups, final AccessPolicy policy) {
+        if (userGroups == null || userGroups.isEmpty() || policy.getGroups().isEmpty()) {
+            return false;
+        }
+
+        for (Group userGroup : userGroups) {
+            if (policy.getGroups().contains(userGroup.getIdentifier())) {
+                return true;
+            }
+        }
+
+        return false;
+    }
+
+    @Override
+    public String getFingerprint() throws AuthorizationAccessException {
+        XMLStreamWriter writer = null;
+        final StringWriter out = new StringWriter();
+        try {
+            writer = XML_OUTPUT_FACTORY.createXMLStreamWriter(out);
+            writer.writeStartDocument();
+            writer.writeStartElement("managedAuthorizations");
+
+            writer.writeStartElement(ACCESS_POLICY_PROVIDER_ELEMENT);
+            if (accessPolicyProvider instanceof ConfigurableAccessPolicyProvider) {
+                writer.writeCharacters(((ConfigurableAccessPolicyProvider) accessPolicyProvider).getFingerprint());
+            }
+            writer.writeEndElement();
+
+            writer.writeStartElement(USER_GROUP_PROVIDER_ELEMENT);
+            if (userGroupProvider instanceof ConfigurableUserGroupProvider) {
+                writer.writeCharacters(((ConfigurableUserGroupProvider) userGroupProvider).getFingerprint());
+            }
+            writer.writeEndElement();
+
+            writer.writeEndElement();
+            writer.writeEndDocument();
+            writer.flush();
+        } catch (XMLStreamException e) {
+            throw new AuthorizationAccessException("Unable to generate fingerprint", e);
+        } finally {
+            if (writer != null) {
+                try {
+                    writer.close();
+                } catch (XMLStreamException e) {
+                    // nothing to do here
+                }
+            }
+        }
+
+        return out.toString();
+    }
+
+    @Override
+    public void inheritFingerprint(String fingerprint) throws AuthorizationAccessException {
+        if (StringUtils.isBlank(fingerprint)) {
+            return;
+        }
+
+        final FingerprintHolder fingerprintHolder = parseFingerprint(fingerprint);
+
+        if (StringUtils.isNotBlank(fingerprintHolder.getPolicyFingerprint()) && accessPolicyProvider instanceof ConfigurableAccessPolicyProvider) {
+            ((ConfigurableAccessPolicyProvider) accessPolicyProvider).inheritFingerprint(fingerprintHolder.getPolicyFingerprint());
+        }
+
+        if (StringUtils.isNotBlank(fingerprintHolder.getUserGroupFingerprint()) && userGroupProvider instanceof ConfigurableUserGroupProvider) {
+            ((ConfigurableUserGroupProvider) userGroupProvider).inheritFingerprint(fingerprintHolder.getUserGroupFingerprint());
+        }
+    }
+
+    @Override
+    public void checkInheritability(String proposedFingerprint) throws AuthorizationAccessException, UninheritableAuthorizationsException {
+        final FingerprintHolder fingerprintHolder = parseFingerprint(proposedFingerprint);
+
+        if (StringUtils.isNotBlank(fingerprintHolder.getPolicyFingerprint())) {
+            if (accessPolicyProvider instanceof ConfigurableAccessPolicyProvider) {
+                ((ConfigurableAccessPolicyProvider) accessPolicyProvider).checkInheritability(fingerprintHolder.getPolicyFingerprint());
+            } else {
+                throw new UninheritableAuthorizationsException("Policy fingerprint is not blank and the configured AccessPolicyProvider does not support fingerprinting.");
+            }
+        }
+
+        if (StringUtils.isNotBlank(fingerprintHolder.getUserGroupFingerprint())) {
+            if (userGroupProvider instanceof ConfigurableUserGroupProvider) {
+                ((ConfigurableUserGroupProvider) userGroupProvider).checkInheritability(fingerprintHolder.getUserGroupFingerprint());
+            } else {
+                throw new UninheritableAuthorizationsException("User/Group fingerprint is not blank and the configured UserGroupProvider does not support fingerprinting.");
+            }
+        }
+    }
+
+    private final FingerprintHolder parseFingerprint(final String fingerprint) throws AuthorizationAccessException {
+        final byte[] fingerprintBytes = fingerprint.getBytes(StandardCharsets.UTF_8);
+
+        try (final ByteArrayInputStream in = new ByteArrayInputStream(fingerprintBytes)) {
+            final DocumentBuilder docBuilder = DOCUMENT_BUILDER_FACTORY.newDocumentBuilder();
+            final Document document = docBuilder.parse(in);
+            final Element rootElement = document.getDocumentElement();
+
+            final NodeList accessPolicyProviderList = rootElement.getElementsByTagName(ACCESS_POLICY_PROVIDER_ELEMENT);
+            if (accessPolicyProviderList.getLength() != 1) {
+                throw new AuthorizationAccessException(String.format("Only one %s element is allowed: %s", ACCESS_POLICY_PROVIDER_ELEMENT, fingerprint));
+            }
+
+            final NodeList userGroupProviderList = rootElement.getElementsByTagName(USER_GROUP_PROVIDER_ELEMENT);
+            if (userGroupProviderList.getLength() != 1) {
+                throw new AuthorizationAccessException(String.format("Only one %s element is allowed: %s", USER_GROUP_PROVIDER_ELEMENT, fingerprint));
+            }
+
+            final Node accessPolicyProvider = accessPolicyProviderList.item(0);
+            final Node userGroupProvider = userGroupProviderList.item(0);
+            return new FingerprintHolder(accessPolicyProvider.getTextContent(), userGroupProvider.getTextContent());
+        } catch (SAXException | ParserConfigurationException | IOException e) {
+            throw new AuthorizationAccessException("Unable to parse fingerprint", e);
+        }
+    }
+
+    @Override
+    public AccessPolicyProvider getAccessPolicyProvider() {
+        return accessPolicyProvider;
+    }
+
+    @Override
+    public void preDestruction() throws SecurityProviderDestructionException {
+
+    }
+
+    private static class FingerprintHolder {
+        private final String policyFingerprint;
+        private final String userGroupFingerprint;
+
+        public FingerprintHolder(String policyFingerprint, String userGroupFingerprint) {
+            this.policyFingerprint = policyFingerprint;
+            this.userGroupFingerprint = userGroupFingerprint;
+        }
+
+        public String getPolicyFingerprint() {
+            return policyFingerprint;
+        }
+
+        public String getUserGroupFingerprint() {
+            return userGroupFingerprint;
+        }
+    }
+}

http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/6f26290d/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/security/authorization/UsersAndAccessPolicies.java
----------------------------------------------------------------------
diff --git a/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/security/authorization/UsersAndAccessPolicies.java b/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/security/authorization/UsersAndAccessPolicies.java
new file mode 100644
index 0000000..7675f27
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/security/authorization/UsersAndAccessPolicies.java
@@ -0,0 +1,52 @@
+/*
+ * 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.security.authorization;
+
+import java.util.Set;
+
+/**
+ * A holder object to provide atomic access to policies for a given resource and users by
+ * identity. Implementations must ensure consistent access to the data backing this instance.
+ */
+public interface UsersAndAccessPolicies {
+
+    /**
+     * Retrieves the set of access policies for a given resource and action.
+     *
+     * @param resourceIdentifier the resource identifier to retrieve policies for
+     * @param action the action to retrieve policies for
+     * @return the access policy for the given resource and action
+     */
+    AccessPolicy getAccessPolicy(final String resourceIdentifier, final RequestAction action);
+
+    /**
+     * Retrieves a user by an identity string.
+     *
+     * @param identity the identity of the user to retrieve
+     * @return the user with the given identity
+     */
+    User getUser(final String identity);
+
+    /**
+     * Retrieves the groups for a given user identity.
+     *
+     * @param userIdentity a user identity
+     * @return the set of groups for the given user identity
+     */
+    Set<Group> getGroups(final String userIdentity);
+
+}

http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/6f26290d/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/security/authorization/file/AuthorizationsHolder.java
----------------------------------------------------------------------
diff --git a/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/security/authorization/file/AuthorizationsHolder.java b/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/security/authorization/file/AuthorizationsHolder.java
new file mode 100644
index 0000000..6e84f49
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/security/authorization/file/AuthorizationsHolder.java
@@ -0,0 +1,187 @@
+/*
+ * 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.security.authorization.file;
+
+
+import org.apache.nifi.registry.security.authorization.file.generated.Authorizations;
+import org.apache.nifi.registry.security.authorization.file.generated.Policies;
+import org.apache.nifi.registry.security.authorization.AccessPolicy;
+import org.apache.nifi.registry.security.authorization.RequestAction;
+
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Map;
+import java.util.Set;
+
+/**
+ * A holder to provide atomic access to data structures.
+ */
+public class AuthorizationsHolder {
+
+    private final Authorizations authorizations;
+
+    private final Set<AccessPolicy> allPolicies;
+    private final Map<String, Set<AccessPolicy>> policiesByResource;
+    private final Map<String, AccessPolicy> policiesById;
+
+    /**
+     * Creates a new holder and populates all convenience authorizations data structures.
+     *
+     * @param authorizations the current authorizations instance
+     */
+    public AuthorizationsHolder(final Authorizations authorizations) {
+        this.authorizations = authorizations;
+
+        // load all access policies
+        final Policies policies = authorizations.getPolicies();
+        final Set<AccessPolicy> allPolicies = Collections.unmodifiableSet(createAccessPolicies(policies));
+
+        // create a convenience map from resource id to policies
+        final Map<String, Set<AccessPolicy>> policiesByResourceMap = Collections.unmodifiableMap(createResourcePolicyMap(allPolicies));
+
+        // create a convenience map from policy id to policy
+        final Map<String, AccessPolicy> policiesByIdMap = Collections.unmodifiableMap(createPoliciesByIdMap(allPolicies));
+
+        // set all the holders
+        this.allPolicies = allPolicies;
+        this.policiesByResource = policiesByResourceMap;
+        this.policiesById = policiesByIdMap;
+    }
+
+    /**
+     * Creates AccessPolicies from the JAXB Policies.
+     *
+     * @param policies the JAXB Policies element
+     * @return a set of AccessPolicies corresponding to the provided Resources
+     */
+    private Set<AccessPolicy> createAccessPolicies(org.apache.nifi.registry.security.authorization.file.generated.Policies policies) {
+        Set<AccessPolicy> allPolicies = new HashSet<>();
+        if (policies == null || policies.getPolicy() == null) {
+            return allPolicies;
+        }
+
+        // load the new authorizations
+        for (final org.apache.nifi.registry.security.authorization.file.generated.Policy policy : policies.getPolicy()) {
+            final String policyIdentifier = policy.getIdentifier();
+            final String resourceIdentifier = policy.getResource();
+
+            // start a new builder and set the policy and resource identifiers
+            final AccessPolicy.Builder builder = new AccessPolicy.Builder()
+                    .identifier(policyIdentifier)
+                    .resource(resourceIdentifier);
+
+            // add each user identifier
+            for (org.apache.nifi.registry.security.authorization.file.generated.Policy.User user : policy.getUser()) {
+                builder.addUser(user.getIdentifier());
+            }
+
+            // add each group identifier
+            for (org.apache.nifi.registry.security.authorization.file.generated.Policy.Group group : policy.getGroup()) {
+                builder.addGroup(group.getIdentifier());
+            }
+
+            // add the appropriate request actions
+            final String authorizationCode = policy.getAction();
+            if (authorizationCode.equals(FileAccessPolicyProvider.READ_CODE)) {
+                builder.action(RequestAction.READ);
+            } else if (authorizationCode.equals(FileAccessPolicyProvider.WRITE_CODE)){
+                builder.action(RequestAction.WRITE);
+            } else if (authorizationCode.equals(FileAccessPolicyProvider.DELETE_CODE)){
+                builder.action(RequestAction.DELETE);
+            } else {
+                throw new IllegalStateException("Unknown Policy Action: " + authorizationCode);
+            }
+
+            // build the policy and add it to the map
+            allPolicies.add(builder.build());
+        }
+
+        return allPolicies;
+    }
+
+    /**
+     * Creates a map from resource identifier to the set of policies for the given resource.
+     *
+     * @param allPolicies the set of all policies
+     * @return a map from resource identifier to policies
+     */
+    private Map<String, Set<AccessPolicy>> createResourcePolicyMap(final Set<AccessPolicy> allPolicies) {
+        Map<String, Set<AccessPolicy>> resourcePolicies = new HashMap<>();
+
+        for (AccessPolicy policy : allPolicies) {
+            Set<AccessPolicy> policies = resourcePolicies.get(policy.getResource());
+            if (policies == null) {
+                policies = new HashSet<>();
+                resourcePolicies.put(policy.getResource(), policies);
+            }
+            policies.add(policy);
+        }
+
+        return resourcePolicies;
+    }
+
+    /**
+     * Creates a Map from policy identifier to AccessPolicy.
+     *
+     * @param policies the set of all access policies
+     * @return the Map from policy identifier to AccessPolicy
+     */
+    private Map<String, AccessPolicy> createPoliciesByIdMap(final Set<AccessPolicy> policies) {
+        Map<String,AccessPolicy> policyMap = new HashMap<>();
+        for (AccessPolicy policy : policies) {
+            policyMap.put(policy.getIdentifier(), policy);
+        }
+        return policyMap;
+    }
+
+    public Authorizations getAuthorizations() {
+        return authorizations;
+    }
+
+    public Set<AccessPolicy> getAllPolicies() {
+        return allPolicies;
+    }
+
+    public Map<String, Set<AccessPolicy>> getPoliciesByResource() {
+        return policiesByResource;
+    }
+
+    public Map<String, AccessPolicy> getPoliciesById() {
+        return policiesById;
+    }
+
+    public AccessPolicy getAccessPolicy(final String resourceIdentifier, final RequestAction action) {
+        if (resourceIdentifier == null) {
+            throw new IllegalArgumentException("Resource Identifier cannot be null");
+        }
+
+        final Set<AccessPolicy> resourcePolicies = policiesByResource.get(resourceIdentifier);
+        if (resourcePolicies == null) {
+            return null;
+        }
+
+        for (AccessPolicy accessPolicy : resourcePolicies) {
+            if (accessPolicy.getAction() == action) {
+                return accessPolicy;
+            }
+        }
+
+        return null;
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/6f26290d/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/security/authorization/file/FileAccessPolicyProvider.java
----------------------------------------------------------------------
diff --git a/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/security/authorization/file/FileAccessPolicyProvider.java b/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/security/authorization/file/FileAccessPolicyProvider.java
new file mode 100644
index 0000000..c3434c4
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/security/authorization/file/FileAccessPolicyProvider.java
@@ -0,0 +1,777 @@
+/*
+ * 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.security.authorization.file;
+
+import org.apache.commons.lang3.StringUtils;
+import org.apache.nifi.registry.properties.NiFiRegistryProperties;
+import org.apache.nifi.registry.properties.util.IdentityMapping;
+import org.apache.nifi.registry.properties.util.IdentityMappingUtil;
+import org.apache.nifi.registry.security.authorization.AccessPolicy;
+import org.apache.nifi.registry.security.authorization.AccessPolicyProviderInitializationContext;
+import org.apache.nifi.registry.security.authorization.AuthorizerConfigurationContext;
+import org.apache.nifi.registry.security.authorization.ConfigurableAccessPolicyProvider;
+import org.apache.nifi.registry.security.authorization.RequestAction;
+import org.apache.nifi.registry.security.authorization.User;
+import org.apache.nifi.registry.security.authorization.UserGroupProvider;
+import org.apache.nifi.registry.security.authorization.UserGroupProviderLookup;
+import org.apache.nifi.registry.security.authorization.annotation.AuthorizerContext;
+import org.apache.nifi.registry.security.authorization.exception.AuthorizationAccessException;
+import org.apache.nifi.registry.security.authorization.exception.UninheritableAuthorizationsException;
+import org.apache.nifi.registry.security.authorization.file.generated.Authorizations;
+import org.apache.nifi.registry.security.authorization.file.generated.Policies;
+import org.apache.nifi.registry.security.authorization.file.generated.Policy;
+import org.apache.nifi.registry.security.exception.SecurityProviderCreationException;
+import org.apache.nifi.registry.security.exception.SecurityProviderDestructionException;
+import org.apache.nifi.registry.util.PropertyValue;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.w3c.dom.Document;
+import org.w3c.dom.Element;
+import org.w3c.dom.Node;
+import org.w3c.dom.NodeList;
+import org.xml.sax.SAXException;
+
+import javax.xml.XMLConstants;
+import javax.xml.bind.JAXBContext;
+import javax.xml.bind.JAXBElement;
+import javax.xml.bind.JAXBException;
+import javax.xml.bind.Marshaller;
+import javax.xml.bind.Unmarshaller;
+import javax.xml.parsers.DocumentBuilder;
+import javax.xml.parsers.DocumentBuilderFactory;
+import javax.xml.parsers.ParserConfigurationException;
+import javax.xml.stream.XMLOutputFactory;
+import javax.xml.stream.XMLStreamException;
+import javax.xml.stream.XMLStreamWriter;
+import javax.xml.transform.stream.StreamSource;
+import javax.xml.validation.Schema;
+import javax.xml.validation.SchemaFactory;
+import java.io.ByteArrayInputStream;
+import java.io.File;
+import java.io.IOException;
+import java.io.StringWriter;
+import java.nio.charset.StandardCharsets;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.Date;
+import java.util.HashSet;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.concurrent.atomic.AtomicReference;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+public class FileAccessPolicyProvider implements ConfigurableAccessPolicyProvider {
+
+    private static final Logger logger = LoggerFactory.getLogger(FileAccessPolicyProvider.class);
+
+    private static final String AUTHORIZATIONS_XSD = "/authorizations.xsd";
+    private static final String JAXB_AUTHORIZATIONS_PATH = "org.apache.nifi.registry.security.authorization.file.generated";
+
+    private static final JAXBContext JAXB_AUTHORIZATIONS_CONTEXT = initializeJaxbContext(JAXB_AUTHORIZATIONS_PATH);
+
+    /**
+     * Load the JAXBContext.
+     */
+    private static JAXBContext initializeJaxbContext(final String contextPath) {
+        try {
+            return JAXBContext.newInstance(contextPath, FileAuthorizer.class.getClassLoader());
+        } catch (JAXBException e) {
+            throw new RuntimeException("Unable to create JAXBContext.");
+        }
+    }
+
+    private static final DocumentBuilderFactory DOCUMENT_BUILDER_FACTORY = DocumentBuilderFactory.newInstance();
+    private static final XMLOutputFactory XML_OUTPUT_FACTORY = XMLOutputFactory.newInstance();
+
+    private static final String POLICY_ELEMENT = "policy";
+    private static final String POLICY_USER_ELEMENT = "policyUser";
+    private static final String POLICY_GROUP_ELEMENT = "policyGroup";
+    private static final String IDENTIFIER_ATTR = "identifier";
+    private static final String RESOURCE_ATTR = "resource";
+    private static final String ACTIONS_ATTR = "actions";
+
+    /* These codes must match the enumeration values set in authorizations.xsd */
+    static final String READ_CODE = "R";
+    static final String WRITE_CODE = "W";
+    static final String DELETE_CODE = "D";
+
+    /*  TODO - move this somewhere into nifi-registry-security-framework so it can be applied to any ConfigurableAccessPolicyProvider
+     *  (and also gets us away from requiring magic strings here) */
+    private static final ResourceActionPair[] INITIAL_ADMIN_ACCESS_POLICIES = {
+            new ResourceActionPair("/tenants", READ_CODE),
+            new ResourceActionPair("/tenants", WRITE_CODE),
+            new ResourceActionPair("/tenants", DELETE_CODE),
+            new ResourceActionPair("/policies", READ_CODE),
+            new ResourceActionPair("/policies", WRITE_CODE),
+            new ResourceActionPair("/policies", DELETE_CODE),
+            new ResourceActionPair("/buckets", READ_CODE),
+            new ResourceActionPair("/buckets", WRITE_CODE),
+            new ResourceActionPair("/buckets", DELETE_CODE),
+            new ResourceActionPair("/actuator", READ_CODE),
+            new ResourceActionPair("/actuator", WRITE_CODE),
+            new ResourceActionPair("/actuator", DELETE_CODE),
+            new ResourceActionPair("/swagger", READ_CODE),
+            new ResourceActionPair("/swagger", WRITE_CODE),
+            new ResourceActionPair("/swagger", DELETE_CODE),
+            new ResourceActionPair("/proxy", WRITE_CODE)
+    };
+
+    /*  TODO - move this somewhere into nifi-registry-security-framework so it can be applied to any ConfigurableAccessPolicyProvider
+     *  (and also gets us away from requiring magic strings here) */
+    private static final ResourceActionPair[] NIFI_ACCESS_POLICIES = {
+            new ResourceActionPair("/buckets", READ_CODE),
+            new ResourceActionPair("/proxy", WRITE_CODE)
+    };
+
+    static final String PROP_NIFI_IDENTITY_PREFIX = "NiFi Identity ";
+    static final String PROP_USER_GROUP_PROVIDER = "User Group Provider";
+    static final String PROP_AUTHORIZATIONS_FILE = "Authorizations File";
+    static final String PROP_INITIAL_ADMIN_IDENTITY = "Initial Admin Identity";
+    static final Pattern NIFI_IDENTITY_PATTERN = Pattern.compile(PROP_NIFI_IDENTITY_PREFIX + "\\S+");
+
+    private Schema authorizationsSchema;
+    private NiFiRegistryProperties properties;
+    private File authorizationsFile;
+    private String initialAdminIdentity;
+    private Set<String> nifiIdentities;
+    private List<IdentityMapping> identityMappings;
+
+    private UserGroupProvider userGroupProvider;
+    private UserGroupProviderLookup userGroupProviderLookup;
+    private final AtomicReference<AuthorizationsHolder> authorizationsHolder = new AtomicReference<>();
+
+    @Override
+    public void initialize(AccessPolicyProviderInitializationContext initializationContext) throws SecurityProviderCreationException {
+        userGroupProviderLookup = initializationContext.getUserGroupProviderLookup();
+
+        try {
+            final SchemaFactory schemaFactory = SchemaFactory.newInstance(XMLConstants.W3C_XML_SCHEMA_NS_URI);
+            authorizationsSchema = schemaFactory.newSchema(FileAuthorizer.class.getResource(AUTHORIZATIONS_XSD));
+        } catch (Exception e) {
+            throw new SecurityProviderCreationException(e);
+        }
+    }
+
+    @Override
+    public void onConfigured(AuthorizerConfigurationContext configurationContext) throws SecurityProviderCreationException {
+        try {
+            final PropertyValue userGroupProviderIdentifier = configurationContext.getProperty(PROP_USER_GROUP_PROVIDER);
+            if (!userGroupProviderIdentifier.isSet()) {
+                throw new SecurityProviderCreationException("The user group provider must be specified.");
+            }
+
+            userGroupProvider = userGroupProviderLookup.getUserGroupProvider(userGroupProviderIdentifier.getValue());
+            if (userGroupProvider == null) {
+                throw new SecurityProviderCreationException("Unable to locate user group provider with identifier " + userGroupProviderIdentifier.getValue());
+            }
+
+            final PropertyValue authorizationsPath = configurationContext.getProperty(PROP_AUTHORIZATIONS_FILE);
+            if (StringUtils.isBlank(authorizationsPath.getValue())) {
+                throw new SecurityProviderCreationException("The authorizations file must be specified.");
+            }
+
+            // get the authorizations file and ensure it exists
+            authorizationsFile = new File(authorizationsPath.getValue());
+            if (!authorizationsFile.exists()) {
+                logger.info("Creating new authorizations file at {}", new Object[] {authorizationsFile.getAbsolutePath()});
+                saveAuthorizations(new Authorizations());
+            }
+
+            // extract the identity mappings from nifi-registry.properties if any are provided
+            identityMappings = Collections.unmodifiableList(IdentityMappingUtil.getIdentityMappings(properties));
+
+            // get the value of the initial admin identity
+            final PropertyValue initialAdminIdentityProp = configurationContext.getProperty(PROP_INITIAL_ADMIN_IDENTITY);
+            initialAdminIdentity = initialAdminIdentityProp.isSet() ? IdentityMappingUtil.mapIdentity(initialAdminIdentityProp.getValue(), identityMappings) : null;
+
+            // extract any nifi identities
+            nifiIdentities = new HashSet<>();
+            for (Map.Entry<String,String> entry : configurationContext.getProperties().entrySet()) {
+                Matcher matcher = NIFI_IDENTITY_PATTERN.matcher(entry.getKey());
+                if (matcher.matches() && !StringUtils.isBlank(entry.getValue())) {
+                    nifiIdentities.add(IdentityMappingUtil.mapIdentity(entry.getValue(), identityMappings));
+                }
+            }
+
+            // load the authorizations
+            load();
+
+            logger.info(String.format("Authorizations file loaded at %s", new Date().toString()));
+        } catch (SecurityProviderCreationException | JAXBException | IllegalStateException | SAXException e) {
+            throw new SecurityProviderCreationException(e);
+        }
+    }
+
+    @Override
+    public UserGroupProvider getUserGroupProvider() {
+        return userGroupProvider;
+    }
+
+    @Override
+    public Set<AccessPolicy> getAccessPolicies() throws AuthorizationAccessException {
+        return authorizationsHolder.get().getAllPolicies();
+    }
+
+    @Override
+    public synchronized AccessPolicy addAccessPolicy(AccessPolicy accessPolicy) throws AuthorizationAccessException {
+        if (accessPolicy == null) {
+            throw new IllegalArgumentException("AccessPolicy cannot be null");
+        }
+
+        // create the new JAXB Policy
+        final Policy policy = createJAXBPolicy(accessPolicy);
+
+        // add the new Policy to the top-level list of policies
+        final AuthorizationsHolder holder = authorizationsHolder.get();
+        final Authorizations authorizations = holder.getAuthorizations();
+        authorizations.getPolicies().getPolicy().add(policy);
+
+        saveAndRefreshHolder(authorizations);
+
+        return authorizationsHolder.get().getPoliciesById().get(accessPolicy.getIdentifier());
+    }
+
+    @Override
+    public AccessPolicy getAccessPolicy(String identifier) throws AuthorizationAccessException {
+        if (identifier == null) {
+            return null;
+        }
+
+        final AuthorizationsHolder holder = authorizationsHolder.get();
+        return holder.getPoliciesById().get(identifier);
+    }
+
+    @Override
+    public AccessPolicy getAccessPolicy(String resourceIdentifier, RequestAction action) throws AuthorizationAccessException {
+        return authorizationsHolder.get().getAccessPolicy(resourceIdentifier, action);
+    }
+
+    @Override
+    public synchronized AccessPolicy updateAccessPolicy(AccessPolicy accessPolicy) throws AuthorizationAccessException {
+        if (accessPolicy == null) {
+            throw new IllegalArgumentException("AccessPolicy cannot be null");
+        }
+
+        final AuthorizationsHolder holder = this.authorizationsHolder.get();
+        final Authorizations authorizations = holder.getAuthorizations();
+
+        // try to find an existing Authorization that matches the policy id
+        Policy updatePolicy = null;
+        for (Policy policy : authorizations.getPolicies().getPolicy()) {
+            if (policy.getIdentifier().equals(accessPolicy.getIdentifier())) {
+                updatePolicy = policy;
+                break;
+            }
+        }
+
+        // no matching Policy so return null
+        if (updatePolicy == null) {
+            return null;
+        }
+
+        // update the Policy, save, reload, and return
+        transferUsersAndGroups(accessPolicy, updatePolicy);
+        saveAndRefreshHolder(authorizations);
+
+        return this.authorizationsHolder.get().getPoliciesById().get(accessPolicy.getIdentifier());
+    }
+
+    @Override
+    public synchronized AccessPolicy deleteAccessPolicy(AccessPolicy accessPolicy) throws AuthorizationAccessException {
+        if (accessPolicy == null) {
+            throw new IllegalArgumentException("AccessPolicy cannot be null");
+        }
+
+        return deleteAccessPolicy(accessPolicy.getIdentifier());
+    }
+
+    @Override
+    public synchronized AccessPolicy deleteAccessPolicy(String accessPolicyIdentifer) throws AuthorizationAccessException {
+        if (accessPolicyIdentifer == null) {
+            throw new IllegalArgumentException("Access policy identifier cannot be null");
+        }
+
+        final AuthorizationsHolder holder = this.authorizationsHolder.get();
+        AccessPolicy deletedPolicy = holder.getPoliciesById().get(accessPolicyIdentifer);
+        if (deletedPolicy == null) {
+            return null;
+        }
+
+        // find the matching Policy and remove it
+        final Authorizations authorizations = holder.getAuthorizations();
+        Iterator<Policy> policyIter = authorizations.getPolicies().getPolicy().iterator();
+        while (policyIter.hasNext()) {
+            final Policy policy = policyIter.next();
+            if (policy.getIdentifier().equals(accessPolicyIdentifer)) {
+                policyIter.remove();
+                break;
+            }
+        }
+
+        saveAndRefreshHolder(authorizations);
+        return deletedPolicy;
+    }
+
+    AuthorizationsHolder getAuthorizationsHolder() {
+        return authorizationsHolder.get();
+    }
+
+    @AuthorizerContext
+    public void setNiFiProperties(NiFiRegistryProperties properties) {
+        this.properties = properties;
+    }
+
+    @Override
+    public synchronized void inheritFingerprint(String fingerprint) throws AuthorizationAccessException {
+        parsePolicies(fingerprint).forEach(policy -> addAccessPolicy(policy));
+    }
+
+    @Override
+    public void checkInheritability(String proposedFingerprint) throws AuthorizationAccessException, UninheritableAuthorizationsException {
+        try {
+            // ensure we can understand the proposed fingerprint
+            parsePolicies(proposedFingerprint);
+        } catch (final AuthorizationAccessException e) {
+            throw new UninheritableAuthorizationsException("Unable to parse the proposed fingerprint: " + e);
+        }
+
+        // ensure we are in a proper state to inherit the fingerprint
+        if (!getAccessPolicies().isEmpty()) {
+            throw new UninheritableAuthorizationsException("Proposed fingerprint is not inheritable because the current access policies is not empty.");
+        }
+    }
+
+    @Override
+    public String getFingerprint() throws AuthorizationAccessException {
+        final List<AccessPolicy> policies = new ArrayList<>(getAccessPolicies());
+        Collections.sort(policies, Comparator.comparing(AccessPolicy::getIdentifier));
+
+        XMLStreamWriter writer = null;
+        final StringWriter out = new StringWriter();
+        try {
+            writer = XML_OUTPUT_FACTORY.createXMLStreamWriter(out);
+            writer.writeStartDocument();
+            writer.writeStartElement("accessPolicies");
+
+            for (AccessPolicy policy : policies) {
+                writePolicy(writer, policy);
+            }
+
+            writer.writeEndElement();
+            writer.writeEndDocument();
+            writer.flush();
+        } catch (XMLStreamException e) {
+            throw new AuthorizationAccessException("Unable to generate fingerprint", e);
+        } finally {
+            if (writer != null) {
+                try {
+                    writer.close();
+                } catch (XMLStreamException e) {
+                    // nothing to do here
+                }
+            }
+        }
+
+        return out.toString();
+    }
+
+    private List<AccessPolicy> parsePolicies(final String fingerprint) {
+        final List<AccessPolicy> policies = new ArrayList<>();
+
+        final byte[] fingerprintBytes = fingerprint.getBytes(StandardCharsets.UTF_8);
+        try (final ByteArrayInputStream in = new ByteArrayInputStream(fingerprintBytes)) {
+            final DocumentBuilder docBuilder = DOCUMENT_BUILDER_FACTORY.newDocumentBuilder();
+            final Document document = docBuilder.parse(in);
+            final Element rootElement = document.getDocumentElement();
+
+            // parse all the policies and add them to the current access policy provider
+            NodeList policyNodes = rootElement.getElementsByTagName(POLICY_ELEMENT);
+            for (int i = 0; i < policyNodes.getLength(); i++) {
+                Node policyNode = policyNodes.item(i);
+                policies.add(parsePolicy((Element) policyNode));
+            }
+        } catch (SAXException | ParserConfigurationException | IOException e) {
+            throw new AuthorizationAccessException("Unable to parse fingerprint", e);
+        }
+
+        return policies;
+    }
+
+    private AccessPolicy parsePolicy(final Element element) {
+        final AccessPolicy.Builder builder = new AccessPolicy.Builder()
+                .identifier(element.getAttribute(IDENTIFIER_ATTR))
+                .resource(element.getAttribute(RESOURCE_ATTR));
+
+        final String actions = element.getAttribute(ACTIONS_ATTR);
+        if (actions.equals(RequestAction.READ.name())) {
+            builder.action(RequestAction.READ);
+        } else if (actions.equals(RequestAction.WRITE.name())) {
+            builder.action(RequestAction.WRITE);
+        } else if (actions.equals(RequestAction.DELETE.name())) {
+            builder.action(RequestAction.DELETE);
+        } else {
+            throw new IllegalStateException("Unknown Policy Action: " + actions);
+        }
+
+        NodeList policyUsers = element.getElementsByTagName(POLICY_USER_ELEMENT);
+        for (int i=0; i < policyUsers.getLength(); i++) {
+            Element policyUserNode = (Element) policyUsers.item(i);
+            builder.addUser(policyUserNode.getAttribute(IDENTIFIER_ATTR));
+        }
+
+        NodeList policyGroups = element.getElementsByTagName(POLICY_GROUP_ELEMENT);
+        for (int i=0; i < policyGroups.getLength(); i++) {
+            Element policyGroupNode = (Element) policyGroups.item(i);
+            builder.addGroup(policyGroupNode.getAttribute(IDENTIFIER_ATTR));
+        }
+
+        return builder.build();
+    }
+
+    private void writePolicy(final XMLStreamWriter writer, final AccessPolicy policy) throws XMLStreamException {
+        // sort the users for the policy
+        List<String> policyUsers = new ArrayList<>(policy.getUsers());
+        Collections.sort(policyUsers);
+
+        // sort the groups for this policy
+        List<String> policyGroups = new ArrayList<>(policy.getGroups());
+        Collections.sort(policyGroups);
+
+        writer.writeStartElement(POLICY_ELEMENT);
+        writer.writeAttribute(IDENTIFIER_ATTR, policy.getIdentifier());
+        writer.writeAttribute(RESOURCE_ATTR, policy.getResource());
+        writer.writeAttribute(ACTIONS_ATTR, policy.getAction().name());
+
+        for (String policyUser : policyUsers) {
+            writer.writeStartElement(POLICY_USER_ELEMENT);
+            writer.writeAttribute(IDENTIFIER_ATTR, policyUser);
+            writer.writeEndElement();
+        }
+
+        for (String policyGroup : policyGroups) {
+            writer.writeStartElement(POLICY_GROUP_ELEMENT);
+            writer.writeAttribute(IDENTIFIER_ATTR, policyGroup);
+            writer.writeEndElement();
+        }
+
+        writer.writeEndElement();
+    }
+
+    /**
+     * Loads the authorizations file and populates the AuthorizationsHolder, only called during start-up.
+     *
+     * @throws JAXBException            Unable to reload the authorized users file
+     */
+    private synchronized void load() throws JAXBException, SAXException {
+        // attempt to unmarshal
+        final Authorizations authorizations = unmarshallAuthorizations();
+        if (authorizations.getPolicies() == null) {
+            authorizations.setPolicies(new Policies());
+        }
+
+        final AuthorizationsHolder authorizationsHolder = new AuthorizationsHolder(authorizations);
+        final boolean emptyAuthorizations = authorizationsHolder.getAllPolicies().isEmpty();
+        final boolean hasInitialAdminIdentity = (initialAdminIdentity != null && !StringUtils.isBlank(initialAdminIdentity));
+        final boolean hasNiFiIdentities = (nifiIdentities != null && !nifiIdentities.isEmpty());
+
+        // if we are starting fresh then we might need to populate an initial admin
+        if (emptyAuthorizations) {
+            if (hasInitialAdminIdentity) {
+               logger.info("Populating authorizations for Initial Admin: " + initialAdminIdentity);
+               populateInitialAdmin(authorizations);
+            }
+
+            if (hasNiFiIdentities) {
+                logger.info("Populating proxy authorizations for NiFi clients: [{}]", StringUtils.join(nifiIdentities, ";"));
+                populateNiFiIdentities(authorizations);
+            }
+
+            saveAndRefreshHolder(authorizations);
+        } else {
+            this.authorizationsHolder.set(authorizationsHolder);
+        }
+    }
+
+    private void saveAuthorizations(final Authorizations authorizations) throws JAXBException {
+        final Marshaller marshaller = JAXB_AUTHORIZATIONS_CONTEXT.createMarshaller();
+        marshaller.setSchema(authorizationsSchema);
+        marshaller.setProperty(Marshaller.JAXB_FORMATTED_OUTPUT, true);
+        marshaller.marshal(authorizations, authorizationsFile);
+    }
+
+    private Authorizations unmarshallAuthorizations() throws JAXBException {
+        final Unmarshaller unmarshaller = JAXB_AUTHORIZATIONS_CONTEXT.createUnmarshaller();
+        unmarshaller.setSchema(authorizationsSchema);
+
+        final JAXBElement<Authorizations> element = unmarshaller.unmarshal(new StreamSource(authorizationsFile), Authorizations.class);
+        return element.getValue();
+    }
+
+    /**
+     *  Creates the initial admin user and sets policies managing buckets, users, and policies.
+     *
+     *  TODO - move this somewhere into nifi-registry-security-framework so it can be applied to any ConfigurableAccessPolicyProvider
+     */
+    private void populateInitialAdmin(final Authorizations authorizations) {
+        final User initialAdmin = userGroupProvider.getUserByIdentity(initialAdminIdentity);
+        if (initialAdmin == null) {
+            throw new SecurityProviderCreationException("Unable to locate initial admin " + initialAdminIdentity + " to seed policies");
+        }
+
+        for (ResourceActionPair resourceAction : INITIAL_ADMIN_ACCESS_POLICIES) {
+            addUserToAccessPolicy(authorizations, resourceAction.resource, initialAdmin.getIdentifier(), resourceAction.actionCode);
+        }
+    }
+
+    /**
+     * Creates a user for each NiFi client and gives each one write permission to /proxy.
+     *
+     * @param authorizations the overall authorizations
+     */
+    private void populateNiFiIdentities(Authorizations authorizations) {
+        for (String nifiIdentity : nifiIdentities) {
+            final User nifiUser = userGroupProvider.getUserByIdentity(nifiIdentity);
+            if (nifiUser == null) {
+                throw new SecurityProviderCreationException("Unable to locate node " + nifiIdentity + " to seed policies.");
+            }
+
+            // grant access to the resources needed for initial nifi-proxy identities
+            for (ResourceActionPair resourceAction : NIFI_ACCESS_POLICIES) {
+                addUserToAccessPolicy(authorizations, resourceAction.resource, nifiUser.getIdentifier(), resourceAction.actionCode);
+            }
+        }
+    }
+
+
+    /**
+     * Creates and adds an access policy for the given resource, identity, and actions to the specified authorizations.
+     *
+     * @param authorizations the Authorizations instance to add the policy to
+     * @param resource the resource for the policy
+     * @param userIdentifier the identifier for the user to add to the policy
+     * @param action the action for the policy
+     */
+    private void addUserToAccessPolicy(final Authorizations authorizations, final String resource, final String userIdentifier, final String action) {
+        // first try to find an existing policy for the given resource and action
+        Policy foundPolicy = null;
+        for (Policy policy : authorizations.getPolicies().getPolicy()) {
+            if (policy.getResource().equals(resource) && policy.getAction().equals(action)) {
+                foundPolicy = policy;
+                break;
+            }
+        }
+
+        if (foundPolicy == null) {
+            // if we didn't find an existing policy create a new one
+            final String uuidSeed = resource + action;
+
+            final AccessPolicy.Builder builder = new AccessPolicy.Builder()
+                    .identifierGenerateFromSeed(uuidSeed)
+                    .resource(resource)
+                    .addUser(userIdentifier);
+
+            if (action.equals(READ_CODE)) {
+                builder.action(RequestAction.READ);
+            } else if (action.equals(WRITE_CODE)) {
+                builder.action(RequestAction.WRITE);
+            } else if (action.equals(DELETE_CODE)) {
+                builder.action(RequestAction.DELETE);
+            } else {
+                throw new IllegalStateException("Unknown Policy Action: " + action);
+            }
+
+            final AccessPolicy accessPolicy = builder.build();
+            final Policy jaxbPolicy = createJAXBPolicy(accessPolicy);
+            authorizations.getPolicies().getPolicy().add(jaxbPolicy);
+        } else {
+            // otherwise add the user to the existing policy
+            Policy.User policyUser = new Policy.User();
+            policyUser.setIdentifier(userIdentifier);
+            foundPolicy.getUser().add(policyUser);
+        }
+    }
+
+    private Policy createJAXBPolicy(final AccessPolicy accessPolicy) {
+        final Policy policy = new Policy();
+        policy.setIdentifier(accessPolicy.getIdentifier());
+        policy.setResource(accessPolicy.getResource());
+
+        switch (accessPolicy.getAction()) {
+            case READ:
+                policy.setAction(READ_CODE);
+                break;
+            case WRITE:
+                policy.setAction(WRITE_CODE);
+                break;
+            case DELETE:
+                policy.setAction(DELETE_CODE);
+                break;
+            default:
+                break;
+        }
+
+        transferUsersAndGroups(accessPolicy, policy);
+        return policy;
+    }
+
+    /**
+     * Sets the given Policy to the state of the provided AccessPolicy. Users and Groups will be cleared and
+     * set to match the AccessPolicy, the resource and action will be set to match the AccessPolicy.
+     *
+     * Does not set the identifier.
+     *
+     * @param accessPolicy the AccessPolicy to transfer state from
+     * @param policy the Policy to transfer state to
+     */
+    private void transferUsersAndGroups(AccessPolicy accessPolicy, Policy policy) {
+        // add users to the policy
+        policy.getUser().clear();
+        for (String userIdentifier : accessPolicy.getUsers()) {
+            Policy.User policyUser = new Policy.User();
+            policyUser.setIdentifier(userIdentifier);
+            policy.getUser().add(policyUser);
+        }
+
+        // add groups to the policy
+        policy.getGroup().clear();
+        for (String groupIdentifier : accessPolicy.getGroups()) {
+            Policy.Group policyGroup = new Policy.Group();
+            policyGroup.setIdentifier(groupIdentifier);
+            policy.getGroup().add(policyGroup);
+        }
+    }
+
+    /**
+     * Adds the given user identifier to the policy if it doesn't already exist.
+     *
+     * @param userIdentifier a user identifier
+     * @param policy a policy to add the user to
+     */
+    private void addUserToPolicy(final String userIdentifier, final Policy policy) {
+        // determine if the user already exists in the policy
+        boolean userExists = false;
+        for (Policy.User policyUser : policy.getUser()) {
+            if (policyUser.getIdentifier().equals(userIdentifier)) {
+                userExists = true;
+                break;
+            }
+        }
+
+        // add the user to the policy if doesn't already exist
+        if (!userExists) {
+            Policy.User policyUser = new Policy.User();
+            policyUser.setIdentifier(userIdentifier);
+            policy.getUser().add(policyUser);
+        }
+    }
+
+    /**
+     * Adds the given group identifier to the policy if it doesn't already exist.
+     *
+     * @param groupIdentifier a group identifier
+     * @param policy a policy to add the user to
+     */
+    private void addGroupToPolicy(final String groupIdentifier, final Policy policy) {
+        // determine if the group already exists in the policy
+        boolean groupExists = false;
+        for (Policy.Group policyGroup : policy.getGroup()) {
+            if (policyGroup.getIdentifier().equals(groupIdentifier)) {
+                groupExists = true;
+                break;
+            }
+        }
+
+        // add the group to the policy if doesn't already exist
+        if (!groupExists) {
+            Policy.Group policyGroup = new Policy.Group();
+            policyGroup.setIdentifier(groupIdentifier);
+            policy.getGroup().add(policyGroup);
+        }
+    }
+
+    /**
+     * Finds the Policy matching the resource and action, or creates a new one and adds it to the list of policies.
+     *
+     * @param policies the policies to search through
+     * @param seedIdentity the seedIdentity to use when creating identifiers for new policies
+     * @param resource the resource for the policy
+     * @param action the action string for the police (R or RW)
+     * @return the matching policy or a new policy
+     */
+    private Policy getOrCreatePolicy(final List<Policy> policies, final String seedIdentity, final String resource, final String action) {
+        Policy foundPolicy = null;
+
+        // try to find a policy with the same resource and actions
+        for (Policy policy : policies) {
+            if (policy.getResource().equals(resource) && policy.getAction().equals(action)) {
+                foundPolicy = policy;
+                break;
+            }
+        }
+
+        // if a matching policy wasn't found then create one
+        if (foundPolicy == null) {
+            final String uuidSeed = resource + action + seedIdentity;
+            final String policyIdentifier = IdentifierUtil.getIdentifier(uuidSeed);
+
+            foundPolicy = new Policy();
+            foundPolicy.setIdentifier(policyIdentifier);
+            foundPolicy.setResource(resource);
+            foundPolicy.setAction(action);
+
+            policies.add(foundPolicy);
+        }
+
+        return foundPolicy;
+    }
+
+    /**
+     * Saves the Authorizations instance by marshalling to a file, then re-populates the
+     * in-memory data structures and sets the new holder.
+     *
+     * Synchronized to ensure only one thread writes the file at a time.
+     *
+     * @param authorizations the authorizations to save and populate from
+     * @throws AuthorizationAccessException if an error occurs saving the authorizations
+     */
+    private synchronized void saveAndRefreshHolder(final Authorizations authorizations) throws AuthorizationAccessException {
+        try {
+            saveAuthorizations(authorizations);
+
+            this.authorizationsHolder.set(new AuthorizationsHolder(authorizations));
+        } catch (JAXBException e) {
+            throw new AuthorizationAccessException("Unable to save Authorizations", e);
+        }
+    }
+
+    @Override
+    public void preDestruction() throws SecurityProviderDestructionException {
+    }
+
+    private static class ResourceActionPair {
+        public String resource;
+        public String actionCode;
+        public ResourceActionPair(String resource, String actionCode) {
+            this.resource = resource;
+            this.actionCode = actionCode;
+        }
+    }
+}

http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/6f26290d/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/security/authorization/file/FileAuthorizer.java
----------------------------------------------------------------------
diff --git a/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/security/authorization/file/FileAuthorizer.java b/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/security/authorization/file/FileAuthorizer.java
new file mode 100644
index 0000000..ad59eb6
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/security/authorization/file/FileAuthorizer.java
@@ -0,0 +1,288 @@
+/*
+ * 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.security.authorization.file;
+
+import org.apache.nifi.registry.properties.NiFiRegistryProperties;
+import org.apache.nifi.registry.security.authorization.AbstractPolicyBasedAuthorizer;
+import org.apache.nifi.registry.security.authorization.AccessPolicy;
+import org.apache.nifi.registry.security.authorization.AccessPolicyProviderInitializationContext;
+import org.apache.nifi.registry.security.authorization.AccessPolicyProviderLookup;
+import org.apache.nifi.registry.security.authorization.AuthorizerConfigurationContext;
+import org.apache.nifi.registry.security.authorization.AuthorizerInitializationContext;
+import org.apache.nifi.registry.security.authorization.Group;
+import org.apache.nifi.registry.security.authorization.RequestAction;
+import org.apache.nifi.registry.security.authorization.StandardAuthorizerConfigurationContext;
+import org.apache.nifi.registry.security.authorization.User;
+import org.apache.nifi.registry.security.authorization.UserGroupProviderInitializationContext;
+import org.apache.nifi.registry.security.authorization.UserGroupProviderLookup;
+import org.apache.nifi.registry.security.authorization.UsersAndAccessPolicies;
+import org.apache.nifi.registry.security.authorization.annotation.AuthorizerContext;
+import org.apache.nifi.registry.security.authorization.exception.AuthorizationAccessException;
+import org.apache.nifi.registry.security.exception.SecurityProviderCreationException;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Set;
+import java.util.regex.Matcher;
+
+/**
+ * Provides authorizes requests to resources using policies persisted in a file.
+ */
+public class FileAuthorizer extends AbstractPolicyBasedAuthorizer {
+
+    private static final Logger logger = LoggerFactory.getLogger(FileAuthorizer.class);
+
+    private static final String FILE_USER_GROUP_PROVIDER_ID = "file-user-group-provider";
+    private static final String FILE_ACCESS_POLICY_PROVIDER_ID = "file-access-policy-provider";
+
+    static final String PROP_LEGACY_AUTHORIZED_USERS_FILE = "Legacy Authorized Users File";
+
+    private FileUserGroupProvider userGroupProvider = new FileUserGroupProvider();
+    private FileAccessPolicyProvider accessPolicyProvider = new FileAccessPolicyProvider();
+
+    @Override
+    public void initialize(final AuthorizerInitializationContext initializationContext) throws SecurityProviderCreationException {
+        // initialize the user group provider
+        userGroupProvider.initialize(new UserGroupProviderInitializationContext() {
+            @Override
+            public String getIdentifier() {
+                return FILE_USER_GROUP_PROVIDER_ID;
+            }
+
+            @Override
+            public UserGroupProviderLookup getUserGroupProviderLookup() {
+                return (identifier) -> null;
+            }
+        });
+
+        // initialize the access policy provider
+        accessPolicyProvider.initialize(new AccessPolicyProviderInitializationContext() {
+            @Override
+            public String getIdentifier() {
+                return FILE_ACCESS_POLICY_PROVIDER_ID;
+            }
+
+            @Override
+            public UserGroupProviderLookup getUserGroupProviderLookup() {
+                return (identifier) -> {
+                    if (FILE_USER_GROUP_PROVIDER_ID.equals(identifier)) {
+                        return userGroupProvider;
+                    }
+
+                    return null;
+                };
+            }
+
+            @Override
+            public AccessPolicyProviderLookup getAccessPolicyProviderLookup() {
+                return (identifier) ->  null;
+            }
+        });
+    }
+
+    @Override
+    public void doOnConfigured(final AuthorizerConfigurationContext configurationContext) throws SecurityProviderCreationException {
+        final Map<String, String> configurationProperties = configurationContext.getProperties();
+
+        // relay the relevant config
+        final Map<String, String> userGroupProperties = new HashMap<>();
+        if (configurationProperties.containsKey(FileUserGroupProvider.PROP_TENANTS_FILE)) {
+            userGroupProperties.put(FileUserGroupProvider.PROP_TENANTS_FILE, configurationProperties.get(FileUserGroupProvider.PROP_TENANTS_FILE));
+        }
+        if (configurationProperties.containsKey(FileAuthorizer.PROP_LEGACY_AUTHORIZED_USERS_FILE)) {
+            userGroupProperties.put(FileAuthorizer.PROP_LEGACY_AUTHORIZED_USERS_FILE, configurationProperties.get(FileAuthorizer.PROP_LEGACY_AUTHORIZED_USERS_FILE));
+        }
+
+        // relay the relevant config
+        final Map<String, String> accessPolicyProperties = new HashMap<>();
+        accessPolicyProperties.put(FileAccessPolicyProvider.PROP_USER_GROUP_PROVIDER, FILE_USER_GROUP_PROVIDER_ID);
+        if (configurationProperties.containsKey(FileAccessPolicyProvider.PROP_AUTHORIZATIONS_FILE)) {
+            accessPolicyProperties.put(FileAccessPolicyProvider.PROP_AUTHORIZATIONS_FILE, configurationProperties.get(FileAccessPolicyProvider.PROP_AUTHORIZATIONS_FILE));
+        }
+        if (configurationProperties.containsKey(FileAccessPolicyProvider.PROP_INITIAL_ADMIN_IDENTITY)) {
+            accessPolicyProperties.put(FileAccessPolicyProvider.PROP_INITIAL_ADMIN_IDENTITY, configurationProperties.get(FileAccessPolicyProvider.PROP_INITIAL_ADMIN_IDENTITY));
+        }
+        if (configurationProperties.containsKey(FileAuthorizer.PROP_LEGACY_AUTHORIZED_USERS_FILE)) {
+            accessPolicyProperties.put(FileAuthorizer.PROP_LEGACY_AUTHORIZED_USERS_FILE, configurationProperties.get(FileAuthorizer.PROP_LEGACY_AUTHORIZED_USERS_FILE));
+        }
+
+        // ensure all nifi identities are seeded into the user provider
+        configurationProperties.forEach((property, value) -> {
+            final Matcher matcher = FileAccessPolicyProvider.NIFI_IDENTITY_PATTERN.matcher(property);
+            if (matcher.matches()) {
+                accessPolicyProperties.put(property, value);
+                userGroupProperties.put(property.replace(FileAccessPolicyProvider.PROP_NIFI_IDENTITY_PREFIX, FileUserGroupProvider.PROP_INITIAL_USER_IDENTITY_PREFIX), value);
+            }
+        });
+
+        // ensure the initial admin is seeded into the user provider if appropriate
+        if (configurationProperties.containsKey(FileAccessPolicyProvider.PROP_INITIAL_ADMIN_IDENTITY)) {
+            int i = 0;
+            while (true) {
+                final String key = FileUserGroupProvider.PROP_INITIAL_USER_IDENTITY_PREFIX + i++;
+                if (!userGroupProperties.containsKey(key)) {
+                    userGroupProperties.put(key, configurationProperties.get(FileAccessPolicyProvider.PROP_INITIAL_ADMIN_IDENTITY));
+                    break;
+                }
+            }
+        }
+
+        // configure the user group provider
+        userGroupProvider.onConfigured(new StandardAuthorizerConfigurationContext(FILE_USER_GROUP_PROVIDER_ID, userGroupProperties));
+
+        // configure the access policy provider
+        accessPolicyProvider.onConfigured(new StandardAuthorizerConfigurationContext(FILE_USER_GROUP_PROVIDER_ID, accessPolicyProperties));
+    }
+
+    @Override
+    public void preDestruction() {
+
+    }
+
+    // ------------------ Groups ------------------
+
+    @Override
+    public synchronized Group doAddGroup(Group group) throws AuthorizationAccessException {
+        return userGroupProvider.addGroup(group);
+    }
+
+    @Override
+    public Group getGroup(String identifier) throws AuthorizationAccessException {
+        return userGroupProvider.getGroup(identifier);
+    }
+
+    @Override
+    public synchronized Group doUpdateGroup(Group group) throws AuthorizationAccessException {
+        return userGroupProvider.updateGroup(group);
+    }
+
+    @Override
+    public synchronized Group deleteGroup(Group group) throws AuthorizationAccessException {
+        return userGroupProvider.deleteGroup(group);
+    }
+
+    @Override
+    public synchronized Group deleteGroup(String groupId) throws AuthorizationAccessException {
+        return userGroupProvider.deleteGroup(groupId);
+    }
+
+    @Override
+    public Set<Group> getGroups() throws AuthorizationAccessException {
+        return userGroupProvider.getGroups();
+    }
+
+    // ------------------ Users ------------------
+
+    @Override
+    public synchronized User doAddUser(final User user) throws AuthorizationAccessException {
+        return userGroupProvider.addUser(user);
+    }
+
+    @Override
+    public User getUser(final String identifier) throws AuthorizationAccessException {
+        return userGroupProvider.getUser(identifier);
+    }
+
+    @Override
+    public User getUserByIdentity(final String identity) throws AuthorizationAccessException {
+        return userGroupProvider.getUserByIdentity(identity);
+    }
+
+    @Override
+    public synchronized User doUpdateUser(final User user) throws AuthorizationAccessException {
+        return userGroupProvider.updateUser(user);
+    }
+
+    @Override
+    public synchronized User deleteUser(final User user) throws AuthorizationAccessException {
+        return userGroupProvider.deleteUser(user);
+    }
+
+    @Override
+    public synchronized User deleteUser(final String userId) throws AuthorizationAccessException {
+        return userGroupProvider.deleteUser(userId);
+    }
+
+    @Override
+    public Set<User> getUsers() throws AuthorizationAccessException {
+        return userGroupProvider.getUsers();
+    }
+
+    // ------------------ AccessPolicies ------------------
+
+    @Override
+    public synchronized AccessPolicy doAddAccessPolicy(final AccessPolicy accessPolicy) throws AuthorizationAccessException {
+        return accessPolicyProvider.addAccessPolicy(accessPolicy);
+    }
+
+    @Override
+    public AccessPolicy getAccessPolicy(final String identifier) throws AuthorizationAccessException {
+        return accessPolicyProvider.getAccessPolicy(identifier);
+    }
+
+    @Override
+    public synchronized AccessPolicy updateAccessPolicy(final AccessPolicy accessPolicy) throws AuthorizationAccessException {
+        return accessPolicyProvider.updateAccessPolicy(accessPolicy);
+    }
+
+    @Override
+    public synchronized AccessPolicy deleteAccessPolicy(final AccessPolicy accessPolicy) throws AuthorizationAccessException {
+        return accessPolicyProvider.deleteAccessPolicy(accessPolicy);
+    }
+
+    @Override
+    public synchronized AccessPolicy deleteAccessPolicy(final String accessPolicyIdentifier) throws AuthorizationAccessException {
+        return accessPolicyProvider.deleteAccessPolicy(accessPolicyIdentifier);
+    }
+
+    @Override
+    public Set<AccessPolicy> getAccessPolicies() throws AuthorizationAccessException {
+        return accessPolicyProvider.getAccessPolicies();
+    }
+
+    @AuthorizerContext
+    public void setNiFiProperties(NiFiRegistryProperties properties) {
+        userGroupProvider.setNiFiProperties(properties);
+        accessPolicyProvider.setNiFiProperties(properties);
+    }
+
+    @Override
+    public synchronized UsersAndAccessPolicies getUsersAndAccessPolicies() throws AuthorizationAccessException {
+        final AuthorizationsHolder authorizationsHolder = accessPolicyProvider.getAuthorizationsHolder();
+        final UserGroupHolder userGroupHolder = userGroupProvider.getUserGroupHolder();
+
+        return new UsersAndAccessPolicies() {
+            @Override
+            public AccessPolicy getAccessPolicy(String resourceIdentifier, RequestAction action) {
+                return authorizationsHolder.getAccessPolicy(resourceIdentifier, action);
+            }
+
+            @Override
+            public User getUser(String identity) {
+                return userGroupHolder.getUser(identity);
+            }
+
+            @Override
+            public Set<Group> getGroups(String userIdentity) {
+                return userGroupHolder.getGroups(userIdentity);
+            }
+        };
+    }
+
+}


[38/51] [partial] nifi-registry git commit: NIFIREG-201 Refactoring project structure to better isolate extensions

Posted by kd...@apache.org.
http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/6f26290d/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/provider/flow/git/GitFlowMetaData.java
----------------------------------------------------------------------
diff --git a/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/provider/flow/git/GitFlowMetaData.java b/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/provider/flow/git/GitFlowMetaData.java
new file mode 100644
index 0000000..4faf007
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/provider/flow/git/GitFlowMetaData.java
@@ -0,0 +1,426 @@
+/*
+ * 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.provider.flow.git;
+
+import org.apache.commons.lang3.concurrent.BasicThreadFactory;
+import org.eclipse.jgit.api.Git;
+import org.eclipse.jgit.api.PushCommand;
+import org.eclipse.jgit.api.Status;
+import org.eclipse.jgit.api.errors.GitAPIException;
+import org.eclipse.jgit.api.errors.NoHeadException;
+import org.eclipse.jgit.lib.ObjectId;
+import org.eclipse.jgit.lib.Repository;
+import org.eclipse.jgit.revwalk.RevCommit;
+import org.eclipse.jgit.revwalk.RevTree;
+import org.eclipse.jgit.storage.file.FileRepositoryBuilder;
+import org.eclipse.jgit.transport.CredentialsProvider;
+import org.eclipse.jgit.transport.PushResult;
+import org.eclipse.jgit.transport.RemoteConfig;
+import org.eclipse.jgit.transport.UsernamePasswordCredentialsProvider;
+import org.eclipse.jgit.treewalk.TreeWalk;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.yaml.snakeyaml.Yaml;
+
+import java.io.File;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStreamWriter;
+import java.io.Writer;
+import java.nio.charset.StandardCharsets;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+import java.util.concurrent.ArrayBlockingQueue;
+import java.util.concurrent.BlockingQueue;
+import java.util.concurrent.Executors;
+import java.util.concurrent.ScheduledExecutorService;
+import java.util.concurrent.ThreadFactory;
+import java.util.concurrent.TimeUnit;
+import java.util.stream.Collectors;
+
+import static java.lang.String.format;
+import static org.apache.commons.lang3.StringUtils.isEmpty;
+
+class GitFlowMetaData {
+
+    static final int CURRENT_LAYOUT_VERSION = 1;
+
+    static final String LAYOUT_VERSION = "layoutVer";
+    static final String BUCKET_ID = "bucketId";
+    static final String FLOWS = "flows";
+    static final String VER = "ver";
+    static final String FILE = "file";
+    static final String BUCKET_FILENAME = "bucket.yml";
+
+    private static final Logger logger = LoggerFactory.getLogger(GitFlowMetaData.class);
+
+    private Repository gitRepo;
+    private String remoteToPush;
+    private CredentialsProvider credentialsProvider;
+
+    private final BlockingQueue<Long> pushQueue = new ArrayBlockingQueue<>(1);
+
+    /**
+     * Bucket ID to Bucket.
+     */
+    private Map<String, Bucket> buckets = new HashMap<>();
+
+    public void setRemoteToPush(String remoteToPush) {
+        this.remoteToPush = remoteToPush;
+    }
+
+    public void setRemoteCredential(String userName, String password) {
+        this.credentialsProvider = new UsernamePasswordCredentialsProvider(userName, password);
+    }
+
+    /**
+     * Open a Git repository using the specified directory.
+     * @param gitProjectRootDir a root directory of a Git project
+     * @return created Repository
+     * @throws IOException thrown when the specified directory does not exist,
+     * does not have read/write privilege or not containing .git directory
+     */
+    private Repository openRepository(final File gitProjectRootDir) throws IOException {
+
+        // Instead of using FileUtils.ensureDirectoryExistAndCanReadAndWrite, check availability manually here.
+        // Because the util will try to create a dir if not exist.
+        // The git dir should be initialized and configured by users.
+        if (!gitProjectRootDir.isDirectory()) {
+            throw new IOException(format("'%s' is not a directory or does not exist.", gitProjectRootDir));
+        }
+
+        if (!(gitProjectRootDir.canRead() && gitProjectRootDir.canWrite())) {
+            throw new IOException(format("Directory '%s' does not have read/write privilege.", gitProjectRootDir));
+        }
+
+        // Search .git dir but avoid searching parent directories.
+        final FileRepositoryBuilder builder = new FileRepositoryBuilder()
+                .readEnvironment()
+                .setMustExist(true)
+                .addCeilingDirectory(gitProjectRootDir)
+                .findGitDir(gitProjectRootDir);
+
+        if (builder.getGitDir() == null) {
+            throw new IOException(format("Directory '%s' does not contain a .git directory." +
+                    " Please init and configure the directory with 'git init' command before using it from NiFi Registry.",
+                    gitProjectRootDir));
+        }
+
+        return builder.build();
+    }
+
+    @SuppressWarnings("unchecked")
+    public void loadGitRepository(File gitProjectRootDir) throws IOException, GitAPIException {
+        gitRepo = openRepository(gitProjectRootDir);
+
+        try (final Git git = new Git(gitRepo)) {
+
+            // Check if remote exists.
+            if (!isEmpty(remoteToPush)) {
+                final List<RemoteConfig> remotes = git.remoteList().call();
+                final boolean isRemoteExist = remotes.stream().anyMatch(remote -> remote.getName().equals(remoteToPush));
+                if (!isRemoteExist) {
+                    final List<String> remoteNames = remotes.stream().map(RemoteConfig::getName).collect(Collectors.toList());
+                    throw new IllegalArgumentException(
+                            format("The configured remote '%s' to push does not exist. Available remotes are %s", remoteToPush, remoteNames));
+                }
+            }
+
+            boolean isLatestCommit = true;
+            try {
+                for (RevCommit commit : git.log().call()) {
+                    final String shortCommitId = commit.getId().abbreviate(7).name();
+                    logger.debug("Processing a commit: {}", shortCommitId);
+                    final RevTree tree = commit.getTree();
+
+                    try (final TreeWalk treeWalk = new TreeWalk(gitRepo)) {
+                        treeWalk.addTree(tree);
+
+                        // Path -> ObjectId
+                        final Map<String, ObjectId> bucketObjectIds = new HashMap<>();
+                        final Map<String, ObjectId> flowSnapshotObjectIds = new HashMap<>();
+                        while (treeWalk.next()) {
+                            if (treeWalk.isSubtree()) {
+                                treeWalk.enterSubtree();
+                            } else {
+                                final String pathString = treeWalk.getPathString();
+                                // TODO: what is this nth?? When does it get grater than 0? Tree count seems to be always 1..
+                                if (pathString.endsWith("/" + BUCKET_FILENAME)) {
+                                    bucketObjectIds.put(pathString, treeWalk.getObjectId(0));
+                                } else if (pathString.endsWith(GitFlowPersistenceProvider.SNAPSHOT_EXTENSION)) {
+                                    flowSnapshotObjectIds.put(pathString, treeWalk.getObjectId(0));
+                                }
+                            }
+                        }
+
+                        if (bucketObjectIds.isEmpty()) {
+                            // No bucket.yml means at this point, all flows are deleted. No need to scan older commits because those are already deleted.
+                            logger.debug("Tree at commit {} does not contain any " + BUCKET_FILENAME + ". Stop loading commits here.", shortCommitId);
+                            return;
+                        }
+
+                        loadBuckets(gitRepo, commit, isLatestCommit, bucketObjectIds, flowSnapshotObjectIds);
+                        isLatestCommit = false;
+                    }
+                }
+            } catch (NoHeadException e) {
+                logger.debug("'{}' does not have any commit yet. Starting with empty buckets.", gitProjectRootDir);
+            }
+
+        }
+    }
+
+    void startPushThread() {
+        // If successfully loaded, start pushing thread if necessary.
+        if (isEmpty(remoteToPush)) {
+            return;
+        }
+
+        final ThreadFactory threadFactory = new BasicThreadFactory.Builder()
+                .daemon(true).namingPattern(getClass().getSimpleName() + " Push thread").build();
+
+        // Use scheduled fixed delay to control the minimum interval between push activities.
+        // The necessity of executing push is controlled by offering messages to the pushQueue.
+        // If multiple commits are made within this time window, those are pushed by a single push execution.
+        final ScheduledExecutorService executorService = Executors.newSingleThreadScheduledExecutor(threadFactory);
+        executorService.scheduleWithFixedDelay(() -> {
+
+            final Long offeredTimestamp;
+            try {
+                offeredTimestamp = pushQueue.take();
+            } catch (InterruptedException e) {
+                logger.warn("Waiting for push request has been interrupted due to {}", e.getMessage(), e);
+                return;
+            }
+
+            logger.debug("Took a push request sent at {} to {}...", offeredTimestamp, remoteToPush);
+            final PushCommand pushCommand = new Git(gitRepo).push().setRemote(remoteToPush);
+            if (credentialsProvider != null) {
+                pushCommand.setCredentialsProvider(credentialsProvider);
+            }
+
+            try {
+                final Iterable<PushResult> pushResults = pushCommand.call();
+                for (PushResult pushResult : pushResults) {
+                    logger.debug(pushResult.getMessages());
+                }
+            } catch (GitAPIException e) {
+                logger.error(format("Failed to push commits to %s due to %s", remoteToPush, e), e);
+            }
+
+        }, 10, 10, TimeUnit.SECONDS);
+    }
+
+    @SuppressWarnings("unchecked")
+    private void loadBuckets(Repository gitRepo, RevCommit commit, boolean isLatestCommit, Map<String, ObjectId> bucketObjectIds, Map<String, ObjectId> flowSnapshotObjectIds) throws IOException {
+        final Yaml yaml = new Yaml();
+        for (String bucketFilePath : bucketObjectIds.keySet()) {
+            final ObjectId bucketObjectId = bucketObjectIds.get(bucketFilePath);
+            final Map<String, Object> bucketMeta;
+            try (InputStream bucketIn = gitRepo.newObjectReader().open(bucketObjectId).openStream()) {
+                bucketMeta = yaml.load(bucketIn);
+            }
+
+            if (!validateRequiredValue(bucketMeta, bucketFilePath, LAYOUT_VERSION, BUCKET_ID, FLOWS)) {
+                continue;
+            }
+
+            int layoutVersion = (int) bucketMeta.get(LAYOUT_VERSION);
+            if (layoutVersion > CURRENT_LAYOUT_VERSION) {
+                logger.warn("{} has unsupported {} {}. This Registry can only support {} or lower. Skipping it.",
+                        bucketFilePath, LAYOUT_VERSION, layoutVersion, CURRENT_LAYOUT_VERSION);
+                continue;
+            }
+
+            final String bucketId = (String) bucketMeta.get(BUCKET_ID);
+
+            final Bucket bucket;
+            if (isLatestCommit) {
+                // If this is the latest commit, then create one.
+                bucket = getBucketOrCreate(bucketId);
+            } else {
+                // Otherwise non-existing bucket means it's already deleted.
+                final Optional<Bucket> bucketOpt = getBucket(bucketId);
+                if (bucketOpt.isPresent()) {
+                    bucket = bucketOpt.get();
+                } else {
+                    logger.debug("Bucket {} does not exist any longer. It may have been deleted.", bucketId);
+                    continue;
+                }
+            }
+
+            // Since the bucketName is restored from pathname, it can be different from the original bucket name when it sanitized.
+            final String bucketDirName = bucketFilePath.substring(0, bucketFilePath.lastIndexOf("/"));
+
+            // Since commits are read in LIFO order, avoid old commits overriding the latest bucket name.
+            if (isEmpty(bucket.getBucketDirName())) {
+                bucket.setBucketDirName(bucketDirName);
+            }
+
+            final Map<String, Object> flows = (Map<String, Object>) bucketMeta.get(FLOWS);
+            loadFlows(commit, isLatestCommit, bucket, bucketFilePath, flows, flowSnapshotObjectIds);
+        }
+    }
+
+    @SuppressWarnings("unchecked")
+    private void loadFlows(RevCommit commit, boolean isLatestCommit, Bucket bucket, String backetFilePath, Map<String, Object> flows, Map<String, ObjectId> flowSnapshotObjectIds) {
+        for (String flowId : flows.keySet()) {
+            final Map<String, Object> flowMeta = (Map<String, Object>) flows.get(flowId);
+
+            if (!validateRequiredValue(flowMeta, backetFilePath + ":" + flowId, VER, FILE)) {
+                continue;
+            }
+
+            final Flow flow;
+            if (isLatestCommit) {
+                // If this is the latest commit, then create one.
+                flow = bucket.getFlowOrCreate(flowId);
+            } else {
+                // Otherwise non-existing flow means it's already deleted.
+                final Optional<Flow> flowOpt = bucket.getFlow(flowId);
+                if (flowOpt.isPresent()) {
+                    flow = flowOpt.get();
+                } else {
+                    logger.debug("Flow {} does not exist in bucket {}:{} any longer. It may have been deleted.", flowId, bucket.getBucketDirName(), bucket.getBucketId());
+                    continue;
+                }
+            }
+
+            final int version = (int) flowMeta.get(VER);
+            final String flowSnapshotFilename = (String) flowMeta.get(FILE);
+
+            // Since commits are read in LIFO order, avoid old commits overriding the latest pointer.
+            if (!flow.hasVersion(version)) {
+                final Flow.FlowPointer pointer = new Flow.FlowPointer(flowSnapshotFilename);
+                final File flowSnapshotFile = new File(new File(backetFilePath).getParent(), flowSnapshotFilename);
+                final ObjectId objectId = flowSnapshotObjectIds.get(flowSnapshotFile.getPath());
+                if (objectId == null) {
+                    logger.warn("Git object id for Flow {} version {} with path {} in bucket {}:{} was not found. Ignoring this entry.",
+                            flowId, version, flowSnapshotFile.getPath(), bucket.getBucketDirName(), bucket.getBucketId());
+                    continue;
+                }
+                pointer.setGitRev(commit.getName());
+                pointer.setObjectId(objectId.getName());
+                flow.putVersion(version, pointer);
+            }
+        }
+    }
+
+    private boolean validateRequiredValue(final Map map, String nameOfMap, Object ... keys) {
+        for (Object key : keys) {
+            if (!map.containsKey(key)) {
+                logger.warn("{} does not have {}. Skipping it.", nameOfMap, key);
+                return false;
+            }
+        }
+        return true;
+    }
+
+    public Bucket getBucketOrCreate(String bucketId) {
+        return buckets.computeIfAbsent(bucketId, k -> new Bucket(bucketId));
+    }
+
+    public Optional<Bucket> getBucket(String bucketId) {
+        return Optional.ofNullable(buckets.get(bucketId));
+    }
+
+
+    void saveBucket(final Bucket bucket, final File bucketDir) throws IOException {
+        final Yaml yaml = new Yaml();
+        final Map<String, Object> serializedBucket = bucket.serialize();
+        final File bucketFile = new File(bucketDir, GitFlowMetaData.BUCKET_FILENAME);
+
+        try (final Writer writer = new OutputStreamWriter(
+                new FileOutputStream(bucketFile), StandardCharsets.UTF_8)) {
+            yaml.dump(serializedBucket, writer);
+        }
+    }
+
+    boolean isGitDirectoryClean() throws GitAPIException {
+        final Status status = new Git(gitRepo).status().call();
+        return status.isClean() && !status.hasUncommittedChanges();
+    }
+
+    /**
+     * Create a Git commit.
+     * @param author The name of a NiFi Registry user who created the snapshot. It will be added to the commit message.
+     * @param message Commit message.
+     * @param bucket A bucket to commit.
+     * @param flowPointer A flow pointer for the flow snapshot which is updated.
+     *                    After a commit is created, new commit rev id and flow snapshot file object id are set to this pointer.
+     *                    It can be null if none of flow content is modified.
+     */
+    void commit(String author, String message, Bucket bucket, Flow.FlowPointer flowPointer) throws GitAPIException, IOException {
+        try (final Git git = new Git(gitRepo)) {
+            // Execute add command for newly added files (if any).
+            git.add().addFilepattern(".").call();
+
+            // Execute add command again for deleted files (if any).
+            git.add().addFilepattern(".").setUpdate(true).call();
+
+            final String commitMessage = isEmpty(author) ? message
+                    : format("%s\n\nBy NiFi Registry user: %s", message, author);
+            final RevCommit commit = git.commit()
+                    .setMessage(commitMessage)
+                    .call();
+
+            if (flowPointer != null) {
+                final RevTree tree = commit.getTree();
+                final String bucketDirName = bucket.getBucketDirName();
+                final String flowSnapshotPath = new File(bucketDirName, flowPointer.getFileName()).getPath();
+                try (final TreeWalk treeWalk = new TreeWalk(gitRepo)) {
+                    treeWalk.addTree(tree);
+
+                    while (treeWalk.next()) {
+                        if (treeWalk.isSubtree()) {
+                            treeWalk.enterSubtree();
+                        } else {
+                            final String pathString = treeWalk.getPathString();
+                            if (pathString.equals(flowSnapshotPath)) {
+                                // Capture updated object id.
+                                final String flowSnapshotObjectId = treeWalk.getObjectId(0).getName();
+                                flowPointer.setObjectId(flowSnapshotObjectId);
+                                break;
+                            }
+                        }
+                    }
+                }
+
+                flowPointer.setGitRev(commit.getName());
+            }
+
+            // Push if necessary.
+            if (!isEmpty(remoteToPush)) {
+                // Use different thread since it takes longer.
+                final long offeredTimestamp = System.currentTimeMillis();
+                if (pushQueue.offer(offeredTimestamp)) {
+                    logger.debug("New push request is offered at {}.", offeredTimestamp);
+                }
+            }
+
+        }
+    }
+
+    byte[] getContent(String objectId) throws IOException {
+        final ObjectId flowSnapshotObjectId = gitRepo.resolve(objectId);
+        return gitRepo.newObjectReader().open(flowSnapshotObjectId).getBytes();
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/6f26290d/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/provider/flow/git/GitFlowPersistenceProvider.java
----------------------------------------------------------------------
diff --git a/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/provider/flow/git/GitFlowPersistenceProvider.java b/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/provider/flow/git/GitFlowPersistenceProvider.java
new file mode 100644
index 0000000..f642632
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/provider/flow/git/GitFlowPersistenceProvider.java
@@ -0,0 +1,258 @@
+/*
+ * 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.provider.flow.git;
+
+import org.apache.nifi.registry.flow.FlowPersistenceException;
+import org.apache.nifi.registry.flow.FlowPersistenceProvider;
+import org.apache.nifi.registry.flow.FlowSnapshotContext;
+import org.apache.nifi.registry.provider.ProviderConfigurationContext;
+import org.apache.nifi.registry.provider.ProviderCreationException;
+import org.apache.nifi.registry.util.FileUtils;
+import org.eclipse.jgit.api.errors.GitAPIException;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.File;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.OutputStream;
+import java.util.Map;
+import java.util.Optional;
+
+import static java.lang.String.format;
+import static org.apache.commons.lang3.StringUtils.isEmpty;
+import static org.apache.nifi.registry.util.FileUtils.sanitizeFilename;
+
+public class GitFlowPersistenceProvider implements FlowPersistenceProvider {
+
+    private static final Logger logger = LoggerFactory.getLogger(GitFlowMetaData.class);
+    static final String FLOW_STORAGE_DIR_PROP = "Flow Storage Directory";
+    private static final String REMOTE_TO_PUSH = "Remote To Push";
+    private static final String REMOTE_ACCESS_USER = "Remote Access User";
+    private static final String REMOTE_ACCESS_PASSWORD = "Remote Access Password";
+    static final String SNAPSHOT_EXTENSION = ".snapshot";
+
+    private File flowStorageDir;
+    private GitFlowMetaData flowMetaData;
+
+    @Override
+    public void onConfigured(ProviderConfigurationContext configurationContext) throws ProviderCreationException {
+        flowMetaData = new GitFlowMetaData();
+
+        final Map<String,String> props = configurationContext.getProperties();
+        if (!props.containsKey(FLOW_STORAGE_DIR_PROP)) {
+            throw new ProviderCreationException("The property " + FLOW_STORAGE_DIR_PROP + " must be provided");
+        }
+
+        final String flowStorageDirValue = props.get(FLOW_STORAGE_DIR_PROP);
+        if (isEmpty(flowStorageDirValue)) {
+            throw new ProviderCreationException("The property " + FLOW_STORAGE_DIR_PROP + " cannot be null or blank");
+        }
+
+        flowMetaData.setRemoteToPush(props.get(REMOTE_TO_PUSH));
+
+        final String remoteUser = props.get(REMOTE_ACCESS_USER);
+        final String remotePassword = props.get(REMOTE_ACCESS_PASSWORD);
+        if (!isEmpty(remoteUser) && isEmpty(remotePassword)) {
+            throw new ProviderCreationException(format("The property %s is specified but %s is not." +
+                    " %s is required for username password authentication.",
+                    REMOTE_ACCESS_USER, REMOTE_ACCESS_PASSWORD, REMOTE_ACCESS_PASSWORD));
+        }
+        if (!isEmpty(remotePassword)) {
+            flowMetaData.setRemoteCredential(remoteUser, remotePassword);
+        }
+
+        try {
+            flowStorageDir = new File(flowStorageDirValue);
+            flowMetaData.loadGitRepository(flowStorageDir);
+            flowMetaData.startPushThread();
+            logger.info("Configured GitFlowPersistenceProvider with Flow Storage Directory {}",
+                    new Object[] {flowStorageDir.getAbsolutePath()});
+        } catch (IOException|GitAPIException e) {
+            throw new ProviderCreationException("Failed to load a git repository " + flowStorageDir, e);
+        }
+    }
+
+    @Override
+    public void saveFlowContent(FlowSnapshotContext context, byte[] content) throws FlowPersistenceException {
+
+        try {
+            // Check if working dir is clean, any uncommitted file?
+            if (!flowMetaData.isGitDirectoryClean()) {
+                throw new FlowPersistenceException(format("Git directory %s is not clean" +
+                                " or has uncommitted changes, resolve those changes first to save flow contents.",
+                        flowStorageDir));
+            }
+        } catch (GitAPIException e) {
+            throw new FlowPersistenceException(format("Failed to get Git status for directory %s due to %s",
+                    flowStorageDir, e));
+        }
+
+        final String bucketId = context.getBucketId();
+        final Bucket bucket = flowMetaData.getBucketOrCreate(bucketId);
+        final String currentBucketDirName = bucket.getBucketDirName();
+        final String bucketDirName = sanitizeFilename(context.getBucketName());
+        final boolean isBucketNameChanged = !bucketDirName.equals(currentBucketDirName);
+        bucket.setBucketDirName(bucketDirName);
+
+        final Flow flow = bucket.getFlowOrCreate(context.getFlowId());
+        final String flowSnapshotFilename = sanitizeFilename(context.getFlowName()) + SNAPSHOT_EXTENSION;
+
+        final Optional<String> currentFlowSnapshotFilename = flow
+                .getLatestVersion().map(flow::getFlowVersion).map(Flow.FlowPointer::getFileName);
+
+        // Add new version.
+        final Flow.FlowPointer flowPointer = new Flow.FlowPointer(flowSnapshotFilename);
+        flow.putVersion(context.getVersion(), flowPointer);
+
+        final File bucketDir = new File(flowStorageDir, bucketDirName);
+        final File flowSnippetFile = new File(bucketDir, flowSnapshotFilename);
+
+        final File currentBucketDir = isEmpty(currentBucketDirName) ? null : new File(flowStorageDir, currentBucketDirName);
+        if (currentBucketDir != null && currentBucketDir.isDirectory()) {
+            if (isBucketNameChanged) {
+                logger.debug("Detected bucket name change from {} to {}, moving it.", currentBucketDirName, bucketDirName);
+                if (!currentBucketDir.renameTo(bucketDir)) {
+                    throw new FlowPersistenceException(format("Failed to move existing bucket %s to %s.", currentBucketDir, bucketDir));
+                }
+            }
+        } else {
+            if (!bucketDir.mkdirs()) {
+                throw new FlowPersistenceException(format("Failed to create new bucket dir %s.", bucketDir));
+            }
+        }
+
+
+        try {
+            if (currentFlowSnapshotFilename.isPresent() && !flowSnapshotFilename.equals(currentFlowSnapshotFilename.get())) {
+                // Delete old file if flow name has been changed.
+                final File latestFlowSnapshotFile = new File(bucketDir, currentFlowSnapshotFilename.get());
+                logger.debug("Detected flow name change from {} to {}, deleting the old snapshot file.",
+                        currentFlowSnapshotFilename.get(), flowSnapshotFilename);
+                latestFlowSnapshotFile.delete();
+            }
+
+            // Save the content.
+            try (final OutputStream os = new FileOutputStream(flowSnippetFile)) {
+                os.write(content);
+                os.flush();
+            }
+
+            // Write a bucket file.
+            flowMetaData.saveBucket(bucket, bucketDir);
+
+            // Create a Git Commit.
+            flowMetaData.commit(context.getAuthor(), context.getComments(), bucket, flowPointer);
+
+        } catch (IOException|GitAPIException e) {
+            throw new FlowPersistenceException("Failed to persist flow.", e);
+        }
+
+        // TODO: What if user rebased commits? Version number to Commit ID mapping will be broken.
+    }
+
+    @Override
+    public byte[] getFlowContent(String bucketId, String flowId, int version) throws FlowPersistenceException {
+
+        final Bucket bucket = getBucketOrFail(bucketId);
+        final Flow flow = getFlowOrFail(bucket, flowId);
+        if (!flow.hasVersion(version)) {
+            throw new FlowPersistenceException(format("Flow ID %s version %d was not found in bucket %s:%s.",
+                    flowId, version, bucket.getBucketDirName(), bucketId));
+        }
+
+        final Flow.FlowPointer flowPointer = flow.getFlowVersion(version);
+        try {
+            return flowMetaData.getContent(flowPointer.getObjectId());
+        } catch (IOException e) {
+            throw new FlowPersistenceException(format("Failed to get content of Flow ID %s version %d in bucket %s:%s due to %s.",
+                    flowId, version, bucket.getBucketDirName(), bucketId, e), e);
+        }
+    }
+
+    // TODO: Need to add userId argument?
+    @Override
+    public void deleteAllFlowContent(String bucketId, String flowId) throws FlowPersistenceException {
+        final Bucket bucket = getBucketOrFail(bucketId);
+        final Flow flow = getFlowOrFail(bucket, flowId);
+        final Optional<Integer> latestVersionOpt = flow.getLatestVersion();
+        if (!latestVersionOpt.isPresent()) {
+            throw new IllegalStateException("Flow version is not added yet, can not be deleted.");
+        }
+
+        final Integer latestVersion = latestVersionOpt.get();
+        final Flow.FlowPointer flowPointer = flow.getFlowVersion(latestVersion);
+
+        // Delete the flow snapshot.
+        final File bucketDir = new File(flowStorageDir, bucket.getBucketDirName());
+        final File flowSnapshotFile = new File(bucketDir, flowPointer.getFileName());
+        if (flowSnapshotFile.exists()) {
+            if (!flowSnapshotFile.delete()) {
+                throw new FlowPersistenceException(format("Failed to delete flow content for %s:%s in bucket %s:%s",
+                        flowPointer.getFileName(), flowId, bucket.getBucketDirName(), bucketId));
+            }
+        }
+
+        bucket.removeFlow(flowId);
+
+        try {
+
+            if (bucket.isEmpty()) {
+                // delete bucket dir if this is the last flow.
+                FileUtils.deleteFile(bucketDir, true);
+            } else {
+                // Write a bucket file.
+                flowMetaData.saveBucket(bucket, bucketDir);
+            }
+
+            // Create a Git Commit.
+            final String commitMessage = format("Deleted flow %s:%s in bucket %s:%s.",
+                    flowPointer.getFileName(), flowId, bucket.getBucketDirName(), bucketId);
+            flowMetaData.commit(null, commitMessage, bucket, null);
+
+        } catch (IOException|GitAPIException e) {
+            throw new FlowPersistenceException(format("Failed to delete flow %s:%s in bucket %s:%s due to %s",
+                    flowPointer.getFileName(), flowId, bucket.getBucketDirName(), bucketId, e), e);
+        }
+
+    }
+
+    private Bucket getBucketOrFail(String bucketId) throws FlowPersistenceException {
+        final Optional<Bucket> bucketOpt = flowMetaData.getBucket(bucketId);
+        if (!bucketOpt.isPresent()) {
+            throw new FlowPersistenceException(format("Bucket ID %s was not found.", bucketId));
+        }
+
+        return bucketOpt.get();
+    }
+
+    private Flow getFlowOrFail(Bucket bucket, String flowId) throws FlowPersistenceException {
+        final Optional<Flow> flowOpt = bucket.getFlow(flowId);
+        if (!flowOpt.isPresent()) {
+            throw new FlowPersistenceException(format("Flow ID %s was not found in bucket %s:%s.",
+                    flowId, bucket.getBucketDirName(), bucket.getBucketId()));
+        }
+
+        return flowOpt.get();
+    }
+
+    @Override
+    public void deleteFlowContent(String bucketId, String flowId, int version) throws FlowPersistenceException {
+        // TODO: Do nothing? This signature is not used. Actually there's nothing to do to the old versions as those exist in old commits even if this method is called.
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/6f26290d/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/provider/hook/LoggingEventHookProvider.java
----------------------------------------------------------------------
diff --git a/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/provider/hook/LoggingEventHookProvider.java b/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/provider/hook/LoggingEventHookProvider.java
new file mode 100644
index 0000000..9ceb59f
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/provider/hook/LoggingEventHookProvider.java
@@ -0,0 +1,59 @@
+/*
+ * 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.provider.hook;
+
+import org.apache.nifi.registry.hook.Event;
+import org.apache.nifi.registry.hook.EventField;
+import org.apache.nifi.registry.hook.EventHookException;
+import org.apache.nifi.registry.hook.EventHookProvider;
+import org.apache.nifi.registry.provider.ProviderConfigurationContext;
+import org.apache.nifi.registry.provider.ProviderCreationException;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+public class LoggingEventHookProvider
+    implements EventHookProvider {
+
+    static final Logger LOGGER = LoggerFactory.getLogger(LoggingEventHookProvider.class);
+
+    @Override
+    public void onConfigured(ProviderConfigurationContext configurationContext) throws ProviderCreationException {
+        // Nothing to do
+    }
+
+    @Override
+    public void handle(final Event event) throws EventHookException {
+
+        final StringBuilder builder = new StringBuilder()
+                .append(event.getEventType())
+                .append(" [");
+
+        int count = 0;
+        for (final EventField argument : event.getFields()) {
+            if (count > 0) {
+                builder.append(", ");
+            }
+            builder.append(argument.getName()).append("=").append(argument.getValue());
+            count++;
+        }
+
+        builder.append("] ");
+
+        LOGGER.info(builder.toString());
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/6f26290d/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/provider/hook/ScriptEventHookProvider.java
----------------------------------------------------------------------
diff --git a/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/provider/hook/ScriptEventHookProvider.java b/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/provider/hook/ScriptEventHookProvider.java
new file mode 100644
index 0000000..f96115e
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/provider/hook/ScriptEventHookProvider.java
@@ -0,0 +1,102 @@
+/*
+ * 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.provider.hook;
+
+import java.io.File;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+
+import org.apache.commons.lang3.StringUtils;
+import org.apache.nifi.registry.hook.Event;
+import org.apache.nifi.registry.hook.EventField;
+import org.apache.nifi.registry.hook.WhitelistFilteringEventHookProvider;
+import org.apache.nifi.registry.provider.ProviderConfigurationContext;
+import org.apache.nifi.registry.provider.ProviderCreationException;
+import org.apache.nifi.registry.util.FileUtils;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * A EventHookProvider that is used to execute a script to handle the event.
+ */
+public class ScriptEventHookProvider
+        extends WhitelistFilteringEventHookProvider {
+
+    static final Logger LOGGER = LoggerFactory.getLogger(ScriptEventHookProvider.class);
+    static final String SCRIPT_PATH_PROP = "Script Path";
+    static final String SCRIPT_WORKDIR_PROP = "Working Directory";
+    private File scriptFile;
+    private File workDirFile;
+
+
+    @Override
+    public void handle(final Event event) {
+        List<String> command = new ArrayList<>();
+        command.add(scriptFile.getAbsolutePath());
+        command.add(event.getEventType().name());
+
+        for (EventField arg : event.getFields()) {
+            command.add(arg.getValue());
+        }
+
+        final String commandString = StringUtils.join(command, " ");
+        final ProcessBuilder builder = new ProcessBuilder(command);
+        builder.directory(workDirFile);
+        LOGGER.debug("Execution of " + commandString);
+
+        try {
+            builder.start();
+        } catch (IOException e) {
+            LOGGER.error("Execution of {0} failed with: {1}", new Object[] { commandString, e.getLocalizedMessage() }, e);
+        }
+    }
+
+    @Override
+    public void onConfigured(ProviderConfigurationContext configurationContext) throws ProviderCreationException {
+        super.onConfigured(configurationContext);
+
+        final Map<String,String> props = configurationContext.getProperties();
+        if (!props.containsKey(SCRIPT_PATH_PROP)) {
+            throw new ProviderCreationException("The property " + SCRIPT_PATH_PROP + " must be provided");
+        }
+
+        final String scripPath = props.get(SCRIPT_PATH_PROP);
+        if (StringUtils.isBlank(scripPath)) {
+            throw new ProviderCreationException("The property " + SCRIPT_PATH_PROP + " cannot be null or blank");
+        }
+
+        if(props.containsKey(SCRIPT_WORKDIR_PROP) && !StringUtils.isBlank(props.get(SCRIPT_WORKDIR_PROP))) {
+            final String workdir = props.get(SCRIPT_WORKDIR_PROP);
+            try {
+                workDirFile = new File(workdir);
+                FileUtils.ensureDirectoryExistAndCanRead(workDirFile);
+            } catch (IOException e) {
+                throw new ProviderCreationException("The working directory " + workdir + " cannot be read.");
+            }
+        }
+
+        scriptFile = new File(scripPath);
+        if(scriptFile.isFile() && scriptFile.canExecute()) {
+            LOGGER.info("Configured ScriptEventHookProvider with script {}", new Object[] {scriptFile.getAbsolutePath()});
+        } else {
+            throw new ProviderCreationException("The script file " + scriptFile.getAbsolutePath() + " cannot be executed.");
+        }
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/6f26290d/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/security/authentication/IdentityProviderFactory.java
----------------------------------------------------------------------
diff --git a/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/security/authentication/IdentityProviderFactory.java b/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/security/authentication/IdentityProviderFactory.java
new file mode 100644
index 0000000..3c2a3f4
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/security/authentication/IdentityProviderFactory.java
@@ -0,0 +1,291 @@
+/*
+ * 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.security.authentication;
+
+import org.apache.commons.lang3.StringUtils;
+import org.apache.nifi.registry.extension.ExtensionManager;
+import org.apache.nifi.registry.properties.NiFiRegistryProperties;
+import org.apache.nifi.registry.properties.SensitivePropertyProtectionException;
+import org.apache.nifi.registry.properties.SensitivePropertyProvider;
+import org.apache.nifi.registry.security.authentication.annotation.IdentityProviderContext;
+import org.apache.nifi.registry.security.authentication.generated.IdentityProviders;
+import org.apache.nifi.registry.security.authentication.generated.Property;
+import org.apache.nifi.registry.security.authentication.generated.Provider;
+import org.apache.nifi.registry.security.util.XmlUtils;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.beans.factory.DisposableBean;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.context.annotation.Primary;
+import org.springframework.lang.Nullable;
+import org.xml.sax.SAXException;
+
+import javax.xml.XMLConstants;
+import javax.xml.bind.JAXBContext;
+import javax.xml.bind.JAXBElement;
+import javax.xml.bind.JAXBException;
+import javax.xml.bind.Unmarshaller;
+import javax.xml.stream.XMLStreamReader;
+import javax.xml.transform.stream.StreamSource;
+import javax.xml.validation.Schema;
+import javax.xml.validation.SchemaFactory;
+import java.io.File;
+import java.lang.reflect.Constructor;
+import java.lang.reflect.Field;
+import java.lang.reflect.InvocationTargetException;
+import java.lang.reflect.Method;
+import java.util.HashMap;
+import java.util.Map;
+
+@Configuration
+public class IdentityProviderFactory implements IdentityProviderLookup, DisposableBean {
+
+    private static final Logger logger = LoggerFactory.getLogger(IdentityProviderFactory.class);
+    private static final String LOGIN_IDENTITY_PROVIDERS_XSD = "/identity-providers.xsd";
+    private static final String JAXB_GENERATED_PATH = "org.apache.nifi.registry.security.authentication.generated";
+    private static final JAXBContext JAXB_CONTEXT = initializeJaxbContext();
+
+    private static JAXBContext initializeJaxbContext() {
+        try {
+            return JAXBContext.newInstance(JAXB_GENERATED_PATH, IdentityProviderFactory.class.getClassLoader());
+        } catch (JAXBException e) {
+            throw new RuntimeException("Unable to create JAXBContext.");
+        }
+    }
+
+    private NiFiRegistryProperties properties;
+    private ExtensionManager extensionManager;
+    private SensitivePropertyProvider sensitivePropertyProvider;
+    private IdentityProvider identityProvider;
+    private final Map<String, IdentityProvider> identityProviders = new HashMap<>();
+
+    @Autowired
+    public IdentityProviderFactory(
+            final NiFiRegistryProperties properties,
+            final ExtensionManager extensionManager,
+            @Nullable final SensitivePropertyProvider sensitivePropertyProvider) {
+        this.properties = properties;
+        this.extensionManager = extensionManager;
+        this.sensitivePropertyProvider = sensitivePropertyProvider;
+
+        if (this.properties == null) {
+            throw new IllegalStateException("NiFiRegistryProperties cannot be null");
+        }
+
+        if (this.extensionManager == null) {
+            throw new IllegalStateException("ExtensionManager cannot be null");
+        }
+    }
+
+    @Override
+    public IdentityProvider getIdentityProvider(String identifier) {
+        return identityProviders.get(identifier);
+    }
+
+    @Bean
+    @Primary
+    public IdentityProvider getIdentityProvider() throws Exception {
+        if (identityProvider == null) {
+            // look up the login identity provider to use
+            final String loginIdentityProviderIdentifier = properties.getProperty(NiFiRegistryProperties.SECURITY_IDENTITY_PROVIDER);
+
+            // ensure the login identity provider class name was specified
+            if (StringUtils.isNotBlank(loginIdentityProviderIdentifier)) {
+                final IdentityProviders loginIdentityProviderConfiguration = loadLoginIdentityProvidersConfiguration();
+
+                // create each login identity provider
+                for (final Provider provider : loginIdentityProviderConfiguration.getProvider()) {
+                    identityProviders.put(provider.getIdentifier(), createLoginIdentityProvider(provider.getIdentifier(), provider.getClazz()));
+                }
+
+                // configure each login identity provider
+                for (final Provider provider : loginIdentityProviderConfiguration.getProvider()) {
+                    final IdentityProvider instance = identityProviders.get(provider.getIdentifier());
+                    instance.onConfigured(loadLoginIdentityProviderConfiguration(provider));
+                }
+
+                // get the login identity provider instance
+                identityProvider = getIdentityProvider(loginIdentityProviderIdentifier);
+
+                // ensure it was found
+                if (identityProvider == null) {
+                    throw new Exception(String.format("The specified login identity provider '%s' could not be found.", loginIdentityProviderIdentifier));
+                }
+            }
+        }
+
+        return identityProvider;
+    }
+
+    @Override
+    public void destroy() throws Exception {
+        if (identityProviders != null) {
+            identityProviders.entrySet().stream().forEach(e -> e.getValue().preDestruction());
+        }
+    }
+
+    private IdentityProviders loadLoginIdentityProvidersConfiguration() throws Exception {
+        final File loginIdentityProvidersConfigurationFile = properties.getIdentityProviderConfigurationFile();
+
+        // load the users from the specified file
+        if (loginIdentityProvidersConfigurationFile.exists()) {
+            try {
+                // find the schema
+                final SchemaFactory schemaFactory = SchemaFactory.newInstance(XMLConstants.W3C_XML_SCHEMA_NS_URI);
+                final Schema schema = schemaFactory.newSchema(IdentityProviders.class.getResource(LOGIN_IDENTITY_PROVIDERS_XSD));
+
+                // attempt to unmarshal
+                XMLStreamReader xsr = XmlUtils.createSafeReader(new StreamSource(loginIdentityProvidersConfigurationFile));
+                final Unmarshaller unmarshaller = JAXB_CONTEXT.createUnmarshaller();
+                unmarshaller.setSchema(schema);
+                final JAXBElement<IdentityProviders> element = unmarshaller.unmarshal(xsr, IdentityProviders.class);
+                return element.getValue();
+            } catch (SAXException | JAXBException e) {
+                throw new Exception("Unable to load the login identity provider configuration file at: " + loginIdentityProvidersConfigurationFile.getAbsolutePath());
+            }
+        } else {
+            throw new Exception("Unable to find the login identity provider configuration file at " + loginIdentityProvidersConfigurationFile.getAbsolutePath());
+        }
+    }
+
+    private IdentityProvider createLoginIdentityProvider(final String identifier, final String loginIdentityProviderClassName) throws Exception {
+        final IdentityProvider instance;
+
+        final ClassLoader classLoader = extensionManager.getExtensionClassLoader(loginIdentityProviderClassName);
+        if (classLoader == null) {
+            throw new IllegalStateException("Extension not found in any of the configured class loaders: " + loginIdentityProviderClassName);
+        }
+
+        // attempt to load the class
+        Class<?> rawLoginIdentityProviderClass = Class.forName(loginIdentityProviderClassName, true, classLoader);
+        Class<? extends IdentityProvider> loginIdentityProviderClass = rawLoginIdentityProviderClass.asSubclass(IdentityProvider.class);
+
+        // otherwise create a new instance
+        Constructor constructor = loginIdentityProviderClass.getConstructor();
+        instance = (IdentityProvider) constructor.newInstance();
+
+        // method injection
+        performMethodInjection(instance, loginIdentityProviderClass);
+
+        // field injection
+        performFieldInjection(instance, loginIdentityProviderClass);
+
+        return instance;
+    }
+
+    private IdentityProviderConfigurationContext loadLoginIdentityProviderConfiguration(final Provider provider) {
+        final Map<String, String> providerProperties = new HashMap<>();
+
+        for (final Property property : provider.getProperty()) {
+            if (!StringUtils.isBlank(property.getEncryption())) {
+                String decryptedValue = decryptValue(property.getValue(), property.getEncryption());
+                providerProperties.put(property.getName(), decryptedValue);
+            } else {
+                providerProperties.put(property.getName(), property.getValue());
+            }
+        }
+
+        return new StandardIdentityProviderConfigurationContext(provider.getIdentifier(), this, providerProperties);
+    }
+
+    private void performMethodInjection(final IdentityProvider instance, final Class loginIdentityProviderClass)
+            throws IllegalAccessException, IllegalArgumentException, InvocationTargetException {
+
+        for (final Method method : loginIdentityProviderClass.getMethods()) {
+            if (method.isAnnotationPresent(IdentityProviderContext.class)) {
+                // make the method accessible
+                final boolean isAccessible = method.isAccessible();
+                method.setAccessible(true);
+
+                try {
+                    final Class<?>[] argumentTypes = method.getParameterTypes();
+
+                    // look for setters (single argument)
+                    if (argumentTypes.length == 1) {
+                        final Class<?> argumentType = argumentTypes[0];
+
+                        // look for well known types
+                        if (NiFiRegistryProperties.class.isAssignableFrom(argumentType)) {
+                            // nifi properties injection
+                            method.invoke(instance, properties);
+                        }
+                    }
+                } finally {
+                    method.setAccessible(isAccessible);
+                }
+            }
+        }
+
+        final Class parentClass = loginIdentityProviderClass.getSuperclass();
+        if (parentClass != null && IdentityProvider.class.isAssignableFrom(parentClass)) {
+            performMethodInjection(instance, parentClass);
+        }
+    }
+
+    private void performFieldInjection(final IdentityProvider instance, final Class loginIdentityProviderClass) throws IllegalArgumentException, IllegalAccessException {
+        for (final Field field : loginIdentityProviderClass.getDeclaredFields()) {
+            if (field.isAnnotationPresent(IdentityProviderContext.class)) {
+                // make the method accessible
+                final boolean isAccessible = field.isAccessible();
+                field.setAccessible(true);
+
+                try {
+                    // get the type
+                    final Class<?> fieldType = field.getType();
+
+                    // only consider this field if it isn't set yet
+                    if (field.get(instance) == null) {
+                        // look for well known types
+                        if (NiFiRegistryProperties.class.isAssignableFrom(fieldType)) {
+                            // nifi properties injection
+                            field.set(instance, properties);
+                        }
+                    }
+
+                } finally {
+                    field.setAccessible(isAccessible);
+                }
+            }
+        }
+
+        final Class parentClass = loginIdentityProviderClass.getSuperclass();
+        if (parentClass != null && IdentityProvider.class.isAssignableFrom(parentClass)) {
+            performFieldInjection(instance, parentClass);
+        }
+    }
+
+    private String decryptValue(String cipherText, String encryptionScheme) throws SensitivePropertyProtectionException {
+        if (sensitivePropertyProvider == null) {
+            throw new SensitivePropertyProtectionException("Sensitive Property Provider dependency was never wired, so protected " +
+                    "properties cannot be decrypted. This usually indicates that a master key for this NiFi Registry was not " +
+                    "detected and configured during the bootstrap startup sequence. Contact the system administrator.");
+        }
+
+        if (!sensitivePropertyProvider.getIdentifierKey().equalsIgnoreCase(encryptionScheme)) {
+            throw new SensitivePropertyProtectionException("Identity Provider configuration XML was protected using " +
+                    encryptionScheme +
+                    ", but the configured Sensitive Property Provider supports " +
+                    sensitivePropertyProvider.getIdentifierKey() +
+                    ". Cannot configure this Identity Provider due to failing to decrypt protected configuration properties.");
+        }
+
+        return sensitivePropertyProvider.unprotect(cipherText);
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/6f26290d/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/security/authentication/StandardIdentityProviderConfigurationContext.java
----------------------------------------------------------------------
diff --git a/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/security/authentication/StandardIdentityProviderConfigurationContext.java b/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/security/authentication/StandardIdentityProviderConfigurationContext.java
new file mode 100644
index 0000000..3e89dcc
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/security/authentication/StandardIdentityProviderConfigurationContext.java
@@ -0,0 +1,54 @@
+/*
+ * 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.security.authentication;
+
+import java.util.Collections;
+import java.util.Map;
+
+public class StandardIdentityProviderConfigurationContext implements IdentityProviderConfigurationContext {
+
+    private final String identifier;
+    private final IdentityProviderLookup lookup;
+    private final Map<String, String> properties;
+
+    public StandardIdentityProviderConfigurationContext(String identifier, final IdentityProviderLookup lookup, Map<String, String> properties) {
+        this.identifier = identifier;
+        this.lookup = lookup;
+        this.properties = properties;
+    }
+
+    @Override
+    public String getIdentifier() {
+        return identifier;
+    }
+
+    @Override
+    public IdentityProviderLookup getIdentityProviderLookup() {
+        return lookup;
+    }
+
+    @Override
+    public Map<String, String> getProperties() {
+        return Collections.unmodifiableMap(properties);
+    }
+
+    @Override
+    public String getProperty(String property) {
+        return properties.get(property);
+    }
+
+}


[16/51] [partial] nifi-registry git commit: NIFIREG-201 Refactoring project structure to better isolate extensions

Posted by kd...@apache.org.
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/FlowResource.java
----------------------------------------------------------------------
diff --git a/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/api/FlowResource.java b/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/api/FlowResource.java
new file mode 100644
index 0000000..afb8e11
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/api/FlowResource.java
@@ -0,0 +1,315 @@
+/*
+ * 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.event.EventService;
+import org.apache.nifi.registry.field.Fields;
+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.exception.AccessDeniedException;
+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.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.PathParam;
+import javax.ws.rs.Produces;
+import javax.ws.rs.core.MediaType;
+import javax.ws.rs.core.Response;
+import java.util.Set;
+import java.util.SortedSet;
+
+@Component
+@Path("/flows")
+@Api(
+        value = "flows",
+        description = "Gets metadata about flows.",
+        authorizations = { @Authorization("Authorization") }
+)
+public class FlowResource extends AuthorizableApplicationResource {
+
+    private final RegistryService registryService;
+    private final LinkService linkService;
+    private final PermissionsService permissionsService;
+
+    @Autowired
+    public FlowResource(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;
+    }
+
+    @GET
+    @Path("fields")
+    @Consumes(MediaType.WILDCARD)
+    @Produces(MediaType.APPLICATION_JSON)
+    @ApiOperation(
+            value = "Retrieves the available field names that can be used for searching or sorting on flows.",
+            response = Fields.class
+    )
+    public Response getAvailableFlowFields() {
+        final Set<String> flowFields = registryService.getFlowFields();
+        final Fields fields = new Fields(flowFields);
+        return Response.status(Response.Status.OK).entity(fields).build();
+    }
+
+    @GET
+    @Path("{flowId}")
+    @Consumes(MediaType.WILDCARD)
+    @Produces(MediaType.APPLICATION_JSON)
+    @ApiOperation(
+            value = "Gets a flow",
+            nickname = "globalGetFlow",
+            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("flowId")
+            @ApiParam("The flow identifier")
+            final String flowId) {
+
+        final VersionedFlow flow = registryService.getFlow(flowId);
+
+        // this should never happen, but if somehow the back-end didn't populate the bucket id let's make sure the flow isn't returned
+        if (StringUtils.isBlank(flow.getBucketIdentifier())) {
+            throw new IllegalStateException("Unable to authorize access because bucket identifier is null or blank");
+        }
+
+        authorizeBucketAccess(RequestAction.READ, flow.getBucketIdentifier());
+
+        permissionsService.populateItemPermissions(flow);
+        linkService.populateFlowLinks(flow);
+
+        return Response.status(Response.Status.OK).entity(flow).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.",
+            nickname = "globalGetFlowVersions",
+            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("flowId")
+            @ApiParam("The flow identifier")
+            final String flowId) {
+
+        final VersionedFlow flow = registryService.getFlow(flowId);
+
+        final String bucketId = flow.getBucketIdentifier();
+        if (StringUtils.isBlank(bucketId)) {
+            throw new IllegalStateException("Unable to authorize access because bucket identifier is null or blank");
+        }
+
+        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/{versionNumber: \\d+}")
+    @Consumes(MediaType.WILDCARD)
+    @Produces(MediaType.APPLICATION_JSON)
+    @ApiOperation(
+            value = "Gets the given version of a flow",
+            nickname = "globalGetFlowVersion",
+            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("flowId")
+            @ApiParam("The flow identifier")
+            final String flowId,
+            @PathParam("versionNumber")
+            @ApiParam("The version number")
+            final Integer versionNumber) {
+
+        final VersionedFlowSnapshotMetadata latestMetadata = registryService.getLatestFlowSnapshotMetadata(flowId);
+
+        final String bucketId = latestMetadata.getBucketIdentifier();
+        if (StringUtils.isBlank(bucketId)) {
+            throw new IllegalStateException("Unable to authorize access because bucket identifier is null or blank");
+        }
+
+        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}/versions/latest")
+    @Consumes(MediaType.WILDCARD)
+    @Produces(MediaType.APPLICATION_JSON)
+    @ApiOperation(
+            value = "Get the latest version of a flow",
+            nickname = "globalGetLatestFlowVersion",
+            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("flowId")
+            @ApiParam("The flow identifier")
+            final String flowId) {
+
+        final VersionedFlowSnapshotMetadata latestMetadata = registryService.getLatestFlowSnapshotMetadata(flowId);
+
+        final String bucketId = latestMetadata.getBucketIdentifier();
+        if (StringUtils.isBlank(bucketId)) {
+            throw new IllegalStateException("Unable to authorize access because bucket identifier is null or blank");
+        }
+
+        authorizeBucketAccess(RequestAction.READ, bucketId);
+
+        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",
+            nickname = "globalGetLatestFlowVersionMetadata",
+            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("flowId")
+            @ApiParam("The flow identifier")
+            final String flowId) {
+
+        final VersionedFlowSnapshotMetadata latestMetadata = registryService.getLatestFlowSnapshotMetadata(flowId);
+
+        final String bucketId = latestMetadata.getBucketIdentifier();
+        if (StringUtils.isBlank(bucketId)) {
+            throw new IllegalStateException("Unable to authorize access because bucket identifier is null or blank");
+        }
+
+        authorizeBucketAccess(RequestAction.READ, bucketId);
+
+        linkService.populateSnapshotLinks(latestMetadata);
+        return Response.status(Response.Status.OK).entity(latestMetadata).build();
+    }
+
+    // override the base implementation so we can provide a different error message that doesn't include the bucket id
+    protected void authorizeBucketAccess(RequestAction action, String bucketId) {
+        try {
+            super.authorizeBucketAccess(RequestAction.READ, bucketId);
+        } catch (AccessDeniedException e) {
+            throw new AccessDeniedException("User not authorized to view the specified flow.", e);
+        }
+    }
+
+    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());
+        }
+
+    }
+}

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/HttpStatusMessages.java
----------------------------------------------------------------------
diff --git a/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/api/HttpStatusMessages.java b/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/api/HttpStatusMessages.java
new file mode 100644
index 0000000..a3ba939
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/api/HttpStatusMessages.java
@@ -0,0 +1,30 @@
+/*
+ * 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;
+
+class HttpStatusMessages {
+
+    /* 4xx messages */
+    static final String MESSAGE_400 = "NiFi Registry was unable to complete the request because it was invalid. The request should not be retried without modification.";
+    static final String MESSAGE_401 = "Client could not be authenticated.";
+    static final String MESSAGE_403 = "Client is not authorized to make this request.";
+    static final String MESSAGE_404 = "The specified resource could not be found.";
+    static final String MESSAGE_409 = "NiFi Registry was unable to complete the request because it assumes a server state that is not valid.";
+
+    /* 5xx messages */
+    static final String MESSAGE_500 = "NiFi Registry was unable to complete the request because an unexpected error occurred.";
+}

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/ItemResource.java
----------------------------------------------------------------------
diff --git a/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/api/ItemResource.java b/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/api/ItemResource.java
new file mode 100644
index 0000000..02b63d2
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/api/ItemResource.java
@@ -0,0 +1,171 @@
+/*
+ * 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.nifi.registry.bucket.BucketItem;
+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.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.Consumes;
+import javax.ws.rs.GET;
+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.Collections;
+import java.util.List;
+import java.util.Set;
+
+@Component
+@Path("/items")
+@Api(
+        value = "items",
+        description = "Retrieve items across all buckets for which the user is authorized.",
+        authorizations = { @Authorization("Authorization") }
+)
+public class ItemResource extends AuthorizableApplicationResource {
+
+    private static final Logger LOGGER = LoggerFactory.getLogger(ItemResource.class);
+
+    @Context
+    UriInfo uriInfo;
+
+    private final LinkService linkService;
+    private final PermissionsService permissionsService;
+    private final RegistryService registryService;
+
+    @Autowired
+    public ItemResource(
+            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;
+    }
+
+
+    @GET
+    @Consumes(MediaType.WILDCARD)
+    @Produces(MediaType.APPLICATION_JSON)
+    @ApiOperation(
+            value = "Get items across all buckets",
+            notes = "The returned items will include only items from buckets for which the user is authorized. " +
+                    "If the user is not authorized to any buckets, an empty list will be returned.",
+            response = BucketItem.class,
+            responseContainer = "List"
+    )
+    @ApiResponses({ @ApiResponse(code = 401, message = HttpStatusMessages.MESSAGE_401) })
+    public Response getItems() {
+
+        // Note: We don't explicitly check for access to (READ, /buckets) or
+        // (READ, /items ) 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();
+        }
+
+        List<BucketItem> items = registryService.getBucketItems(authorizedBucketIds);
+        if (items == null) {
+            items = Collections.emptyList();
+        }
+        permissionsService.populateItemPermissions(items);
+        linkService.populateItemLinks(items);
+
+        return Response.status(Response.Status.OK).entity(items).build();
+    }
+
+    @GET
+    @Path("{bucketId}")
+    @Consumes(MediaType.WILDCARD)
+    @Produces(MediaType.APPLICATION_JSON)
+    @ApiOperation(
+            value = "Gets items of the given bucket",
+            response = BucketItem.class,
+            responseContainer = "List",
+            nickname = "getItemsInBucket",
+            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) })
+    public Response getItems(
+            @PathParam("bucketId")
+            @ApiParam("The bucket identifier")
+            final String bucketId) {
+
+        authorizeBucketAccess(RequestAction.READ, bucketId);
+
+        final List<BucketItem> items = registryService.getBucketItems(bucketId);
+        permissionsService.populateItemPermissions(items);
+        linkService.populateItemLinks(items);
+
+        return Response.status(Response.Status.OK).entity(items).build();
+    }
+
+    @GET
+    @Path("fields")
+    @Consumes(MediaType.WILDCARD)
+    @Produces(MediaType.APPLICATION_JSON)
+    @ApiOperation(
+            value = "Retrieves the available field names for searching or sorting on bucket items.",
+            response = Fields.class
+    )
+    public Response getAvailableBucketItemFields() {
+        final Set<String> bucketFields = registryService.getBucketItemFields();
+        final Fields fields = new Fields(bucketFields);
+        return Response.status(Response.Status.OK).entity(fields).build();
+    }
+
+}

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/TenantResource.java
----------------------------------------------------------------------
diff --git a/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/api/TenantResource.java b/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/api/TenantResource.java
new file mode 100644
index 0000000..7215ccb
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/api/TenantResource.java
@@ -0,0 +1,579 @@
+/*
+ * 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.authorization.User;
+import org.apache.nifi.registry.authorization.UserGroup;
+import org.apache.nifi.registry.event.EventService;
+import org.apache.nifi.registry.exception.ResourceNotFoundException;
+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.resource.Authorizable;
+import org.apache.nifi.registry.service.AuthorizationService;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Component;
+
+import javax.servlet.http.HttpServletRequest;
+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 java.net.URI;
+import java.util.List;
+
+/**
+ * RESTful endpoints for managing tenants, ie, users and user groups.
+ */
+@Component
+@Path("tenants")
+@Api(
+        value = "tenants",
+        description = "Endpoint for managing users and user groups.",
+        authorizations = { @Authorization("Authorization") }
+)
+public class TenantResource extends AuthorizableApplicationResource {
+
+    private static final Logger logger = LoggerFactory.getLogger(TenantResource.class);
+
+    private Authorizer authorizer;
+
+    @Autowired
+    public TenantResource(AuthorizationService authorizationService, EventService eventService) {
+        super(authorizationService, eventService);
+        authorizer = authorizationService.getAuthorizer();
+    }
+
+
+    // ---------- User endpoints --------------------------------------------------------------------------------------
+
+    /**
+     * Creates a new user.
+     *
+     * @param httpServletRequest request
+     * @param requestUser the user to create
+     * @return the user that was created
+     */
+    @POST
+    @Consumes(MediaType.APPLICATION_JSON)
+    @Produces(MediaType.APPLICATION_JSON)
+    @Path("users")
+    @ApiOperation(
+            value = "Creates a user",
+            notes = NON_GUARANTEED_ENDPOINT,
+            response = User.class,
+            extensions = {
+                    @Extension(name = "access-policy", properties = {
+                            @ExtensionProperty(name = "action", value = "write"),
+                            @ExtensionProperty(name = "resource", value = "/tenants") })
+            }
+    )
+    @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 createUser(
+            @Context
+            final HttpServletRequest httpServletRequest,
+            @ApiParam(value = "The user configuration details.", required = true)
+            final User requestUser) {
+
+        verifyAuthorizerSupportsConfigurableUserGroups();
+
+        if (requestUser == null) {
+            throw new IllegalArgumentException("User details must be specified when creating a new user.");
+        }
+        if (requestUser.getIdentifier() != null) {
+            throw new IllegalArgumentException("User identifier cannot be specified when creating a new user.");
+        }
+        if (StringUtils.isBlank(requestUser.getIdentity())) {
+            throw new IllegalArgumentException("User identity must be specified when creating a new user.");
+        }
+
+        authorizeAccess(RequestAction.WRITE);
+
+        User createdUser = authorizationService.createUser(requestUser);
+
+        String locationUri = generateUserUri(createdUser);
+        return generateCreatedResponse(URI.create(locationUri), createdUser).build();
+    }
+
+    /**
+     * Retrieves all the of users in this NiFi.
+     *
+     * @return a list of users
+     */
+    @GET
+    @Consumes(MediaType.WILDCARD)
+    @Produces(MediaType.APPLICATION_JSON)
+    @Path("users")
+    @ApiOperation(
+            value = "Gets all users",
+            notes = NON_GUARANTEED_ENDPOINT,
+            response = User.class,
+            responseContainer = "List",
+            extensions = {
+                    @Extension(name = "access-policy", properties = {
+                            @ExtensionProperty(name = "action", value = "read"),
+                            @ExtensionProperty(name = "resource", value = "/tenants") })
+            }
+    )
+    @ApiResponses({
+            @ApiResponse(code = 400, message = HttpStatusMessages.MESSAGE_400),
+            @ApiResponse(code = 401, message = HttpStatusMessages.MESSAGE_401),
+            @ApiResponse(code = 403, message = HttpStatusMessages.MESSAGE_403),
+            @ApiResponse(code = 409, message = HttpStatusMessages.MESSAGE_409) })
+    public Response getUsers() {
+        verifyAuthorizerIsManaged();
+
+        authorizeAccess(RequestAction.READ);
+
+        // get all the users
+        final List<User> users = authorizationService.getUsers();
+
+        // generate the response
+        return generateOkResponse(users).build();
+    }
+
+    /**
+     * Retrieves the specified user.
+     *
+     * @param identifier The id of the user to retrieve
+     * @return An userEntity.
+     */
+    @GET
+    @Consumes(MediaType.WILDCARD)
+    @Produces(MediaType.APPLICATION_JSON)
+    @Path("users/{id}")
+    @ApiOperation(
+            value = "Gets a user",
+            notes = NON_GUARANTEED_ENDPOINT,
+            response = User.class,
+            extensions = {
+                    @Extension(name = "access-policy", properties = {
+                            @ExtensionProperty(name = "action", value = "read"),
+                            @ExtensionProperty(name = "resource", value = "/tenants") })
+            }
+    )
+    @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 getUser(
+            @ApiParam(value = "The user id.", required = true)
+            @PathParam("id") final String identifier) {
+        verifyAuthorizerIsManaged();
+        authorizeAccess(RequestAction.READ);
+
+        final User user = authorizationService.getUser(identifier);
+        if (user == null) {
+            logger.warn("The specified user id [{}] does not exist.", identifier);
+
+            throw new ResourceNotFoundException("The specified user ID does not exist in this registry.");
+        }
+        return generateOkResponse(user).build();
+    }
+
+    /**
+     * Updates a user.
+     *
+     * @param httpServletRequest request
+     * @param identifier The id of the user to update
+     * @param requestUser The user with updated fields.
+     * @return The updated user
+     */
+    @PUT
+    @Consumes(MediaType.APPLICATION_JSON)
+    @Produces(MediaType.APPLICATION_JSON)
+    @Path("users/{id}")
+    @ApiOperation(
+            value = "Updates a user",
+            notes = NON_GUARANTEED_ENDPOINT,
+            response = User.class,
+            extensions = {
+                    @Extension(name = "access-policy", properties = {
+                            @ExtensionProperty(name = "action", value = "write"),
+                            @ExtensionProperty(name = "resource", value = "/tenants") })
+            }
+    )
+    @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 updateUser(
+            @Context
+            final HttpServletRequest httpServletRequest,
+            @ApiParam(value = "The user id.", required = true)
+            @PathParam("id")
+            final String identifier,
+            @ApiParam(value = "The user configuration details.", required = true)
+            final User requestUser) {
+
+        verifyAuthorizerSupportsConfigurableUserGroups();
+        authorizeAccess(RequestAction.WRITE);
+
+        if (requestUser == null) {
+            throw new IllegalArgumentException("User details must be specified when updating a user.");
+        }
+        if (!identifier.equals(requestUser.getIdentifier())) {
+            throw new IllegalArgumentException(String.format("The user id in the request body (%s) does not equal the "
+                    + "user id of the requested resource (%s).", requestUser.getIdentifier(), identifier));
+        }
+
+        final User updatedUser = authorizationService.updateUser(requestUser);
+        if (updatedUser == null) {
+            logger.warn("The specified user id [{}] does not exist.", identifier);
+
+            throw new ResourceNotFoundException("The specified user ID does not exist in this registry.");
+        }
+
+        return generateOkResponse(updatedUser).build();
+    }
+
+    /**
+     * Removes the specified user.
+     *
+     * @param httpServletRequest request
+     * @param identifier         The id of the user to remove.
+     * @return A entity containing the client id and an updated revision.
+     */
+    @DELETE
+    @Consumes(MediaType.WILDCARD)
+    @Produces(MediaType.APPLICATION_JSON)
+    @Path("users/{id}")
+    @ApiOperation(
+            value = "Deletes a user",
+            notes = NON_GUARANTEED_ENDPOINT,
+            response = User.class,
+            extensions = {
+                    @Extension(name = "access-policy", properties = {
+                            @ExtensionProperty(name = "action", value = "delete"),
+                            @ExtensionProperty(name = "resource", value = "/tenants") })
+            }
+    )
+    @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 removeUser(
+            @Context
+            final HttpServletRequest httpServletRequest,
+            @ApiParam(value = "The user id.", required = true)
+            @PathParam("id")
+            final String identifier) {
+
+        verifyAuthorizerSupportsConfigurableUserGroups();
+        authorizeAccess(RequestAction.DELETE);
+
+        final User user = authorizationService.deleteUser(identifier);
+        if (user == null) {
+            logger.warn("The specified user id [{}] does not exist.", identifier);
+
+            throw new ResourceNotFoundException("The specified user ID does not exist in this registry.");
+        }
+        return generateOkResponse(user).build();
+    }
+
+
+    // ---------- User Group endpoints --------------------------------------------------------------------------------
+
+    /**
+     * Creates a new user group.
+     *
+     * @param httpServletRequest request
+     * @param requestUserGroup the user group to create
+     * @return the created user group
+     */
+    @POST
+    @Consumes(MediaType.APPLICATION_JSON)
+    @Produces(MediaType.APPLICATION_JSON)
+    @Path("user-groups")
+    @ApiOperation(
+            value = "Creates a user group",
+            notes = NON_GUARANTEED_ENDPOINT,
+            response = UserGroup.class,
+            extensions = {
+                    @Extension(name = "access-policy", properties = {
+                            @ExtensionProperty(name = "action", value = "write"),
+                            @ExtensionProperty(name = "resource", value = "/tenants") })
+            }
+    )
+    @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 createUserGroup(
+            @Context
+            final HttpServletRequest httpServletRequest,
+            @ApiParam(value = "The user group configuration details.", required = true)
+            final UserGroup requestUserGroup) {
+
+        verifyAuthorizerSupportsConfigurableUserGroups();
+        authorizeAccess(RequestAction.WRITE);
+
+        if (requestUserGroup == null) {
+            throw new IllegalArgumentException("User group details must be specified when creating a new group.");
+        }
+        if (requestUserGroup.getIdentifier() != null) {
+            throw new IllegalArgumentException("User group ID cannot be specified when creating a new group.");
+        }
+        if (StringUtils.isBlank(requestUserGroup.getIdentity())) {
+            throw new IllegalArgumentException("User group identity must be specified when creating a new group.");
+        }
+
+        UserGroup createdGroup = authorizationService.createUserGroup(requestUserGroup);
+
+        String locationUri = generateUserGroupUri(createdGroup);
+        return generateCreatedResponse(URI.create(locationUri), createdGroup).build();
+    }
+
+    /**
+     * Retrieves all the of user groups in this NiFi.
+     *
+     * @return a list of all user groups in this NiFi.
+     */
+    @GET
+    @Consumes(MediaType.WILDCARD)
+    @Produces(MediaType.APPLICATION_JSON)
+    @Path("user-groups")
+    @ApiOperation(
+            value = "Gets all user groups",
+            notes = NON_GUARANTEED_ENDPOINT,
+            response = UserGroup.class,
+            responseContainer = "List",
+            extensions = {
+                    @Extension(name = "access-policy", properties = {
+                            @ExtensionProperty(name = "action", value = "read"),
+                            @ExtensionProperty(name = "resource", value = "/tenants") })
+            }
+    )
+    @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 getUserGroups() {
+        verifyAuthorizerIsManaged();
+        authorizeAccess(RequestAction.READ);
+
+        final List<UserGroup> userGroups = authorizationService.getUserGroups();
+        return generateOkResponse(userGroups).build();
+    }
+
+    /**
+     * Retrieves the specified user group.
+     *
+     * @param identifier The id of the user group to retrieve
+     * @return An userGroupEntity.
+     */
+    @GET
+    @Consumes(MediaType.WILDCARD)
+    @Produces(MediaType.APPLICATION_JSON)
+    @Path("user-groups/{id}")
+    @ApiOperation(
+            value = "Gets a user group",
+            notes = NON_GUARANTEED_ENDPOINT,
+            response = UserGroup.class,
+            extensions = {
+                    @Extension(name = "access-policy", properties = {
+                            @ExtensionProperty(name = "action", value = "read"),
+                            @ExtensionProperty(name = "resource", value = "/tenants") })
+            }
+    )
+    @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 getUserGroup(
+            @ApiParam(value = "The user group id.", required = true)
+            @PathParam("id") final String identifier) {
+        verifyAuthorizerIsManaged();
+        authorizeAccess(RequestAction.READ);
+
+        final UserGroup userGroup = authorizationService.getUserGroup(identifier);
+        if (userGroup == null) {
+            logger.warn("The specified user group id [{}] does not exist.", identifier);
+
+            throw new ResourceNotFoundException("The specified user group ID does not exist in this registry.");
+        }
+
+        return generateOkResponse(userGroup).build();
+    }
+
+    /**
+     * Updates a user group.
+     *
+     * @param httpServletRequest request
+     * @param identifier The id of the user group to update.
+     * @param requestUserGroup The user group with updated fields.
+     * @return The resulting, updated user group.
+     */
+    @PUT
+    @Consumes(MediaType.APPLICATION_JSON)
+    @Produces(MediaType.APPLICATION_JSON)
+    @Path("user-groups/{id}")
+    @ApiOperation(
+            value = "Updates a user group",
+            notes = NON_GUARANTEED_ENDPOINT,
+            response = UserGroup.class,
+            extensions = {
+                    @Extension(name = "access-policy", properties = {
+                            @ExtensionProperty(name = "action", value = "write"),
+                            @ExtensionProperty(name = "resource", value = "/tenants") })
+            }
+    )
+    @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 updateUserGroup(
+            @Context
+            final HttpServletRequest httpServletRequest,
+            @ApiParam(value = "The user group id.", required = true)
+            @PathParam("id")
+            final String identifier,
+            @ApiParam(value = "The user group configuration details.", required = true)
+            final UserGroup requestUserGroup) {
+
+        verifyAuthorizerSupportsConfigurableUserGroups();
+
+        if (requestUserGroup == null) {
+            throw new IllegalArgumentException("User group details must be specified to update a user group.");
+        }
+        if (!identifier.equals(requestUserGroup.getIdentifier())) {
+            throw new IllegalArgumentException(String.format("The user group id in the request body (%s) does not equal the "
+                    + "user group id of the requested resource (%s).", requestUserGroup.getIdentifier(), identifier));
+        }
+
+        authorizeAccess(RequestAction.WRITE);
+
+        UserGroup updatedUserGroup = authorizationService.updateUserGroup(requestUserGroup);
+        if (updatedUserGroup == null) {
+            logger.warn("The specified user group id [{}] does not exist.", identifier);
+
+            throw new ResourceNotFoundException("The specified user group ID does not exist in this registry.");
+        }
+
+        return generateOkResponse(updatedUserGroup).build();
+    }
+
+    /**
+     * Removes the specified user group.
+     *
+     * @param httpServletRequest request
+     * @param identifier                 The id of the user group to remove.
+     * @return The deleted user group.
+     */
+    @DELETE
+    @Consumes(MediaType.WILDCARD)
+    @Produces(MediaType.APPLICATION_JSON)
+    @Path("user-groups/{id}")
+    @ApiOperation(
+            value = "Deletes a user group",
+            notes = NON_GUARANTEED_ENDPOINT,
+            response = UserGroup.class,
+            extensions = {
+                    @Extension(name = "access-policy", properties = {
+                            @ExtensionProperty(name = "action", value = "delete"),
+                            @ExtensionProperty(name = "resource", value = "/tenants") })
+            }
+    )
+    @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 removeUserGroup(
+            @Context
+            final HttpServletRequest httpServletRequest,
+            @ApiParam(value = "The user group id.", required = true)
+            @PathParam("id")
+            final String identifier) {
+        verifyAuthorizerSupportsConfigurableUserGroups();
+        authorizeAccess(RequestAction.DELETE);
+
+        final UserGroup userGroup = authorizationService.deleteUserGroup(identifier);
+        if (userGroup == null) {
+            logger.warn("The specified user group id [{}] does not exist.", identifier);
+
+            throw new ResourceNotFoundException("The specified user group ID does not exist in this registry.");
+        }
+
+        return generateOkResponse(userGroup).build();
+    }
+
+
+    private void verifyAuthorizerIsManaged() {
+        if (!AuthorizerCapabilityDetection.isManagedAuthorizer(authorizer)) {
+            throw new IllegalStateException(AuthorizationService.MSG_NON_MANAGED_AUTHORIZER);
+        }
+    }
+
+    private void verifyAuthorizerSupportsConfigurableUserGroups() {
+        if (!AuthorizerCapabilityDetection.isConfigurableUserGroupProvider(authorizer)) {
+            throw new IllegalStateException(AuthorizationService.MSG_NON_CONFIGURABLE_USERS);
+        }
+    }
+
+    private void authorizeAccess(RequestAction actionType) {
+        final Authorizable tenantsAuthorizable = authorizableLookup.getTenantsAuthorizable();
+        authorizationService.authorize(tenantsAuthorizable, actionType);
+    }
+
+    private String generateUserUri(final User user) {
+        return generateResourceUri("tenants", "users", user.getIdentifier());
+    }
+
+    private String generateUserGroupUri(final UserGroup userGroup) {
+        return generateResourceUri("tenants", "user-groups", userGroup.getIdentifier());
+    }
+
+}

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/exception/UnauthorizedException.java
----------------------------------------------------------------------
diff --git a/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/exception/UnauthorizedException.java b/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/exception/UnauthorizedException.java
new file mode 100644
index 0000000..46e6fc9
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/exception/UnauthorizedException.java
@@ -0,0 +1,74 @@
+/*
+ * 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.exception;
+
+import org.apache.commons.lang3.StringUtils;
+import org.apache.nifi.registry.security.authentication.IdentityProviderUsage;
+
+import java.util.List;
+
+/**
+ * An exception for a convenient way to create a 401 Unauthorized response
+ * using an exception mapper
+ */
+public class UnauthorizedException extends RuntimeException {
+
+    private String[] wwwAuthenticateChallenge;
+
+    public UnauthorizedException() {
+    }
+
+    public UnauthorizedException(String message) {
+        super(message);
+    }
+
+    public UnauthorizedException(String message, Throwable cause) {
+        super(message, cause);
+    }
+
+    public UnauthorizedException(Throwable cause) {
+        super(cause);
+    }
+
+    public UnauthorizedException withAuthenticateChallenge(IdentityProviderUsage.AuthType authType) {
+        wwwAuthenticateChallenge = new String[] { authType.getHttpAuthScheme() };
+        return this;
+    }
+
+    public UnauthorizedException withAuthenticateChallenge(List<IdentityProviderUsage.AuthType> authTypes) {
+        wwwAuthenticateChallenge = new String[authTypes.size()];
+        for (int i = 0; i < authTypes.size(); i++) {
+            wwwAuthenticateChallenge[i] = authTypes.get(i).getHttpAuthScheme();
+        }
+        return this;
+    }
+
+    public UnauthorizedException withAuthenticateChallenge(String authType) {
+        wwwAuthenticateChallenge = new String[] { authType };
+        return this;
+    }
+
+    public UnauthorizedException withAuthenticateChallenge(String[] authTypes) {
+        wwwAuthenticateChallenge = authTypes;
+        return this;
+    }
+
+    public String getWwwAuthenticateChallenge() {
+        return StringUtils.join(wwwAuthenticateChallenge, ",");
+    }
+
+}

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/link/LinkService.java
----------------------------------------------------------------------
diff --git a/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/link/LinkService.java b/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/link/LinkService.java
new file mode 100644
index 0000000..19e2168
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/link/LinkService.java
@@ -0,0 +1,110 @@
+/*
+ * 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.link;
+
+import org.apache.nifi.registry.bucket.Bucket;
+import org.apache.nifi.registry.bucket.BucketItem;
+import org.apache.nifi.registry.flow.VersionedFlow;
+import org.apache.nifi.registry.flow.VersionedFlowSnapshotMetadata;
+import org.apache.nifi.registry.web.link.builder.BucketLinkBuilder;
+import org.apache.nifi.registry.web.link.builder.LinkBuilder;
+import org.apache.nifi.registry.web.link.builder.VersionedFlowLinkBuilder;
+import org.apache.nifi.registry.web.link.builder.VersionedFlowSnapshotLinkBuilder;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.stereotype.Service;
+
+import javax.ws.rs.core.Link;
+
+@Service
+public class LinkService {
+
+    private static final Logger LOGGER = LoggerFactory.getLogger(LinkService.class);
+
+    private final LinkBuilder<Bucket> bucketLinkBuilder = new BucketLinkBuilder();
+
+    private final LinkBuilder<VersionedFlow> versionedFlowLinkBuilder = new VersionedFlowLinkBuilder();
+
+    private final LinkBuilder<VersionedFlowSnapshotMetadata> snapshotMetadataLinkBuilder = new VersionedFlowSnapshotLinkBuilder();
+
+    // ---- Bucket Links
+
+    public void populateBucketLinks(final Iterable<Bucket> buckets) {
+        if (buckets == null) {
+            return;
+        }
+
+        buckets.forEach(b -> populateBucketLinks(b));
+    }
+
+    public void populateBucketLinks(final Bucket bucket) {
+        final Link bucketLink = bucketLinkBuilder.createLink(bucket);
+        bucket.setLink(bucketLink);
+    }
+
+    // ---- Flow Links
+
+    public void populateFlowLinks(final Iterable<VersionedFlow> versionedFlows) {
+        if (versionedFlows == null) {
+            return;
+        }
+
+        versionedFlows.forEach(f  -> populateFlowLinks(f));
+    }
+
+    public void populateFlowLinks(final VersionedFlow versionedFlow) {
+        final Link flowLink = versionedFlowLinkBuilder.createLink(versionedFlow);
+        versionedFlow.setLink(flowLink);
+    }
+
+    // ---- Flow Snapshot Links
+
+    public void populateSnapshotLinks(final Iterable<VersionedFlowSnapshotMetadata> snapshotMetadatas) {
+        if (snapshotMetadatas == null) {
+            return;
+        }
+
+        snapshotMetadatas.forEach(s -> populateSnapshotLinks(s));
+    }
+
+    public void populateSnapshotLinks(final VersionedFlowSnapshotMetadata snapshotMetadata) {
+        final Link snapshotLink = snapshotMetadataLinkBuilder.createLink(snapshotMetadata);
+        snapshotMetadata.setLink(snapshotLink);
+    }
+
+    // ---- BucketItem Links
+
+    public void populateItemLinks(final Iterable<BucketItem> items) {
+        if (items == null) {
+            return;
+        }
+
+        items.forEach(i -> populateItemLinks(i));
+    }
+
+    public void populateItemLinks(final BucketItem bucketItem) {
+        if (bucketItem == null) {
+            return;
+        }
+
+        if (bucketItem instanceof VersionedFlow) {
+            populateFlowLinks((VersionedFlow)bucketItem);
+        } else {
+            LOGGER.error("Unable to create link for BucketItem with type: " + bucketItem.getClass().getCanonicalName());
+        }
+    }
+}

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/link/builder/BucketLinkBuilder.java
----------------------------------------------------------------------
diff --git a/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/link/builder/BucketLinkBuilder.java b/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/link/builder/BucketLinkBuilder.java
new file mode 100644
index 0000000..f0409c7
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/link/builder/BucketLinkBuilder.java
@@ -0,0 +1,45 @@
+/*
+ * 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.link.builder;
+
+import org.apache.nifi.registry.bucket.Bucket;
+
+import javax.ws.rs.core.Link;
+import javax.ws.rs.core.UriBuilder;
+import java.net.URI;
+
+/**
+ * LinkBuilder that builds "self" links for Buckets.
+ */
+public class BucketLinkBuilder implements LinkBuilder<Bucket> {
+
+    private static final String PATH = "buckets/{id}";
+
+    @Override
+    public Link createLink(final Bucket bucket) {
+        if (bucket == null) {
+            return null;
+        }
+
+        final URI uri = UriBuilder.fromPath(PATH)
+                .resolveTemplate("id", bucket.getIdentifier())
+                .build();
+
+        return Link.fromUri(uri).rel("self").build();
+    }
+
+}

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/link/builder/LinkBuilder.java
----------------------------------------------------------------------
diff --git a/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/link/builder/LinkBuilder.java b/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/link/builder/LinkBuilder.java
new file mode 100644
index 0000000..ec356fd
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/link/builder/LinkBuilder.java
@@ -0,0 +1,30 @@
+/*
+ * 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.link.builder;
+
+import javax.ws.rs.core.Link;
+
+/**
+ * Creates a Link for a given type.
+ *
+ * @param <T> the type to create a link for
+ */
+public interface LinkBuilder<T> {
+
+    Link createLink(T t);
+
+}

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/link/builder/VersionedFlowLinkBuilder.java
----------------------------------------------------------------------
diff --git a/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/link/builder/VersionedFlowLinkBuilder.java b/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/link/builder/VersionedFlowLinkBuilder.java
new file mode 100644
index 0000000..38d3d0e
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/link/builder/VersionedFlowLinkBuilder.java
@@ -0,0 +1,46 @@
+/*
+ * 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.link.builder;
+
+import org.apache.nifi.registry.flow.VersionedFlow;
+
+import javax.ws.rs.core.Link;
+import javax.ws.rs.core.UriBuilder;
+import java.net.URI;
+
+/**
+ * LinkBuilder that builds "self" links for VersionedFlows.
+ */
+public class VersionedFlowLinkBuilder implements LinkBuilder<VersionedFlow> {
+
+    private static final String PATH = "buckets/{bucketId}/flows/{flowId}";
+
+    @Override
+    public Link createLink(final VersionedFlow versionedFlow) {
+        if (versionedFlow == null) {
+            return null;
+        }
+
+        final URI uri = UriBuilder.fromPath(PATH)
+                .resolveTemplate("bucketId", versionedFlow.getBucketIdentifier())
+                .resolveTemplate("flowId", versionedFlow.getIdentifier())
+                .build();
+
+        return Link.fromUri(uri).rel("self").build();
+    }
+
+}

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/link/builder/VersionedFlowSnapshotLinkBuilder.java
----------------------------------------------------------------------
diff --git a/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/link/builder/VersionedFlowSnapshotLinkBuilder.java b/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/link/builder/VersionedFlowSnapshotLinkBuilder.java
new file mode 100644
index 0000000..4085c6d
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/link/builder/VersionedFlowSnapshotLinkBuilder.java
@@ -0,0 +1,47 @@
+/*
+ * 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.link.builder;
+
+import org.apache.nifi.registry.flow.VersionedFlowSnapshotMetadata;
+
+import javax.ws.rs.core.Link;
+import javax.ws.rs.core.UriBuilder;
+import java.net.URI;
+
+/**
+ * LinkBuilder that builds "self" links for VersionedFlowSnapshotMetadata.
+ */
+public class VersionedFlowSnapshotLinkBuilder implements LinkBuilder<VersionedFlowSnapshotMetadata> {
+
+    private static final String PATH = "buckets/{bucketId}/flows/{flowId}/versions/{versionNumber}";
+
+    @Override
+    public Link createLink(final VersionedFlowSnapshotMetadata snapshotMetadata) {
+        if (snapshotMetadata == null) {
+            return null;
+        }
+
+        final URI uri = UriBuilder.fromPath(PATH)
+                .resolveTemplate("bucketId", snapshotMetadata.getBucketIdentifier())
+                .resolveTemplate("flowId", snapshotMetadata.getFlowIdentifier())
+                .resolveTemplate("versionNumber", snapshotMetadata.getVersion())
+                .build();
+
+        return Link.fromUri(uri).rel("content").build();
+    }
+
+}

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/mapper/AccessDeniedExceptionMapper.java
----------------------------------------------------------------------
diff --git a/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/mapper/AccessDeniedExceptionMapper.java b/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/mapper/AccessDeniedExceptionMapper.java
new file mode 100644
index 0000000..5b9e3ee
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/mapper/AccessDeniedExceptionMapper.java
@@ -0,0 +1,75 @@
+/*
+ * 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.mapper;
+
+import org.apache.commons.lang3.StringUtils;
+import org.apache.nifi.registry.security.authorization.exception.AccessDeniedException;
+import org.apache.nifi.registry.security.authorization.user.NiFiUser;
+import org.apache.nifi.registry.security.authorization.user.NiFiUserUtils;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.stereotype.Component;
+
+import javax.ws.rs.core.Response;
+import javax.ws.rs.core.Response.Status;
+import javax.ws.rs.ext.ExceptionMapper;
+import javax.ws.rs.ext.Provider;
+
+/**
+ * Maps access denied exceptions into a client response.
+ */
+@Component
+@Provider
+public class AccessDeniedExceptionMapper implements ExceptionMapper<AccessDeniedException> {
+
+    private static final Logger logger = LoggerFactory.getLogger(AccessDeniedExceptionMapper.class);
+
+    @Override
+    public Response toResponse(AccessDeniedException exception) {
+        // get the current user
+        NiFiUser user = NiFiUserUtils.getNiFiUser();
+
+        // if the user was authenticated - forbidden, otherwise unauthorized... the user may be null if the
+        // AccessDeniedException was thrown from a /access endpoint that isn't subject to the security
+        // filter chain. for instance, one that performs kerberos negotiation
+        final Status status;
+        if (user == null || user.isAnonymous()) {
+            status = Status.UNAUTHORIZED;
+        } else {
+            status = Status.FORBIDDEN;
+        }
+
+        final String identity;
+        if (user == null) {
+            identity = "<no user found>";
+        } else {
+            identity = user.toString();
+        }
+
+        logger.info(String.format("%s does not have permission to access the requested resource. %s Returning %s response.", identity, exception.getMessage(), status));
+
+        if (logger.isDebugEnabled()) {
+            logger.debug(StringUtils.EMPTY, exception);
+        }
+
+        return Response.status(status)
+                .entity(String.format("%s Contact the system administrator.", exception.getMessage()))
+                .type("text/plain")
+                .build();
+    }
+
+}

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/mapper/AdministrationExceptionMapper.java
----------------------------------------------------------------------
diff --git a/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/mapper/AdministrationExceptionMapper.java b/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/mapper/AdministrationExceptionMapper.java
new file mode 100644
index 0000000..b97222c
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/mapper/AdministrationExceptionMapper.java
@@ -0,0 +1,46 @@
+/*
+ * 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.mapper;
+
+import org.apache.nifi.registry.exception.AdministrationException;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.stereotype.Component;
+
+import javax.ws.rs.core.Response;
+import javax.ws.rs.ext.ExceptionMapper;
+import javax.ws.rs.ext.Provider;
+
+/**
+ * Maps administration exceptions into client responses.
+ */
+@Component
+@Provider
+public class AdministrationExceptionMapper implements ExceptionMapper<AdministrationException> {
+
+    private static final Logger logger = LoggerFactory.getLogger(AdministrationExceptionMapper.class);
+
+    @Override
+    public Response toResponse(AdministrationException exception) {
+        // log the error
+        logger.error(String.format("%s. Returning %s response.", exception, Response.Status.INTERNAL_SERVER_ERROR), exception);
+
+        // generate the response
+        return Response.status(Response.Status.INTERNAL_SERVER_ERROR).entity(exception.getMessage()).type("text/plain").build();
+    }
+
+}

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/mapper/AuthenticationCredentialsNotFoundExceptionMapper.java
----------------------------------------------------------------------
diff --git a/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/mapper/AuthenticationCredentialsNotFoundExceptionMapper.java b/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/mapper/AuthenticationCredentialsNotFoundExceptionMapper.java
new file mode 100644
index 0000000..ee7fb74
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/mapper/AuthenticationCredentialsNotFoundExceptionMapper.java
@@ -0,0 +1,50 @@
+/*
+ * 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.mapper;
+
+import org.apache.commons.lang3.StringUtils;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.security.authentication.AuthenticationCredentialsNotFoundException;
+import org.springframework.stereotype.Component;
+
+import javax.ws.rs.core.Response;
+import javax.ws.rs.ext.ExceptionMapper;
+import javax.ws.rs.ext.Provider;
+
+/**
+ * Maps exceptions that occur because no valid credentials were found into the corresponding response.
+ */
+@Component
+@Provider
+public class AuthenticationCredentialsNotFoundExceptionMapper implements ExceptionMapper<AuthenticationCredentialsNotFoundException> {
+
+    private static final Logger logger = LoggerFactory.getLogger(AuthenticationCredentialsNotFoundExceptionMapper.class);
+
+    @Override
+    public Response toResponse(AuthenticationCredentialsNotFoundException exception) {
+        // log the error
+        logger.info(String.format("No valid credentials were found in the request: %s. Returning %s response.", exception, Response.Status.FORBIDDEN));
+
+        if (logger.isDebugEnabled()) {
+            logger.debug(StringUtils.EMPTY, exception);
+        }
+
+        return Response.status(Response.Status.FORBIDDEN).entity("Access is denied.").type("text/plain").build();
+    }
+
+}

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/mapper/AuthorizationAccessExceptionMapper.java
----------------------------------------------------------------------
diff --git a/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/mapper/AuthorizationAccessExceptionMapper.java b/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/mapper/AuthorizationAccessExceptionMapper.java
new file mode 100644
index 0000000..ff4b7ec
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/mapper/AuthorizationAccessExceptionMapper.java
@@ -0,0 +1,46 @@
+/*
+ * 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.mapper;
+
+import org.apache.nifi.registry.security.authorization.exception.AuthorizationAccessException;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.stereotype.Component;
+
+import javax.ws.rs.core.Response;
+import javax.ws.rs.ext.ExceptionMapper;
+import javax.ws.rs.ext.Provider;
+
+/**
+ * Maps authorization access exceptions into client responses.
+ */
+@Component
+@Provider
+public class AuthorizationAccessExceptionMapper implements ExceptionMapper<AuthorizationAccessException> {
+
+    private static final Logger logger = LoggerFactory.getLogger(AuthorizationAccessExceptionMapper.class);
+
+    @Override
+    public Response toResponse(AuthorizationAccessException e) {
+        // log the error
+        logger.error(String.format("%s. Returning %s response.", e, Response.Status.INTERNAL_SERVER_ERROR), e);
+
+        // generate the response
+        return Response.status(Response.Status.INTERNAL_SERVER_ERROR).entity(e.getMessage()).type("text/plain").build();
+    }
+
+}

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/mapper/BadRequestExceptionMapper.java
----------------------------------------------------------------------
diff --git a/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/mapper/BadRequestExceptionMapper.java b/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/mapper/BadRequestExceptionMapper.java
new file mode 100644
index 0000000..2577fa0
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/mapper/BadRequestExceptionMapper.java
@@ -0,0 +1,49 @@
+/*
+ * 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.mapper;
+
+import org.apache.commons.lang3.StringUtils;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.stereotype.Component;
+
+import javax.ws.rs.BadRequestException;
+import javax.ws.rs.core.Response;
+import javax.ws.rs.ext.ExceptionMapper;
+import javax.ws.rs.ext.Provider;
+
+/**
+ * Maps exceptions into client responses.
+ */
+@Component
+@Provider
+public class BadRequestExceptionMapper implements ExceptionMapper<BadRequestException> {
+
+    private static final Logger logger = LoggerFactory.getLogger(BadRequestExceptionMapper.class);
+
+    @Override
+    public Response toResponse(BadRequestException exception) {
+        logger.info(String.format("%s. Returning %s response.", exception, Response.Status.BAD_REQUEST));
+
+        if (logger.isDebugEnabled()) {
+            logger.debug(StringUtils.EMPTY, exception);
+        }
+
+        return Response.status(Response.Status.BAD_REQUEST).entity(exception.getMessage()).type("text/plain").build();
+    }
+
+}
\ No newline at end of file

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/mapper/ConstraintViolationExceptionMapper.java
----------------------------------------------------------------------
diff --git a/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/mapper/ConstraintViolationExceptionMapper.java b/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/mapper/ConstraintViolationExceptionMapper.java
new file mode 100644
index 0000000..b691775
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/mapper/ConstraintViolationExceptionMapper.java
@@ -0,0 +1,68 @@
+/*
+ * 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.mapper;
+
+import org.apache.commons.lang3.StringUtils;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.stereotype.Component;
+
+import javax.validation.ConstraintViolation;
+import javax.validation.ConstraintViolationException;
+import javax.validation.Path;
+import javax.ws.rs.core.Response;
+import javax.ws.rs.ext.ExceptionMapper;
+import javax.ws.rs.ext.Provider;
+
+@Component
+@Provider
+public class ConstraintViolationExceptionMapper implements ExceptionMapper<ConstraintViolationException> {
+
+    private static final Logger logger = LoggerFactory.getLogger(ConstraintViolationExceptionMapper.class);
+
+    @Override
+    public Response toResponse(ConstraintViolationException exception) {
+        logger.info(String.format("%s. Returning %s response.", exception, Response.Status.BAD_REQUEST));
+
+        if (logger.isDebugEnabled()) {
+            logger.debug(StringUtils.EMPTY, exception);
+        }
+
+        // start with the overall message which will be something like "Cannot create xyz"
+        final StringBuilder errorMessage = new StringBuilder(exception.getMessage()).append(" - ");
+
+        boolean first = true;
+        for (final ConstraintViolation violation : exception.getConstraintViolations()) {
+            if (!first) {
+                errorMessage.append(", ");
+            }
+            first = false;
+
+            // lastNode should end up as the field that failed validation
+            Path.Node lastNode = null;
+            for (final Path.Node node : violation.getPropertyPath()) {
+                lastNode = node;
+            }
+
+            // append something like "xyz must not be..."
+            errorMessage.append(lastNode.getName()).append(" ").append(violation.getMessage());
+        }
+
+        return Response.status(Response.Status.BAD_REQUEST).entity(errorMessage.toString()).type("text/plain").build();
+    }
+
+}

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/mapper/IllegalArgumentExceptionMapper.java
----------------------------------------------------------------------
diff --git a/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/mapper/IllegalArgumentExceptionMapper.java b/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/mapper/IllegalArgumentExceptionMapper.java
new file mode 100644
index 0000000..7186c0f
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/mapper/IllegalArgumentExceptionMapper.java
@@ -0,0 +1,48 @@
+/*
+ * 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.mapper;
+
+import javax.ws.rs.core.Response;
+import javax.ws.rs.core.Response.Status;
+import javax.ws.rs.ext.ExceptionMapper;
+import javax.ws.rs.ext.Provider;
+import org.apache.commons.lang3.StringUtils;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.stereotype.Component;
+
+/**
+ * Maps exceptions into client responses.
+ */
+@Component
+@Provider
+public class IllegalArgumentExceptionMapper implements ExceptionMapper<IllegalArgumentException> {
+
+    private static final Logger logger = LoggerFactory.getLogger(IllegalArgumentExceptionMapper.class);
+
+    @Override
+    public Response toResponse(IllegalArgumentException exception) {
+        logger.info(String.format("%s. Returning %s response.", exception, Response.Status.BAD_REQUEST));
+
+        if (logger.isDebugEnabled()) {
+            logger.debug(StringUtils.EMPTY, exception);
+        }
+
+        return Response.status(Status.BAD_REQUEST).entity(exception.getMessage()).type("text/plain").build();
+    }
+
+}


[10/51] [partial] nifi-registry git commit: NIFIREG-201 Refactoring project structure to better isolate extensions

Posted by kd...@apache.org.
http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/6f26290d/nifi-registry-core/nifi-registry-web-api/src/test/resources/conf/secure-file/authorizers.xml
----------------------------------------------------------------------
diff --git a/nifi-registry-core/nifi-registry-web-api/src/test/resources/conf/secure-file/authorizers.xml b/nifi-registry-core/nifi-registry-web-api/src/test/resources/conf/secure-file/authorizers.xml
new file mode 100644
index 0000000..40911b2
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-web-api/src/test/resources/conf/secure-file/authorizers.xml
@@ -0,0 +1,143 @@
+<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
+<!--
+  ~ 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.
+  -->
+<!--
+    This file lists the userGroupProviders, accessPolicyProviders, and authorizers to use when running securely. In order
+    to use a specific authorizer it must be configured here and its identifier must be specified in the nifi-registry.properties file.
+    If the authorizer is a managedAuthorizer, it may need to be configured with an accessPolicyProvider and an userGroupProvider.
+    This file allows for configuration of them, but they must be configured in order:
+
+    ...
+    all userGroupProviders
+    all accessPolicyProviders
+    all Authorizers
+    ...
+-->
+<authorizers>
+
+    <!--
+        The FileUserGroupProvider will provide support for managing users and groups which is backed by a file
+        on the local file system.
+
+        - Users File - The file where the FileUserGroupProvider will store users and groups.
+
+        - Initial User Identity [unique key] - The identity of a users and systems to seed the Users File. The name of
+            each property must be unique, for example: "Initial User Identity A", "Initial User Identity B",
+            "Initial User Identity C" or "Initial User Identity 1", "Initial User Identity 2", "Initial User Identity 3"
+
+            NOTE: Any identity mapping rules specified in nifi-registry.properties will also be applied to the user identities,
+            so the values should be the unmapped identities (i.e. full DN from a certificate).
+    -->
+    <userGroupProvider>
+        <identifier>file-user-group-provider</identifier>
+        <class>org.apache.nifi.registry.security.authorization.file.FileUserGroupProvider</class>
+        <property name="Users File">./target/test-classes/conf/secure-file/users.xml</property>
+        <property name="Initial User Identity 1">CN=user1, OU=nifi</property>
+        <property name="Initial User Identity 2">CN=no-access, OU=nifi</property>
+    </userGroupProvider>
+
+    <!--
+        The CompositeUserGroupProvider will provide support for retrieving users and groups from multiple sources.
+
+        - User Group Provider [unique key] - The identifier of user group providers to load from. The name of
+            each property must be unique, for example: "User Group Provider A", "User Group Provider B",
+            "User Group Provider C" or "User Group Provider 1", "User Group Provider 2", "User Group Provider 3"
+
+            NOTE: Any identity mapping rules specified in nifi-registry.properties are not applied in this implementation. This behavior
+            would need to be applied by the base implementation.
+    -->
+    <!-- To enable the composite-user-group-provider remove 2 lines. This is 1 of 2.
+    <userGroupProvider>
+        <identifier>composite-user-group-provider</identifier>
+        <class>org.apache.nifi.registry.security.authorization.CompositeUserGroupProvider</class>
+        <property name="User Group Provider 1"></property>
+    </userGroupProvider>
+    To enable the composite-user-group-provider remove 2 lines. This is 2 of 2. -->
+
+    <!--
+        The CompositeConfigurableUserGroupProvider will provide support for retrieving users and groups from multiple sources.
+        Additionally, a single configurable user group provider is required. Users from the configurable user group provider
+        are configurable, however users loaded from one of the User Group Provider [unique key] will not be.
+
+        - Configurable User Group Provider - A configurable user group provider.
+
+        - User Group Provider [unique key] - The identifier of user group providers to load from. The name of
+            each property must be unique, for example: "User Group Provider A", "User Group Provider B",
+            "User Group Provider C" or "User Group Provider 1", "User Group Provider 2", "User Group Provider 3"
+
+            NOTE: Any identity mapping rules specified in nifi-registry.properties are not applied in this implementation. This behavior
+            would need to be applied by the base implementation.
+    -->
+    <!-- To enable the composite-configurable-user-group-provider remove 2 lines. This is 1 of 2.
+    <userGroupProvider>
+        <identifier>composite-configurable-user-group-provider</identifier>
+        <class>org.apache.nifi.registry.security.authorization.CompositeConfigurableUserGroupProvider</class>
+        <property name="Configurable User Group Provider">file-user-group-provider</property>
+        <property name="User Group Provider 1"></property>
+    </userGroupProvider>
+    To enable the composite-configurable-user-group-provider remove 2 lines. This is 2 of 2. -->
+
+    <!--
+        The FileAccessPolicyProvider will provide support for managing access policies which is backed by a file
+        on the local file system.
+
+        - User Group Provider - The identifier for an User Group Provider defined above that will be used to access
+            users and groups for use in the managed access policies.
+
+        - Authorizations File - The file where the FileAccessPolicyProvider will store policies.
+
+        - Initial Admin Identity - The identity of an initial admin user that will be granted access to the UI and
+            given the ability to create additional users, groups, and policies. The value of this property could be
+            a DN when using certificates or LDAP. This property will only be used when there
+            are no other policies defined.
+
+            NOTE: Any identity mapping rules specified in nifi-registry.properties will also be applied to the initial admin identity,
+            so the value should be the unmapped identity. This identity must be found in the configured User Group Provider.
+
+        - NiFi Identity [unique key] - The identity of a NiFi node that will have access to this NiFi Registry and will be able
+            to act as a proxy on behalf of a NiFi Registry end user. A property should be created for the identity of every NiFi
+            node that needs to access this NiFi Registry.
+
+            NOTE: Any identity mapping rules specified in nifi-registry.properties will also be applied to the nifi identities,
+            so the values should be the unmapped identities (i.e. full DN from a certificate). This identity must be found
+            in the configured User Group Provider.
+    -->
+    <accessPolicyProvider>
+        <identifier>file-access-policy-provider</identifier>
+        <class>org.apache.nifi.registry.security.authorization.file.FileAccessPolicyProvider</class>
+        <property name="User Group Provider">file-user-group-provider</property>
+        <property name="Authorizations File">./target/test-classes/conf/secure-file/authorizations.xml</property>
+        <property name="Initial Admin Identity">CN=user1, OU=nifi</property>
+
+        <!--<property name="NiFi Identity 1"></property>-->
+    </accessPolicyProvider>
+
+    <!--
+        The StandardManagedAuthorizer. This authorizer implementation must be configured with the
+        Access Policy Provider which it will use to access and manage users, groups, and policies.
+        These users, groups, and policies will be used to make all access decisions during authorization
+        requests.
+
+        - Access Policy Provider - The identifier for an Access Policy Provider defined above.
+    -->
+    <authorizer>
+        <identifier>managed-authorizer</identifier>
+        <class>org.apache.nifi.registry.security.authorization.StandardManagedAuthorizer</class>
+        <property name="Access Policy Provider">file-access-policy-provider</property>
+    </authorizer>
+
+</authorizers>
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/6f26290d/nifi-registry-core/nifi-registry-web-api/src/test/resources/conf/secure-file/nifi-registry-client.properties
----------------------------------------------------------------------
diff --git a/nifi-registry-core/nifi-registry-web-api/src/test/resources/conf/secure-file/nifi-registry-client.properties b/nifi-registry-core/nifi-registry-web-api/src/test/resources/conf/secure-file/nifi-registry-client.properties
new file mode 100644
index 0000000..8eb6b56
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-web-api/src/test/resources/conf/secure-file/nifi-registry-client.properties
@@ -0,0 +1,25 @@
+#
+# 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.
+#
+
+# client security properties #
+nifi.registry.security.keystore=./target/test-classes/keys/client-ks.jks
+nifi.registry.security.keystoreType=JKS
+nifi.registry.security.keystorePasswd=clientKeystorePassword
+nifi.registry.security.keyPasswd=u1Pass
+nifi.registry.security.truststore=./target/test-classes/keys/localhost-ts.jks
+nifi.registry.security.truststoreType=JKS
+nifi.registry.security.truststorePasswd=localhostTruststorePassword

http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/6f26290d/nifi-registry-core/nifi-registry-web-api/src/test/resources/conf/secure-file/nifi-registry.properties
----------------------------------------------------------------------
diff --git a/nifi-registry-core/nifi-registry-web-api/src/test/resources/conf/secure-file/nifi-registry.properties b/nifi-registry-core/nifi-registry-web-api/src/test/resources/conf/secure-file/nifi-registry.properties
new file mode 100644
index 0000000..408f44d
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-web-api/src/test/resources/conf/secure-file/nifi-registry.properties
@@ -0,0 +1,30 @@
+#
+# 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.
+#
+
+# web properties #
+nifi.registry.web.https.host=localhost
+nifi.registry.web.https.port=0
+
+# security properties #
+#
+# ** Server KeyStore and TrustStore configuration set in Spring profile properties for embedded Jetty **
+#
+nifi.registry.security.authorizers.configuration.file=./target/test-classes/conf/secure-file/authorizers.xml
+nifi.registry.security.authorizer=managed-authorizer
+
+# providers properties #
+nifi.registry.providers.configuration.file=./target/test-classes/conf/providers.xml

http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/6f26290d/nifi-registry-core/nifi-registry-web-api/src/test/resources/conf/secure-kerberos/authorizers.xml
----------------------------------------------------------------------
diff --git a/nifi-registry-core/nifi-registry-web-api/src/test/resources/conf/secure-kerberos/authorizers.xml b/nifi-registry-core/nifi-registry-web-api/src/test/resources/conf/secure-kerberos/authorizers.xml
new file mode 100644
index 0000000..d548696
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-web-api/src/test/resources/conf/secure-kerberos/authorizers.xml
@@ -0,0 +1,101 @@
+<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
+<!--
+  ~ 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.
+  -->
+<!--
+    This file lists the userGroupProviders, accessPolicyProviders, and authorizers to use when running securely. In order
+    to use a specific authorizer it must be configured here and its identifier must be specified in the nifi-registry.properties file.
+    If the authorizer is a managedAuthorizer, it may need to be configured with an accessPolicyProvider and an userGroupProvider.
+    This file allows for configuration of them, but they must be configured in order:
+
+    ...
+    all userGroupProviders
+    all accessPolicyProviders
+    all Authorizers
+    ...
+-->
+<authorizers>
+
+    <!--
+        The FileUserGroupProvider will provide support for managing users and groups which is backed by a file
+        on the local file system.
+
+        - Users File - The file where the FileUserGroupProvider will store users and groups.
+
+        - Initial User Identity [unique key] - The identity of a users and systems to seed the Users File. The name of
+            each property must be unique, for example: "Initial User Identity A", "Initial User Identity B",
+            "Initial User Identity C" or "Initial User Identity 1", "Initial User Identity 2", "Initial User Identity 3"
+
+            NOTE: Any identity mapping rules specified in nifi-registry.properties will also be applied to the user identities,
+            so the values should be the unmapped identities (i.e. full DN from a certificate).
+    -->
+    <userGroupProvider>
+        <identifier>file-user-group-provider</identifier>
+        <class>org.apache.nifi.registry.security.authorization.file.FileUserGroupProvider</class>
+        <property name="Users File">./target/test-classes/conf/secure-kerberos/users.xml</property>
+        <property name="Initial User Identity 1">kerberosUser@LOCALHOST</property>
+    </userGroupProvider>
+
+    <!--
+        The FileAccessPolicyProvider will provide support for managing access policies which is backed by a file
+        on the local file system.
+
+        - User Group Provider - The identifier for an User Group Provider defined above that will be used to access
+            users and groups for use in the managed access policies.
+
+        - Authorizations File - The file where the FileAccessPolicyProvider will store policies.
+
+        - Initial Admin Identity - The identity of an initial admin user that will be granted access to the UI and
+            given the ability to create additional users, groups, and policies. The value of this property could be
+            a DN when using certificates or LDAP. This property will only be used when there
+            are no other policies defined.
+
+            NOTE: Any identity mapping rules specified in nifi-registry.properties will also be applied to the initial admin identity,
+            so the value should be the unmapped identity. This identity must be found in the configured User Group Provider.
+
+        - NiFi Identity [unique key] - The identity of a NiFi node that will have access to this NiFi Registry and will be able
+            to act as a proxy on behalf of a NiFi Registry end user. A property should be created for the identity of every NiFi
+            node that needs to access this NiFi Registry.
+
+            NOTE: Any identity mapping rules specified in nifi-registry.properties will also be applied to the nifi identities,
+            so the values should be the unmapped identities (i.e. full DN from a certificate). This identity must be found
+            in the configured User Group Provider.
+    -->
+    <accessPolicyProvider>
+        <identifier>file-access-policy-provider</identifier>
+        <class>org.apache.nifi.registry.security.authorization.file.FileAccessPolicyProvider</class>
+        <property name="User Group Provider">file-user-group-provider</property>
+        <property name="Authorizations File">./target/test-classes/conf/secure-kerberos/authorizations.xml</property>
+        <property name="Initial Admin Identity">kerberosUser@LOCALHOST</property>
+
+        <!--<property name="NiFi Identity 1"></property>-->
+    </accessPolicyProvider>
+
+    <!--
+        The StandardManagedAuthorizer. This authorizer implementation must be configured with the
+        Access Policy Provider which it will use to access and manage users, groups, and policies.
+        These users, groups, and policies will be used to make all access decisions during authorization
+        requests.
+
+        - Access Policy Provider - The identifier for an Access Policy Provider defined above.
+    -->
+    <authorizer>
+        <identifier>managed-authorizer</identifier>
+        <class>org.apache.nifi.registry.security.authorization.StandardManagedAuthorizer</class>
+        <property name="Access Policy Provider">file-access-policy-provider</property>
+    </authorizer>
+
+</authorizers>
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/6f26290d/nifi-registry-core/nifi-registry-web-api/src/test/resources/conf/secure-kerberos/identity-providers.xml
----------------------------------------------------------------------
diff --git a/nifi-registry-core/nifi-registry-web-api/src/test/resources/conf/secure-kerberos/identity-providers.xml b/nifi-registry-core/nifi-registry-web-api/src/test/resources/conf/secure-kerberos/identity-providers.xml
new file mode 100644
index 0000000..cd101ea
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-web-api/src/test/resources/conf/secure-kerberos/identity-providers.xml
@@ -0,0 +1,31 @@
+<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
+<!--
+  ~ 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.
+  -->
+<!--
+    This file lists the login identity providers to use when running securely. In order
+    to use a specific provider it must be configured here and its identifier
+    must be specified in the nifi-registry.properties file.
+-->
+<identityProviders>
+
+    <!-- This test conf is for KerberosSpnegoIdentityProvider,
+         which is configured in nifi-registry.properties and loaded as an auto-scanned Spring Bean.
+
+         This is not intended for KerberosIdentityProvider,
+         which would be loaded from here using IdentityProviderFactory -->
+
+</identityProviders>
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/6f26290d/nifi-registry-core/nifi-registry-web-api/src/test/resources/conf/secure-kerberos/nifi-registry-client.properties
----------------------------------------------------------------------
diff --git a/nifi-registry-core/nifi-registry-web-api/src/test/resources/conf/secure-kerberos/nifi-registry-client.properties b/nifi-registry-core/nifi-registry-web-api/src/test/resources/conf/secure-kerberos/nifi-registry-client.properties
new file mode 100644
index 0000000..f431ccc
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-web-api/src/test/resources/conf/secure-kerberos/nifi-registry-client.properties
@@ -0,0 +1,22 @@
+#
+# 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.
+#
+
+# client security properties #
+# Don't use a client cert for one-way TLS. Client identity will be provided via Kerberos SPNEGO to get JWT
+nifi.registry.security.truststore=./target/test-classes/keys/localhost-ts.jks
+nifi.registry.security.truststoreType=JKS
+nifi.registry.security.truststorePasswd=localhostTruststorePassword

http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/6f26290d/nifi-registry-core/nifi-registry-web-api/src/test/resources/conf/secure-kerberos/nifi-registry.properties
----------------------------------------------------------------------
diff --git a/nifi-registry-core/nifi-registry-web-api/src/test/resources/conf/secure-kerberos/nifi-registry.properties b/nifi-registry-core/nifi-registry-web-api/src/test/resources/conf/secure-kerberos/nifi-registry.properties
new file mode 100644
index 0000000..3d5c122
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-web-api/src/test/resources/conf/secure-kerberos/nifi-registry.properties
@@ -0,0 +1,36 @@
+#
+# 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.
+#
+
+# web properties #
+nifi.registry.web.https.host=localhost
+nifi.registry.web.https.port=0
+
+# security properties #
+#
+# ** Server KeyStore and TrustStore configuration set in Spring profile properties for embedded Jetty **
+#
+nifi.registry.security.authorizers.configuration.file=./target/test-classes/conf/secure-kerberos/authorizers.xml
+nifi.registry.security.authorizer=managed-authorizer
+
+# providers properties #
+nifi.registry.providers.configuration.file=./target/test-classes/conf/providers.xml
+
+# kerberos properties # (aside from expiration, these don't actually matter as the KerberosServiceAuthenticationProvider will be mocked)
+nifi.registry.kerberos.krb5.file=/path/to/krb5.conf
+nifi.registry.kerberos.spnego.authentication.expiration=12 hours
+nifi.registry.kerberos.spnego.principal=HTTP/localhost@LOCALHOST
+nifi.registry.kerberos.spnego.keytab.location=/path/to/keytab

http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/6f26290d/nifi-registry-core/nifi-registry-web-api/src/test/resources/conf/secure-ldap/authorizers.protected.xml
----------------------------------------------------------------------
diff --git a/nifi-registry-core/nifi-registry-web-api/src/test/resources/conf/secure-ldap/authorizers.protected.xml b/nifi-registry-core/nifi-registry-web-api/src/test/resources/conf/secure-ldap/authorizers.protected.xml
new file mode 100644
index 0000000..44007bd
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-web-api/src/test/resources/conf/secure-ldap/authorizers.protected.xml
@@ -0,0 +1,221 @@
+<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
+<!--
+  ~ 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.
+  -->
+<!--
+    This file lists the userGroupProviders, accessPolicyProviders, and authorizers to use when running securely. In order
+    to use a specific authorizer it must be configured here and its identifier must be specified in the nifi-registry.properties file.
+    If the authorizer is a managedAuthorizer, it may need to be configured with an accessPolicyProvider and an userGroupProvider.
+    This file allows for configuration of them, but they must be configured in order:
+
+    ...
+    all userGroupProviders
+    all accessPolicyProviders
+    all Authorizers
+    ...
+-->
+<authorizers>
+
+    <!--
+        The LdapUserGroupProvider will retrieve users and groups from an LDAP server. The users and groups
+        are not configurable.
+
+        'Authentication Strategy' - How the connection to the LDAP server is authenticated. Possible
+            values are ANONYMOUS, SIMPLE, LDAPS, or START_TLS.
+
+        'Manager DN' - The DN of the manager that is used to bind to the LDAP server to search for users.
+        'Manager Password' - The password of the manager that is used to bind to the LDAP server to
+            search for users.
+
+        'TLS - Keystore' - Path to the Keystore that is used when connecting to LDAP using LDAPS or START_TLS.
+        'TLS - Keystore Password' - Password for the Keystore that is used when connecting to LDAP
+            using LDAPS or START_TLS.
+        'TLS - Keystore Type' - Type of the Keystore that is used when connecting to LDAP using
+            LDAPS or START_TLS (i.e. JKS or PKCS12).
+        'TLS - Truststore' - Path to the Truststore that is used when connecting to LDAP using LDAPS or START_TLS.
+        'TLS - Truststore Password' - Password for the Truststore that is used when connecting to
+            LDAP using LDAPS or START_TLS.
+        'TLS - Truststore Type' - Type of the Truststore that is used when connecting to LDAP using
+            LDAPS or START_TLS (i.e. JKS or PKCS12).
+        'TLS - Client Auth' - Client authentication policy when connecting to LDAP using LDAPS or START_TLS.
+            Possible values are REQUIRED, WANT, NONE.
+        'TLS - Protocol' - Protocol to use when connecting to LDAP using LDAPS or START_TLS. (i.e. TLS,
+            TLSv1.1, TLSv1.2, etc).
+        'TLS - Shutdown Gracefully' - Specifies whether the TLS should be shut down gracefully
+            before the target context is closed. Defaults to false.
+
+        'Referral Strategy' - Strategy for handling referrals. Possible values are FOLLOW, IGNORE, THROW.
+        'Connect Timeout' - Duration of connect timeout. (i.e. 10 secs).
+        'Read Timeout' - Duration of read timeout. (i.e. 10 secs).
+
+        'Url' - Space-separated list of URLs of the LDAP servers (i.e. ldap://<hostname>:<port>).
+        'Page Size' - Sets the page size when retrieving users and groups. If not specified, no paging is performed.
+        'Sync Interval' - Duration of time between syncing users and groups. (i.e. 30 mins).
+
+        'User Search Base' - Base DN for searching for users (i.e. ou=users,o=nifi). Required to search users.
+        'User Object Class' - Object class for identifying users (i.e. person). Required if searching users.
+        'User Search Scope' - Search scope for searching users (ONE_LEVEL, OBJECT, or SUBTREE). Required if searching users.
+        'User Search Filter' - Filter for searching for users against the 'User Search Base' (i.e. (memberof=cn=team1,ou=groups,o=nifi) ). Optional.
+        'User Identity Attribute' - Attribute to use to extract user identity (i.e. cn). Optional. If not set, the entire DN is used.
+        'User Group Name Attribute' - Attribute to use to define group membership (i.e. memberof). Optional. If not set
+            group membership will not be calculated through the users. Will rely on group membership being defined
+            through 'Group Member Attribute' if set.
+
+        'Group Search Base' - Base DN for searching for groups (i.e. ou=groups,o=nifi). Required to search groups.
+        'Group Object Class' - Object class for identifying groups (i.e. groupOfNames). Required if searching groups.
+        'Group Search Scope' - Search scope for searching groups (ONE_LEVEL, OBJECT, or SUBTREE). Required if searching groups.
+        'Group Search Filter' - Filter for searching for groups against the 'Group Search Base'. Optional.
+        'Group Name Attribute' - Attribute to use to extract group name (i.e. cn). Optional. If not set, the entire DN is used.
+        'Group Member Attribute' - Attribute to use to define group membership (i.e. member). Optional. If not set
+            group membership will not be calculated through the groups. Will rely on group member being defined
+            through 'User Group Name Attribute' if set.
+
+        NOTE: Any identity mapping rules specified in nifi-registry.properties will also be applied to the user identities.
+            Group names are not mapped.
+    -->
+    <userGroupProvider>
+        <identifier>ldap-user-group-provider</identifier>
+        <class>org.apache.nifi.registry.security.ldap.tenants.LdapUserGroupProvider</class>
+        <property name="Authentication Strategy">SIMPLE</property>
+
+        <property name="Manager DN">cn=read-only-admin,dc=example,dc=com</property>
+        <!-- password='password' encrypted with algo='aes/gcm/128' and key='0123456789ABCDEFFEDCBA9876543210' -->
+        <property name="Manager Password" encryption="aes/gcm/128">oVU8w3uH7yZlKscG||Hu4ZtRgZWKISn3DyGuB50rKL1qGceWZp</property>
+
+        <!--
+        <property name="TLS - Keystore"></property>
+        <property name="TLS - Keystore Password"></property>
+        <property name="TLS - Keystore Type"></property>
+        <property name="TLS - Truststore"></property>
+        <property name="TLS - Truststore Password"></property>
+        <property name="TLS - Truststore Type"></property>
+        <property name="TLS - Client Auth"></property>
+        <property name="TLS - Protocol"></property>
+        <property name="TLS - Shutdown Gracefully"></property>
+        -->
+
+        <property name="Referral Strategy">FOLLOW</property>
+        <property name="Connect Timeout">10 secs</property>
+        <property name="Read Timeout">10 secs</property>
+
+        <property name="Url">ldap://localhost:8389</property>
+        <!--<property name="Page Size"></property>-->
+        <property name="Sync Interval">30 mins</property>
+
+        <property name="User Search Base">dc=example,dc=com</property>
+        <property name="User Object Class">person</property>
+        <property name="User Search Scope">ONE_LEVEL</property>
+        <property name="User Search Filter">(uid=*)</property>
+        <property name="User Identity Attribute">uid</property>
+        <!--<property name="User Group Name Attribute"></property>-->
+
+        <property name="Group Search Base">dc=example,dc=com</property>
+        <property name="Group Object Class">groupOfUniqueNames</property>
+        <property name="Group Search Scope">ONE_LEVEL</property>
+        <property name="Group Search Filter">(ou=*)</property>
+        <property name="Group Name Attribute">ou</property>
+        <property name="Group Member Attribute">uniqueMember</property>
+    </userGroupProvider>
+
+    <!--
+        The CompositeUserGroupProvider will provide support for retrieving users and groups from multiple sources.
+
+        - User Group Provider [unique key] - The identifier of user group providers to load from. The name of
+            each property must be unique, for example: "User Group Provider A", "User Group Provider B",
+            "User Group Provider C" or "User Group Provider 1", "User Group Provider 2", "User Group Provider 3"
+
+            NOTE: Any identity mapping rules specified in nifi-registry.properties are not applied in this implementation. This behavior
+            would need to be applied by the base implementation.
+    -->
+    <!-- To enable the composite-user-group-provider remove 2 lines. This is 1 of 2.
+    <userGroupProvider>
+        <identifier>composite-user-group-provider</identifier>
+        <class>org.apache.nifi.authorization.CompositeUserGroupProvider</class>
+        <property name="User Group Provider 1"></property>
+    </userGroupProvider>
+    To enable the composite-user-group-provider remove 2 lines. This is 2 of 2. -->
+
+    <!--
+        The CompositeConfigurableUserGroupProvider will provide support for retrieving users and groups from multiple sources.
+        Additionally, a single configurable user group provider is required. Users from the configurable user group provider
+        are configurable, however users loaded from one of the User Group Provider [unique key] will not be.
+
+        - Configurable User Group Provider - A configurable user group provider.
+
+        - User Group Provider [unique key] - The identifier of user group providers to load from. The name of
+            each property must be unique, for example: "User Group Provider A", "User Group Provider B",
+            "User Group Provider C" or "User Group Provider 1", "User Group Provider 2", "User Group Provider 3"
+
+            NOTE: Any identity mapping rules specified in nifi-registry.properties are not applied in this implementation. This behavior
+            would need to be applied by the base implementation.
+    -->
+    <!-- To enable the composite-configurable-user-group-provider remove 2 lines. This is 1 of 2.
+    <userGroupProvider>
+        <identifier>composite-configurable-user-group-provider</identifier>
+        <class>org.apache.nifi.authorization.CompositeConfigurableUserGroupProvider</class>
+        <property name="Configurable User Group Provider">file-user-group-provider</property>
+        <property name="User Group Provider 1"></property>
+    </userGroupProvider>
+    To enable the composite-configurable-user-group-provider remove 2 lines. This is 2 of 2. -->
+
+    <!--
+        The FileAccessPolicyProvider will provide support for managing access policies which is backed by a file
+        on the local file system.
+
+        - User Group Provider - The identifier for an User Group Provider defined above that will be used to access
+            users and groups for use in the managed access policies.
+
+        - Authorizations File - The file where the FileAccessPolicyProvider will store policies.
+
+        - Initial Admin Identity - The identity of an initial admin user that will be granted access to the UI and
+            given the ability to create additional users, groups, and policies. The value of this property could be
+            a DN when using certificates or LDAP. This property will only be used when there
+            are no other policies defined.
+
+            NOTE: Any identity mapping rules specified in nifi-registry.properties will also be applied to the initial admin identity,
+            so the value should be the unmapped identity. This identity must be found in the configured User Group Provider.
+
+        - NiFi Identity [unique key] - The identity of a NiFi node that will have access to this NiFi Registry and will be able
+            to act as a proxy on behalf of a NiFi Registry end user. A property should be created for the identity of every NiFi
+            node that needs to access this NiFi Registry.
+
+            NOTE: Any identity mapping rules specified in nifi-registry.properties will also be applied to the nifi identities,
+            so the values should be the unmapped identities (i.e. full DN from a certificate). This identity must be found
+            in the configured User Group Provider.
+    -->
+    <accessPolicyProvider>
+        <identifier>file-access-policy-provider</identifier>
+        <class>org.apache.nifi.registry.security.authorization.file.FileAccessPolicyProvider</class>
+        <property name="User Group Provider">ldap-user-group-provider</property>
+        <property name="Authorizations File">./target/test-classes/conf/secure-ldap/authorizations.xml</property>
+        <property name="Initial Admin Identity">nifiadmin</property>
+    </accessPolicyProvider>
+
+    <!--
+        The StandardManagedAuthorizer. This authorizer implementation must be configured with the
+        Access Policy Provider which it will use to access and manage users, groups, and policies.
+        These users, groups, and policies will be used to make all access decisions during authorization
+        requests.
+
+        - Access Policy Provider - The identifier for an Access Policy Provider defined above.
+    -->
+    <authorizer>
+        <identifier>managed-authorizer</identifier>
+        <class>org.apache.nifi.registry.security.authorization.StandardManagedAuthorizer</class>
+        <property name="Access Policy Provider">file-access-policy-provider</property>
+    </authorizer>
+
+</authorizers>
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/6f26290d/nifi-registry-core/nifi-registry-web-api/src/test/resources/conf/secure-ldap/authorizers.xml
----------------------------------------------------------------------
diff --git a/nifi-registry-core/nifi-registry-web-api/src/test/resources/conf/secure-ldap/authorizers.xml b/nifi-registry-core/nifi-registry-web-api/src/test/resources/conf/secure-ldap/authorizers.xml
new file mode 100644
index 0000000..c55e5a2
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-web-api/src/test/resources/conf/secure-ldap/authorizers.xml
@@ -0,0 +1,242 @@
+<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
+<!--
+  ~ 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.
+  -->
+<!--
+    This file lists the userGroupProviders, accessPolicyProviders, and authorizers to use when running securely. In order
+    to use a specific authorizer it must be configured here and its identifier must be specified in the nifi-registry.properties file.
+    If the authorizer is a managedAuthorizer, it may need to be configured with an accessPolicyProvider and an userGroupProvider.
+    This file allows for configuration of them, but they must be configured in order:
+
+    ...
+    all userGroupProviders
+    all accessPolicyProviders
+    all Authorizers
+    ...
+-->
+<authorizers>
+
+    <!--
+        The FileUserGroupProvider will provide support for managing users and groups which is backed by a file
+        on the local file system.
+
+        - Users File - The file where the FileUserGroupProvider will store users and groups.
+
+        - Initial User Identity [unique key] - The identity of a users and systems to seed the Users File. The name of
+            each property must be unique, for example: "Initial User Identity A", "Initial User Identity B",
+            "Initial User Identity C" or "Initial User Identity 1", "Initial User Identity 2", "Initial User Identity 3"
+
+            NOTE: Any identity mapping rules specified in nifi-registry.properties will also be applied to the user identities,
+            so the values should be the unmapped identities (i.e. full DN from a certificate).
+    -->
+    <!-- To enable the file-user-group-provider remove 2 lines. This is 1 of 2.
+    <userGroupProvider>
+        <identifier>file-user-group-provider</identifier>
+        <class>org.apache.nifi.registry.authorization.file.FileUserGroupProvider</class>
+        <property name="Users File">./target/test-classes/conf/secure-file/users.xml</property>
+        <property name="Initial User Identity 1">CN=user1, OU=nifi</property>
+    </userGroupProvider>
+    To enable the file-user-group-provider remove 2 lines. This is 2 of 2. -->
+
+    <!--
+        The LdapUserGroupProvider will retrieve users and groups from an LDAP server. The users and groups
+        are not configurable.
+
+        'Authentication Strategy' - How the connection to the LDAP server is authenticated. Possible
+            values are ANONYMOUS, SIMPLE, LDAPS, or START_TLS.
+
+        'Manager DN' - The DN of the manager that is used to bind to the LDAP server to search for users.
+        'Manager Password' - The password of the manager that is used to bind to the LDAP server to
+            search for users.
+
+        'TLS - Keystore' - Path to the Keystore that is used when connecting to LDAP using LDAPS or START_TLS.
+        'TLS - Keystore Password' - Password for the Keystore that is used when connecting to LDAP
+            using LDAPS or START_TLS.
+        'TLS - Keystore Type' - Type of the Keystore that is used when connecting to LDAP using
+            LDAPS or START_TLS (i.e. JKS or PKCS12).
+        'TLS - Truststore' - Path to the Truststore that is used when connecting to LDAP using LDAPS or START_TLS.
+        'TLS - Truststore Password' - Password for the Truststore that is used when connecting to
+            LDAP using LDAPS or START_TLS.
+        'TLS - Truststore Type' - Type of the Truststore that is used when connecting to LDAP using
+            LDAPS or START_TLS (i.e. JKS or PKCS12).
+        'TLS - Client Auth' - Client authentication policy when connecting to LDAP using LDAPS or START_TLS.
+            Possible values are REQUIRED, WANT, NONE.
+        'TLS - Protocol' - Protocol to use when connecting to LDAP using LDAPS or START_TLS. (i.e. TLS,
+            TLSv1.1, TLSv1.2, etc).
+        'TLS - Shutdown Gracefully' - Specifies whether the TLS should be shut down gracefully
+            before the target context is closed. Defaults to false.
+
+        'Referral Strategy' - Strategy for handling referrals. Possible values are FOLLOW, IGNORE, THROW.
+        'Connect Timeout' - Duration of connect timeout. (i.e. 10 secs).
+        'Read Timeout' - Duration of read timeout. (i.e. 10 secs).
+
+        'Url' - Space-separated list of URLs of the LDAP servers (i.e. ldap://<hostname>:<port>).
+        'Page Size' - Sets the page size when retrieving users and groups. If not specified, no paging is performed.
+        'Sync Interval' - Duration of time between syncing users and groups. (i.e. 30 mins).
+
+        'User Search Base' - Base DN for searching for users (i.e. ou=users,o=nifi). Required to search users.
+        'User Object Class' - Object class for identifying users (i.e. person). Required if searching users.
+        'User Search Scope' - Search scope for searching users (ONE_LEVEL, OBJECT, or SUBTREE). Required if searching users.
+        'User Search Filter' - Filter for searching for users against the 'User Search Base' (i.e. (memberof=cn=team1,ou=groups,o=nifi) ). Optional.
+        'User Identity Attribute' - Attribute to use to extract user identity (i.e. cn). Optional. If not set, the entire DN is used.
+        'User Group Name Attribute' - Attribute to use to define group membership (i.e. memberof). Optional. If not set
+            group membership will not be calculated through the users. Will rely on group membership being defined
+            through 'Group Member Attribute' if set.
+
+        'Group Search Base' - Base DN for searching for groups (i.e. ou=groups,o=nifi). Required to search groups.
+        'Group Object Class' - Object class for identifying groups (i.e. groupOfNames). Required if searching groups.
+        'Group Search Scope' - Search scope for searching groups (ONE_LEVEL, OBJECT, or SUBTREE). Required if searching groups.
+        'Group Search Filter' - Filter for searching for groups against the 'Group Search Base'. Optional.
+        'Group Name Attribute' - Attribute to use to extract group name (i.e. cn). Optional. If not set, the entire DN is used.
+        'Group Member Attribute' - Attribute to use to define group membership (i.e. member). Optional. If not set
+            group membership will not be calculated through the groups. Will rely on group member being defined
+            through 'User Group Name Attribute' if set.
+
+        NOTE: Any identity mapping rules specified in nifi-registry.properties will also be applied to the user identities.
+            Group names are not mapped.
+    -->
+    <userGroupProvider>
+        <identifier>ldap-user-group-provider</identifier>
+        <class>org.apache.nifi.registry.security.ldap.tenants.LdapUserGroupProvider</class>
+        <property name="Authentication Strategy">SIMPLE</property>
+
+        <property name="Manager DN">cn=read-only-admin,dc=example,dc=com</property>
+        <property name="Manager Password">password</property>
+
+        <!--
+        <property name="TLS - Keystore"></property>
+        <property name="TLS - Keystore Password"></property>
+        <property name="TLS - Keystore Type"></property>
+        <property name="TLS - Truststore"></property>
+        <property name="TLS - Truststore Password"></property>
+        <property name="TLS - Truststore Type"></property>
+        <property name="TLS - Client Auth"></property>
+        <property name="TLS - Protocol"></property>
+        <property name="TLS - Shutdown Gracefully"></property>
+        -->
+
+        <property name="Referral Strategy">FOLLOW</property>
+        <property name="Connect Timeout">10 secs</property>
+        <property name="Read Timeout">10 secs</property>
+
+        <property name="Url">ldap://localhost:8389</property>
+        <!--<property name="Page Size"></property>-->
+        <property name="Sync Interval">30 mins</property>
+
+        <property name="User Search Base">dc=example,dc=com</property>
+        <property name="User Object Class">person</property>
+        <property name="User Search Scope">ONE_LEVEL</property>
+        <property name="User Search Filter">(uid=*)</property>
+        <property name="User Identity Attribute">uid</property>
+        <!--<property name="User Group Name Attribute"></property>-->
+
+        <property name="Group Search Base">dc=example,dc=com</property>
+        <property name="Group Object Class">groupOfUniqueNames</property>
+        <property name="Group Search Scope">ONE_LEVEL</property>
+        <property name="Group Search Filter">(ou=*)</property>
+        <property name="Group Name Attribute">ou</property>
+        <property name="Group Member Attribute">uniqueMember</property>
+    </userGroupProvider>
+
+    <!--
+        The CompositeUserGroupProvider will provide support for retrieving users and groups from multiple sources.
+
+        - User Group Provider [unique key] - The identifier of user group providers to load from. The name of
+            each property must be unique, for example: "User Group Provider A", "User Group Provider B",
+            "User Group Provider C" or "User Group Provider 1", "User Group Provider 2", "User Group Provider 3"
+
+            NOTE: Any identity mapping rules specified in nifi-registry.properties are not applied in this implementation. This behavior
+            would need to be applied by the base implementation.
+    -->
+    <!-- To enable the composite-user-group-provider remove 2 lines. This is 1 of 2.
+    <userGroupProvider>
+        <identifier>composite-user-group-provider</identifier>
+        <class>org.apache.nifi.registry.security.authorization.CompositeUserGroupProvider</class>
+        <property name="User Group Provider 1"></property>
+    </userGroupProvider>
+    To enable the composite-user-group-provider remove 2 lines. This is 2 of 2. -->
+
+    <!--
+        The CompositeConfigurableUserGroupProvider will provide support for retrieving users and groups from multiple sources.
+        Additionally, a single configurable user group provider is required. Users from the configurable user group provider
+        are configurable, however users loaded from one of the User Group Provider [unique key] will not be.
+
+        - Configurable User Group Provider - A configurable user group provider.
+
+        - User Group Provider [unique key] - The identifier of user group providers to load from. The name of
+            each property must be unique, for example: "User Group Provider A", "User Group Provider B",
+            "User Group Provider C" or "User Group Provider 1", "User Group Provider 2", "User Group Provider 3"
+
+            NOTE: Any identity mapping rules specified in nifi-registry.properties are not applied in this implementation. This behavior
+            would need to be applied by the base implementation.
+    -->
+    <!-- To enable the composite-configurable-user-group-provider remove 2 lines. This is 1 of 2.
+    <userGroupProvider>
+        <identifier>composite-configurable-user-group-provider</identifier>
+        <class>org.apache.nifi.registry.security.authorization.CompositeConfigurableUserGroupProvider</class>
+        <property name="Configurable User Group Provider">file-user-group-provider</property>
+        <property name="User Group Provider 1"></property>
+    </userGroupProvider>
+    To enable the composite-configurable-user-group-provider remove 2 lines. This is 2 of 2. -->
+
+    <!--
+        The FileAccessPolicyProvider will provide support for managing access policies which is backed by a file
+        on the local file system.
+
+        - User Group Provider - The identifier for an User Group Provider defined above that will be used to access
+            users and groups for use in the managed access policies.
+
+        - Authorizations File - The file where the FileAccessPolicyProvider will store policies.
+
+        - Initial Admin Identity - The identity of an initial admin user that will be granted access to the UI and
+            given the ability to create additional users, groups, and policies. The value of this property could be
+            a DN when using certificates or LDAP. This property will only be used when there
+            are no other policies defined.
+
+            NOTE: Any identity mapping rules specified in nifi-registry.properties will also be applied to the initial admin identity,
+            so the value should be the unmapped identity. This identity must be found in the configured User Group Provider.
+
+        - NiFi Identity [unique key] - The identity of a NiFi node that will have access to this NiFi Registry and will be able
+            to act as a proxy on behalf of a NiFi Registry end user. A property should be created for the identity of every NiFi
+            node that needs to access this NiFi Registry.
+
+            NOTE: Any identity mapping rules specified in nifi-registry.properties will also be applied to the nifi identities,
+            so the values should be the unmapped identities (i.e. full DN from a certificate). This identity must be found
+            in the configured User Group Provider.
+    -->
+    <accessPolicyProvider>
+        <identifier>file-access-policy-provider</identifier>
+        <class>org.apache.nifi.registry.security.authorization.file.FileAccessPolicyProvider</class>
+        <property name="User Group Provider">ldap-user-group-provider</property>
+        <property name="Authorizations File">./target/test-classes/conf/secure-ldap/authorizations.xml</property>
+        <property name="Initial Admin Identity">nifiadmin</property>
+    </accessPolicyProvider>
+
+    <!--
+        The StandardManagedAuthorizer. This authorizer implementation must be configured with the
+        Access Policy Provider which it will use to access and manage users, groups, and policies.
+        These users, groups, and policies will be used to make all access decisions during authorization
+        requests.
+
+        - Access Policy Provider - The identifier for an Access Policy Provider defined above.
+    -->
+    <authorizer>
+        <identifier>managed-authorizer</identifier>
+        <class>org.apache.nifi.registry.security.authorization.StandardManagedAuthorizer</class>
+        <property name="Access Policy Provider">file-access-policy-provider</property>
+    </authorizer>
+
+</authorizers>
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/6f26290d/nifi-registry-core/nifi-registry-web-api/src/test/resources/conf/secure-ldap/bootstrap.conf
----------------------------------------------------------------------
diff --git a/nifi-registry-core/nifi-registry-web-api/src/test/resources/conf/secure-ldap/bootstrap.conf b/nifi-registry-core/nifi-registry-web-api/src/test/resources/conf/secure-ldap/bootstrap.conf
new file mode 100644
index 0000000..4bd28ba
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-web-api/src/test/resources/conf/secure-ldap/bootstrap.conf
@@ -0,0 +1,60 @@
+#
+# 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.
+#
+
+# Java command to use when running nifi-registry
+java=java
+
+# Username to use when running nifi-registry. This value will be ignored on Windows.
+run.as=
+
+# Configure where nifi-registry's lib and conf directories live
+lib.dir=./lib
+conf.dir=./conf
+
+# How long to wait after telling nifi-registry to shutdown before explicitly killing the Process
+graceful.shutdown.seconds=20
+
+# Disable JSR 199 so that we can use JSP's without running a JDK
+java.arg.1=-Dorg.apache.jasper.compiler.disablejsr199=true
+
+# JVM memory settings
+java.arg.2=-Xms512m
+java.arg.3=-Xmx512m
+
+# Enable Remote Debugging
+java.arg.debug=-agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=8000
+
+java.arg.4=-Djava.net.preferIPv4Stack=true
+
+# allowRestrictedHeaders is required for Cluster/Node communications to work properly
+java.arg.5=-Dsun.net.http.allowRestrictedHeaders=true
+java.arg.6=-Djava.protocol.handler.pkgs=sun.net.www.protocol
+
+# Java 7 and below have issues with Code Cache. The following lines allow us to run well even with
+# many classes loaded in the JVM.
+#java.arg.7=-XX:ReservedCodeCacheSize=256m
+#java.arg.8=-XX:CodeCacheFlushingMinimumFreeSpace=10m
+#java.arg.9=-XX:+UseCodeCacheFlushing
+#java.arg.11=-XX:PermSize=128M
+#java.arg.12=-XX:MaxPermSize=128M
+
+# The G1GC is still considered experimental but has proven to be very advantageous in providing great
+# performance without significant "stop-the-world" delays.
+#java.arg.10=-XX:+UseG1GC
+
+# Master key in hexadecimal format for encrypted sensitive configuration values
+nifi.registry.bootstrap.sensitive.key=0123456789ABCDEFFEDCBA9876543210

http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/6f26290d/nifi-registry-core/nifi-registry-web-api/src/test/resources/conf/secure-ldap/identity-providers.protected.xml
----------------------------------------------------------------------
diff --git a/nifi-registry-core/nifi-registry-web-api/src/test/resources/conf/secure-ldap/identity-providers.protected.xml b/nifi-registry-core/nifi-registry-web-api/src/test/resources/conf/secure-ldap/identity-providers.protected.xml
new file mode 100644
index 0000000..51336e2
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-web-api/src/test/resources/conf/secure-ldap/identity-providers.protected.xml
@@ -0,0 +1,89 @@
+<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
+<!--
+  ~ 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.
+  -->
+<!--
+    This file lists the login identity providers to use when running securely. In order
+    to use a specific provider it must be configured here and it's identifier
+    must be specified in the nifi-registry.properties file.
+-->
+<identityProviders>
+    <!--
+        Identity Provider for users logging in with username/password against an LDAP server.
+        
+        'Authentication Strategy' - How the connection to the LDAP server is authenticated. Possible
+            values are ANONYMOUS, SIMPLE, LDAPS, or START_TLS.
+        
+        'Manager DN' - The DN of the manager that is used to bind to the LDAP server to search for users.
+        'Manager Password' - The password of the manager that is used to bind to the LDAP server to
+            search for users.
+            
+        'TLS - Keystore' - Path to the Keystore that is used when connecting to LDAP using LDAPS or START_TLS.
+        'TLS - Keystore Password' - Password for the Keystore that is used when connecting to LDAP
+            using LDAPS or START_TLS.
+        'TLS - Keystore Type' - Type of the Keystore that is used when connecting to LDAP using
+            LDAPS or START_TLS (i.e. JKS or PKCS12).
+        'TLS - Truststore' - Path to the Truststore that is used when connecting to LDAP using LDAPS or START_TLS.
+        'TLS - Truststore Password' - Password for the Truststore that is used when connecting to
+            LDAP using LDAPS or START_TLS.
+        'TLS - Truststore Type' - Type of the Truststore that is used when connecting to LDAP using
+            LDAPS or START_TLS (i.e. JKS or PKCS12).
+        'TLS - Client Auth' - Client authentication policy when connecting to LDAP using LDAPS or START_TLS.
+            Possible values are REQUIRED, WANT, NONE.
+        'TLS - Protocol' - Protocol to use when connecting to LDAP using LDAPS or START_TLS. (i.e. TLS,
+            TLSv1.1, TLSv1.2, etc).
+        'TLS - Shutdown Gracefully' - Specifies whether the TLS should be shut down gracefully 
+            before the target context is closed. Defaults to false.
+            
+        'Referral Strategy' - Strategy for handling referrals. Possible values are FOLLOW, IGNORE, THROW.
+        'Connect Timeout' - Duration of connect timeout. (i.e. 10 secs).
+        'Read Timeout' - Duration of read timeout. (i.e. 10 secs).
+       
+        'Url' - Space-separated list of URLs of the LDAP servers (i.e. ldap://<hostname>:<port>).
+        'User Search Base' - Base DN for searching for users (i.e. CN=Users,DC=example,DC=com).
+        'User Search Filter' - Filter for searching for users against the 'User Search Base'.
+            (i.e. sAMAccountName={0}). The user specified name is inserted into '{0}'.
+
+        'Identity Strategy' - Strategy to identify users. Possible values are USE_DN and USE_USERNAME.
+            The default functionality if this property is missing is USE_DN in order to retain
+            backward compatibility. USE_DN will use the full DN of the user entry if possible.
+            USE_USERNAME will use the username the user logged in with.
+        'Authentication Expiration' - The duration of how long the user authentication is valid
+            for. If the user never logs out, they will be required to log back in following
+            this duration.
+    -->
+    <provider>
+        <identifier>ldap-identity-provider</identifier>
+        <class>org.apache.nifi.registry.security.ldap.LdapIdentityProvider</class>
+        <property name="Authentication Strategy">SIMPLE</property>
+
+        <property name="Manager DN">cn=read-only-admin,dc=example,dc=com</property>
+        <!-- password='password' encrypted with algo='aes/gcm/128' and key='0123456789ABCDEFFEDCBA9876543210' -->
+        <property name="Manager Password" encryption="aes/gcm/128">p43I3jRcK+wPhR3c||oaqMg3YGo2WblTxBJSgI8H9fLMBwQiaM</property>
+        
+        <property name="Referral Strategy">FOLLOW</property>
+        <property name="Connect Timeout">10 secs</property>
+        <property name="Read Timeout">10 secs</property>
+
+        <property name="Url">ldap://localhost:8389</property>
+        <property name="User Search Base">dc=example,dc=com</property>
+        <property name="User Search Filter">(uid={0})</property>
+
+        <property name="Identity Strategy">USE_USERNAME</property>
+        <property name="Authentication Expiration">12 hours</property>
+    </provider>
+
+</identityProviders>
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/6f26290d/nifi-registry-core/nifi-registry-web-api/src/test/resources/conf/secure-ldap/identity-providers.xml
----------------------------------------------------------------------
diff --git a/nifi-registry-core/nifi-registry-web-api/src/test/resources/conf/secure-ldap/identity-providers.xml b/nifi-registry-core/nifi-registry-web-api/src/test/resources/conf/secure-ldap/identity-providers.xml
new file mode 100644
index 0000000..90c7777
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-web-api/src/test/resources/conf/secure-ldap/identity-providers.xml
@@ -0,0 +1,88 @@
+<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
+<!--
+  ~ 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.
+  -->
+<!--
+    This file lists the login identity providers to use when running securely. In order
+    to use a specific provider it must be configured here and it's identifier
+    must be specified in the nifi-registry.properties file.
+-->
+<identityProviders>
+    <!--
+        Identity Provider for users logging in with username/password against an LDAP server.
+        
+        'Authentication Strategy' - How the connection to the LDAP server is authenticated. Possible
+            values are ANONYMOUS, SIMPLE, LDAPS, or START_TLS.
+        
+        'Manager DN' - The DN of the manager that is used to bind to the LDAP server to search for users.
+        'Manager Password' - The password of the manager that is used to bind to the LDAP server to
+            search for users.
+            
+        'TLS - Keystore' - Path to the Keystore that is used when connecting to LDAP using LDAPS or START_TLS.
+        'TLS - Keystore Password' - Password for the Keystore that is used when connecting to LDAP
+            using LDAPS or START_TLS.
+        'TLS - Keystore Type' - Type of the Keystore that is used when connecting to LDAP using
+            LDAPS or START_TLS (i.e. JKS or PKCS12).
+        'TLS - Truststore' - Path to the Truststore that is used when connecting to LDAP using LDAPS or START_TLS.
+        'TLS - Truststore Password' - Password for the Truststore that is used when connecting to
+            LDAP using LDAPS or START_TLS.
+        'TLS - Truststore Type' - Type of the Truststore that is used when connecting to LDAP using
+            LDAPS or START_TLS (i.e. JKS or PKCS12).
+        'TLS - Client Auth' - Client authentication policy when connecting to LDAP using LDAPS or START_TLS.
+            Possible values are REQUIRED, WANT, NONE.
+        'TLS - Protocol' - Protocol to use when connecting to LDAP using LDAPS or START_TLS. (i.e. TLS,
+            TLSv1.1, TLSv1.2, etc).
+        'TLS - Shutdown Gracefully' - Specifies whether the TLS should be shut down gracefully 
+            before the target context is closed. Defaults to false.
+            
+        'Referral Strategy' - Strategy for handling referrals. Possible values are FOLLOW, IGNORE, THROW.
+        'Connect Timeout' - Duration of connect timeout. (i.e. 10 secs).
+        'Read Timeout' - Duration of read timeout. (i.e. 10 secs).
+       
+        'Url' - Space-separated list of URLs of the LDAP servers (i.e. ldap://<hostname>:<port>).
+        'User Search Base' - Base DN for searching for users (i.e. CN=Users,DC=example,DC=com).
+        'User Search Filter' - Filter for searching for users against the 'User Search Base'.
+            (i.e. sAMAccountName={0}). The user specified name is inserted into '{0}'.
+
+        'Identity Strategy' - Strategy to identify users. Possible values are USE_DN and USE_USERNAME.
+            The default functionality if this property is missing is USE_DN in order to retain
+            backward compatibility. USE_DN will use the full DN of the user entry if possible.
+            USE_USERNAME will use the username the user logged in with.
+        'Authentication Expiration' - The duration of how long the user authentication is valid
+            for. If the user never logs out, they will be required to log back in following
+            this duration.
+    -->
+    <provider>
+        <identifier>ldap-identity-provider</identifier>
+        <class>org.apache.nifi.registry.security.ldap.LdapIdentityProvider</class>
+        <property name="Authentication Strategy">SIMPLE</property>
+
+        <property name="Manager DN">cn=read-only-admin,dc=example,dc=com</property>
+        <property name="Manager Password">password</property>
+        
+        <property name="Referral Strategy">FOLLOW</property>
+        <property name="Connect Timeout">10 secs</property>
+        <property name="Read Timeout">10 secs</property>
+
+        <property name="Url">ldap://localhost:8389</property>
+        <property name="User Search Base">dc=example,dc=com</property>
+        <property name="User Search Filter">(uid={0})</property>
+
+        <property name="Identity Strategy">USE_USERNAME</property>
+        <property name="Authentication Expiration">12 hours</property>
+    </provider>
+
+</identityProviders>
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/6f26290d/nifi-registry-core/nifi-registry-web-api/src/test/resources/conf/secure-ldap/nifi-registry-client.properties
----------------------------------------------------------------------
diff --git a/nifi-registry-core/nifi-registry-web-api/src/test/resources/conf/secure-ldap/nifi-registry-client.properties b/nifi-registry-core/nifi-registry-web-api/src/test/resources/conf/secure-ldap/nifi-registry-client.properties
new file mode 100644
index 0000000..68cb0f9
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-web-api/src/test/resources/conf/secure-ldap/nifi-registry-client.properties
@@ -0,0 +1,22 @@
+#
+# 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.
+#
+
+# client security properties #
+# Don't use a client cert for one-way TLS. Client identity will be provided via LDAP user/pass to get JWT
+nifi.registry.security.truststore=./target/test-classes/keys/localhost-ts.jks
+nifi.registry.security.truststoreType=JKS
+nifi.registry.security.truststorePasswd=localhostTruststorePassword

http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/6f26290d/nifi-registry-core/nifi-registry-web-api/src/test/resources/conf/secure-ldap/nifi-registry.properties
----------------------------------------------------------------------
diff --git a/nifi-registry-core/nifi-registry-web-api/src/test/resources/conf/secure-ldap/nifi-registry.properties b/nifi-registry-core/nifi-registry-web-api/src/test/resources/conf/secure-ldap/nifi-registry.properties
new file mode 100644
index 0000000..1b46ac2
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-web-api/src/test/resources/conf/secure-ldap/nifi-registry.properties
@@ -0,0 +1,32 @@
+#
+# 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.
+#
+
+# web properties #
+nifi.registry.web.https.host=localhost
+nifi.registry.web.https.port=0
+
+# security properties #
+#
+# ** Server KeyStore and TrustStore configuration set in Spring profile properties for embedded Jetty **
+#
+nifi.registry.security.authorizers.configuration.file=./target/test-classes/conf/secure-ldap/authorizers.protected.xml
+nifi.registry.security.authorizer=managed-authorizer
+nifi.registry.security.identity.providers.configuration.file=./target/test-classes/conf/secure-ldap/identity-providers.protected.xml
+nifi.registry.security.identity.provider=ldap-identity-provider
+
+# providers properties #
+nifi.registry.providers.configuration.file=./target/test-classes/conf/providers.xml

http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/6f26290d/nifi-registry-core/nifi-registry-web-api/src/test/resources/conf/secure-ldap/test-ldap-data.ldif
----------------------------------------------------------------------
diff --git a/nifi-registry-core/nifi-registry-web-api/src/test/resources/conf/secure-ldap/test-ldap-data.ldif b/nifi-registry-core/nifi-registry-web-api/src/test/resources/conf/secure-ldap/test-ldap-data.ldif
new file mode 100644
index 0000000..db45689
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-web-api/src/test/resources/conf/secure-ldap/test-ldap-data.ldif
@@ -0,0 +1,261 @@
+#
+# 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.
+#
+
+# extended LDIF
+#
+# LDAPv3
+# base <dc=example,dc=com> with scope subtree
+# filter: objectclass=*
+# requesting: ALL
+#
+# Adapted from Forum Systems' LDAP Test Server
+#
+
+# example.com
+dn: dc=example,dc=com
+objectClass: top
+objectClass: dcObject
+objectClass: organization
+o: example.com
+dc: example
+
+# read-only-admin, example.com
+dn: cn=read-only-admin,dc=example,dc=com
+sn: Read Only Admin
+cn: read-only-admin
+objectClass: inetOrgPerson
+objectClass: organizationalPerson
+objectClass: person
+objectClass: top
+
+# nifiadmin, example.com
+dn: uid=nifiadmin,dc=example,dc=com
+sn: nifiadmin
+objectClass: inetOrgPerson
+objectClass: organizationalPerson
+objectClass: person
+objectClass: top
+uid: nifiadmin
+cn: NiFi Admin
+userPassword: password
+
+# newton, example.com
+dn: uid=newton,dc=example,dc=com
+sn: Newton
+objectClass: inetOrgPerson
+objectClass: organizationalPerson
+objectClass: person
+objectClass: top
+uid: newton
+cn: Isaac Newton
+userPassword: password
+
+# einstein, example.com
+dn: uid=einstein,dc=example,dc=com
+objectClass: inetOrgPerson
+objectClass: organizationalPerson
+objectClass: person
+objectClass: top
+cn: Albert Einstein
+sn: Einstein
+uid: einstein
+userPassword: password
+
+# tesla, example.com
+dn: uid=tesla,dc=example,dc=com
+objectClass: inetOrgPerson
+objectClass: organizationalPerson
+objectClass: person
+objectClass: top
+objectClass: posixAccount
+cn: Nikola Tesla
+sn: Tesla
+uid: tesla
+uidNumber: 88888
+gidNumber: 99999
+homeDirectory: home
+userPassword: password
+
+# galileo, example.com
+dn: uid=galileo,dc=example,dc=com
+objectClass: inetOrgPerson
+objectClass: organizationalPerson
+objectClass: person
+objectClass: top
+cn: Galileo Galilei
+sn: Galilei
+uid: galileo
+mail: galileo@example.com
+userPassword: password
+
+# euler, example.com
+dn: uid=euler,dc=example,dc=com
+objectClass: inetOrgPerson
+objectClass: organizationalPerson
+objectClass: person
+objectClass: top
+uid: euler
+sn: Euler
+cn: Leonhard Euler
+userPassword: password
+
+# gauss, example.com
+dn: uid=gauss,dc=example,dc=com
+objectClass: inetOrgPerson
+objectClass: organizationalPerson
+objectClass: person
+objectClass: top
+cn: Carl Friedrich Gauss
+sn: Gauss
+uid: gauss
+userPassword: password
+
+# riemann, example.com
+dn: uid=riemann,dc=example,dc=com
+objectClass: inetOrgPerson
+objectClass: organizationalPerson
+objectClass: person
+objectClass: top
+cn: Bernhard Riemann
+sn: Riemann
+uid: riemann
+userPassword: password
+
+# euclid, example.com
+dn: uid=euclid,dc=example,dc=com
+uid: euclid
+objectClass: inetOrgPerson
+objectClass: organizationalPerson
+objectClass: person
+objectClass: top
+cn: Euclid
+sn: Euclid
+userPassword: password
+
+# curie, example.com
+dn: uid=curie,dc=example,dc=com
+uid: curie
+objectClass: inetOrgPerson
+objectClass: organizationalPerson
+objectClass: person
+objectClass: top
+cn: Marie Curie
+sn: Curie
+userPassword: password
+
+# nobel, example.com
+dn: uid=nobel,dc=example,dc=com
+uid: nobel
+objectClass: inetOrgPerson
+objectClass: organizationalPerson
+objectClass: person
+objectClass: top
+sn: Nobel
+cn: Alfred Nobel
+userPassword: password
+
+# boyle, example.com
+dn: uid=boyle,dc=example,dc=com
+uid: boyle
+objectClass: inetOrgPerson
+objectClass: organizationalPerson
+objectClass: person
+objectClass: top
+cn: Robert Boyle
+sn: Boyle
+telephoneNumber: 999-867-5309
+userPassword: password
+
+# pasteur, example.com
+dn: uid=pasteur,dc=example,dc=com
+objectClass: inetOrgPerson
+objectClass: organizationalPerson
+objectClass: person
+objectClass: top
+sn: Pasteur
+cn: Louis Pasteur
+uid: pasteur
+telephoneNumber: 602-214-4978
+userPassword: password
+
+# nogroup, example.com
+dn: uid=nogroup,dc=example,dc=com
+uid: nogroup
+objectClass: inetOrgPerson
+objectClass: organizationalPerson
+objectClass: person
+objectClass: top
+cn: No Group
+sn: Group
+
+# test, example.com
+dn: uid=test,dc=example,dc=com
+objectClass: posixAccount
+objectClass: top
+objectClass: inetOrgPerson
+gidNumber: 0
+givenName: Test
+sn: Test
+displayName: Test
+uid: test
+initials: TS
+homeDirectory: home
+cn: Test
+uidNumber: 24601
+o: Company
+
+# mathematicians, example.com
+dn: ou=mathematicians,dc=example,dc=com
+uniqueMember: uid=euclid,dc=example,dc=com
+uniqueMember: uid=riemann,dc=example,dc=com
+uniqueMember: uid=euler,dc=example,dc=com
+uniqueMember: uid=gauss,dc=example,dc=com
+uniqueMember: uid=test,dc=example,dc=com
+ou: mathematicians
+cn: Mathematicians
+objectClass: groupOfUniqueNames
+objectClass: top
+
+# scientists, example.com
+dn: ou=scientists,dc=example,dc=com
+uniqueMember: uid=einstein,dc=example,dc=com
+uniqueMember: uid=galileo,dc=example,dc=com
+uniqueMember: uid=tesla,dc=example,dc=com
+uniqueMember: uid=newton,dc=example,dc=com
+ou: scientists
+cn: Scientists
+objectClass: groupOfUniqueNames
+objectClass: top
+
+# italians, example.com
+dn: ou=italians,dc=example,dc=com
+uniqueMember: uid=galileo,dc=example,dc=com
+ou: italians
+cn: Italians
+objectClass: groupOfUniqueNames
+objectClass: top
+
+# chemists, example.com
+dn: ou=chemists,dc=example,dc=com
+ou: chemists
+objectClass: groupOfUniqueNames
+objectClass: top
+uniqueMember: uid=curie,dc=example,dc=com
+uniqueMember: uid=boyle,dc=example,dc=com
+uniqueMember: uid=nobel,dc=example,dc=com
+uniqueMember: uid=pasteur,dc=example,dc=com
+cn: Chemists

http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/6f26290d/nifi-registry-core/nifi-registry-web-api/src/test/resources/conf/unsecured/nifi-registry.properties
----------------------------------------------------------------------
diff --git a/nifi-registry-core/nifi-registry-web-api/src/test/resources/conf/unsecured/nifi-registry.properties b/nifi-registry-core/nifi-registry-web-api/src/test/resources/conf/unsecured/nifi-registry.properties
new file mode 100644
index 0000000..113773c
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-web-api/src/test/resources/conf/unsecured/nifi-registry.properties
@@ -0,0 +1,25 @@
+#
+# 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.
+#
+
+# web properties #
+nifi.registry.web.http.host=localhost
+
+# providers properties #
+nifi.registry.providers.configuration.file=./target/test-classes/conf/providers.xml
+
+# database properties
+nifi.registry.db.url.append=;LOCK_TIMEOUT=25000;WRITE_DELAY=0;AUTO_SERVER=FALSE
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/6f26290d/nifi-registry-core/nifi-registry-web-api/src/test/resources/db/BucketsIT.sql
----------------------------------------------------------------------
diff --git a/nifi-registry-core/nifi-registry-web-api/src/test/resources/db/BucketsIT.sql b/nifi-registry-core/nifi-registry-web-api/src/test/resources/db/BucketsIT.sql
new file mode 100644
index 0000000..2d6bd23
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-web-api/src/test/resources/db/BucketsIT.sql
@@ -0,0 +1,26 @@
+-- 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.
+
+-- test data for buckets
+
+insert into bucket (id, name, description, created)
+  values ('1', 'Bucket 1', 'This is test bucket 1', parsedatetime('2017-09-11 12:51:00.000 UTC', 'yyyy-MM-dd hh:mm:ss.SSS z'));
+
+insert into bucket (id, name, description, created)
+  values ('2', 'Bucket 2', 'This is test bucket 2', parsedatetime('2017-09-11 12:52:00.000 UTC', 'yyyy-MM-dd hh:mm:ss.SSS z'));
+
+insert into bucket (id, name, description, created)
+  values ('3', 'Bucket 3', 'This is test bucket 3', parsedatetime('2017-09-11 12:53:00.000 UTC', 'yyyy-MM-dd hh:mm:ss.SSS z'));
+

http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/6f26290d/nifi-registry-core/nifi-registry-web-api/src/test/resources/db/FlowsIT.sql
----------------------------------------------------------------------
diff --git a/nifi-registry-core/nifi-registry-web-api/src/test/resources/db/FlowsIT.sql b/nifi-registry-core/nifi-registry-web-api/src/test/resources/db/FlowsIT.sql
new file mode 100644
index 0000000..c36f987
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-web-api/src/test/resources/db/FlowsIT.sql
@@ -0,0 +1,50 @@
+-- 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.
+
+-- test data for buckets
+
+insert into bucket (id, name, description, created)
+  values ('1', 'Bucket 1', 'This is test bucket 1', parsedatetime('2017-09-11 12:51:00.000 UTC', 'yyyy-MM-dd hh:mm:ss.SSS z'));
+
+insert into bucket (id, name, description, created)
+  values ('2', 'Bucket 2', 'This is test bucket 2', parsedatetime('2017-09-11 12:52:00.000 UTC', 'yyyy-MM-dd hh:mm:ss.SSS z'));
+
+insert into bucket (id, name, description, created)
+  values ('3', 'Bucket 3', 'This is test bucket 3', parsedatetime('2017-09-11 12:53:00.000 UTC', 'yyyy-MM-dd hh:mm:ss.SSS z'));
+
+-- test data for flows
+
+insert into bucket_item (id, name, description, created, modified, item_type, bucket_id)
+  values ('1', 'Flow 1', 'This is flow 1', parsedatetime('2017-09-11 12:56:00.000 UTC', 'yyyy-MM-dd hh:mm:ss.SSS z'), parsedatetime('2017-09-11 12:56:00.000 UTC', 'yyyy-MM-dd hh:mm:ss.SSS z'), 'FLOW', '1');
+
+insert into flow (id) values ('1');
+
+insert into bucket_item (id, name, description, created, modified, item_type, bucket_id)
+  values ('2', 'Flow 2', 'This is flow 2', parsedatetime('2017-09-11 12:56:00.000 UTC', 'yyyy-MM-dd hh:mm:ss.SSS z'), parsedatetime('2017-09-11 12:56:00.000 UTC', 'yyyy-MM-dd hh:mm:ss.SSS z'), 'FLOW', '1');
+
+insert into flow (id) values ('2');
+
+insert into bucket_item (id, name, description, created, modified, item_type, bucket_id)
+  values ('3', 'Flow 3', 'This is flow 3', parsedatetime('2017-09-11 12:56:00.000 UTC', 'yyyy-MM-dd hh:mm:ss.SSS z'), parsedatetime('2017-09-11 12:56:00.000 UTC', 'yyyy-MM-dd hh:mm:ss.SSS z'), 'FLOW', '2');
+
+insert into flow (id) values ('3');
+
+-- test data for flow snapshots
+
+insert into flow_snapshot (flow_id, version, created, created_by, comments)
+  values ('1', 1, parsedatetime('2017-09-11 12:57:00.000 UTC', 'yyyy-MM-dd hh:mm:ss.SSS z'), 'user1', 'This is flow 1 snapshot 1');
+
+insert into flow_snapshot (flow_id, version, created, created_by, comments)
+  values ('1', 2, parsedatetime('2017-09-11 12:58:00.000 UTC', 'yyyy-MM-dd hh:mm:ss.SSS z'), 'user2', 'This is flow 1 snapshot 2');


[06/51] [partial] nifi-registry git commit: NIFIREG-201 Refactoring project structure to better isolate extensions

Posted by kd...@apache.org.
http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/6f26290d/nifi-registry-core/nifi-registry-web-ui/src/main/frontend/package-lock.json
----------------------------------------------------------------------
diff --git a/nifi-registry-core/nifi-registry-web-ui/src/main/frontend/package-lock.json b/nifi-registry-core/nifi-registry-web-ui/src/main/frontend/package-lock.json
new file mode 100644
index 0000000..6a85fd7
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-web-ui/src/main/frontend/package-lock.json
@@ -0,0 +1,7817 @@
+{
+  "//": "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.",
+  "name": "nifi-registry",
+  "version": "0.0.1",
+  "lockfileVersion": 1,
+  "requires": true,
+  "dependencies": {
+    "@angular/animations": {
+      "version": "5.2.0",
+      "resolved": "https://registry.npmjs.org/@angular/animations/-/animations-5.2.0.tgz",
+      "integrity": "sha512-JLR42YHiJppO4ruAkFxgbzghUDtHkXHkKPM8udd2qyt16T7e1OX7EEOrrmldUu59CC56tZnJ/32p4SrYmxyBSA==",
+      "requires": {
+        "tslib": "1.8.0"
+      }
+    },
+    "@angular/cdk": {
+      "version": "5.2.0",
+      "resolved": "https://registry.npmjs.org/@angular/cdk/-/cdk-5.2.0.tgz",
+      "integrity": "sha1-Q2j2dJ6RXNzHXTJa4z/bP4WogQg=",
+      "requires": {
+        "tslib": "1.8.0"
+      }
+    },
+    "@angular/common": {
+      "version": "5.2.0",
+      "resolved": "https://registry.npmjs.org/@angular/common/-/common-5.2.0.tgz",
+      "integrity": "sha512-yMFn2isC7/XOs56/2Kzzbb1AASHiwipAPOVFtKe7TdZQClO8fJXwCnk326rzr615+CG0eSBNQWeiFGyWN2riBA==",
+      "requires": {
+        "tslib": "1.8.0"
+      }
+    },
+    "@angular/compiler": {
+      "version": "5.2.0",
+      "resolved": "https://registry.npmjs.org/@angular/compiler/-/compiler-5.2.0.tgz",
+      "integrity": "sha512-RfYa4ESgjGX0T0ob/Xz00IF7nd2xZkoyRy6oKgL82q42uzB3xZUDMrFNgeGxAUs3H22IkL46/5SSPOMOTMZ0NA==",
+      "requires": {
+        "tslib": "1.8.0"
+      }
+    },
+    "@angular/core": {
+      "version": "5.2.0",
+      "resolved": "https://registry.npmjs.org/@angular/core/-/core-5.2.0.tgz",
+      "integrity": "sha512-s2ne45DguNUubhC1YgybGECC4Tyx3G4EZCntUiRMDWWkmKXSK+6dgHMesyDo8R5Oat8VfN4Anf8l3JHS1He8kg==",
+      "requires": {
+        "tslib": "1.8.0"
+      }
+    },
+    "@angular/flex-layout": {
+      "version": "5.0.0-beta.14",
+      "resolved": "https://registry.npmjs.org/@angular/flex-layout/-/flex-layout-5.0.0-beta.14.tgz",
+      "integrity": "sha512-/fsOqXFUKdCmzzZx0bZ0HCYwcV+BSbVuIgOhaCrZKHj2rqiWKKPgj1ErU3HMT68bBBGag0u0skTdLGtrBorRIA==",
+      "requires": {
+        "tslib": "1.8.0"
+      }
+    },
+    "@angular/forms": {
+      "version": "5.2.0",
+      "resolved": "https://registry.npmjs.org/@angular/forms/-/forms-5.2.0.tgz",
+      "integrity": "sha512-g1/SF9lY0ZwzJ0w4NXbFsTGGEuUdgtaZny8DmkaqtmA7idby3FW398X0tv25KQfVYKtL+p9Jp1Y8EI0CvrIsvw==",
+      "requires": {
+        "tslib": "1.8.0"
+      }
+    },
+    "@angular/http": {
+      "version": "5.2.0",
+      "resolved": "https://registry.npmjs.org/@angular/http/-/http-5.2.0.tgz",
+      "integrity": "sha512-V5Cl24dP3rCXTTQvDc0TIKoWqBRAa0DWAQbtr7iuDAt5a1vPGdKz5K1sEiiV6ziwX6gzjiwHjUvL+B+WbIUrQA==",
+      "requires": {
+        "tslib": "1.8.0"
+      }
+    },
+    "@angular/material": {
+      "version": "5.2.0",
+      "resolved": "https://registry.npmjs.org/@angular/material/-/material-5.2.0.tgz",
+      "integrity": "sha1-hZnjFJ1ISH4+kulB+p3FUXbjoM8=",
+      "requires": {
+        "tslib": "1.8.0"
+      }
+    },
+    "@angular/platform-browser": {
+      "version": "5.2.0",
+      "resolved": "https://registry.npmjs.org/@angular/platform-browser/-/platform-browser-5.2.0.tgz",
+      "integrity": "sha512-c6cR15MfopPwGZ097HdRuAi9+R9BhA3bRRFpP2HmrSSB/BW4ZNovUYwB2QUMSYbd9s0lYTtnavqGm6DKcyF2QA==",
+      "requires": {
+        "tslib": "1.8.0"
+      }
+    },
+    "@angular/platform-browser-dynamic": {
+      "version": "5.2.0",
+      "resolved": "https://registry.npmjs.org/@angular/platform-browser-dynamic/-/platform-browser-dynamic-5.2.0.tgz",
+      "integrity": "sha512-xG1eNoi8sm4Jcly2y98r5mqYVe3XV8sUJCtOhvGBYtvt4dKEQ5tOns6fWQ0nUbl6Vv3Y0xgGUS1JCtfut3DuaQ==",
+      "requires": {
+        "tslib": "1.8.0"
+      }
+    },
+    "@angular/router": {
+      "version": "5.2.0",
+      "resolved": "https://registry.npmjs.org/@angular/router/-/router-5.2.0.tgz",
+      "integrity": "sha512-VXDXtp2A1GQEUEhXg0ZzqHdTUERLgDSo3/Mmpzt+dgLMKlXDSCykcm4gINwE5VQLGD1zQvDFCCRv3seGRNfrqA==",
+      "requires": {
+        "tslib": "1.8.0"
+      }
+    },
+    "@covalent/core": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/@covalent/core/-/core-1.0.0.tgz",
+      "integrity": "sha512-qUGL6CtyHNa3ttKGrvuQY0lJyQR9Dxp04vP0vrXmYrKapVbfYZ82qwJ2+PrX1EcWAQ6b/B2giFe0Q83ePin04g==",
+      "requires": {
+        "tslib": "1.8.0"
+      }
+    },
+    "@nifi-fds/core": {
+      "version": "0.1.0",
+      "resolved": "https://registry.npmjs.org/@nifi-fds/core/-/core-0.1.0.tgz",
+      "integrity": "sha512-aUoXOjhgQBZSxmzpBeYupIGvtw+J7Os+6n3q9JS22vcr7uVh588UMb1STKH1nVPRtDgkiU9ooGuXp9hkSCnC0w=="
+    },
+    "@types/node": {
+      "version": "6.0.106",
+      "resolved": "https://registry.npmjs.org/@types/node/-/node-6.0.106.tgz",
+      "integrity": "sha1-ORvDWYq1gjVj9xVYRyEhUok+3Nc=",
+      "dev": true
+    },
+    "@types/q": {
+      "version": "0.0.32",
+      "resolved": "https://registry.npmjs.org/@types/q/-/q-0.0.32.tgz",
+      "integrity": "sha1-vShOV8hPEyXacCur/IKlMoGQwMU=",
+      "dev": true
+    },
+    "@types/selenium-webdriver": {
+      "version": "3.0.10",
+      "resolved": "https://registry.npmjs.org/@types/selenium-webdriver/-/selenium-webdriver-3.0.10.tgz",
+      "integrity": "sha512-ikB0JHv6vCR1KYUQAzTO4gi/lXLElT4Tx+6De2pc/OZwizE9LRNiTa+U8TBFKBD/nntPnr/MPSHSnOTybjhqNA==",
+      "dev": true
+    },
+    "abbrev": {
+      "version": "1.1.1",
+      "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz",
+      "integrity": "sha1-+PLIh60Qv2f2NPAFtph/7TF5qsg=",
+      "dev": true
+    },
+    "accepts": {
+      "version": "1.3.5",
+      "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.5.tgz",
+      "integrity": "sha1-63d99gEXI6OxTopywIBcjoZ0a9I=",
+      "dev": true,
+      "requires": {
+        "mime-types": "2.1.18",
+        "negotiator": "0.6.1"
+      }
+    },
+    "adm-zip": {
+      "version": "0.4.11",
+      "resolved": "https://registry.npmjs.org/adm-zip/-/adm-zip-0.4.11.tgz",
+      "integrity": "sha512-L8vcjDTCOIJk7wFvmlEUN7AsSb8T+2JrdP7KINBjzr24TJ5Mwj590sLu3BC7zNZowvJWa/JtPmD8eJCzdtDWjA==",
+      "dev": true
+    },
+    "after": {
+      "version": "0.8.2",
+      "resolved": "https://registry.npmjs.org/after/-/after-0.8.2.tgz",
+      "integrity": "sha1-/ts5T58OAqqXaOcCvaI7UF+ufh8=",
+      "dev": true
+    },
+    "ajv": {
+      "version": "5.5.2",
+      "resolved": "https://registry.npmjs.org/ajv/-/ajv-5.5.2.tgz",
+      "integrity": "sha1-c7Xuyj+rZT49P5Qis0GtQiBdyWU=",
+      "dev": true,
+      "requires": {
+        "co": "4.6.0",
+        "fast-deep-equal": "1.1.0",
+        "fast-json-stable-stringify": "2.0.0",
+        "json-schema-traverse": "0.3.1"
+      }
+    },
+    "align-text": {
+      "version": "0.1.4",
+      "resolved": "https://registry.npmjs.org/align-text/-/align-text-0.1.4.tgz",
+      "integrity": "sha1-DNkKVhCT810KmSVsIrcGlDP60Rc=",
+      "dev": true,
+      "requires": {
+        "kind-of": "3.2.2",
+        "longest": "1.0.1",
+        "repeat-string": "1.6.1"
+      }
+    },
+    "amdefine": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/amdefine/-/amdefine-1.0.1.tgz",
+      "integrity": "sha1-SlKCrBZHKek2Gbz9OtFR+BfOkfU=",
+      "dev": true
+    },
+    "angular2-jwt": {
+      "version": "0.2.3",
+      "resolved": "https://registry.npmjs.org/angular2-jwt/-/angular2-jwt-0.2.3.tgz",
+      "integrity": "sha1-VO/do87tuoX2o3sWXyKsIrit8CE="
+    },
+    "angular2-moment": {
+      "version": "1.9.0",
+      "resolved": "https://registry.npmjs.org/angular2-moment/-/angular2-moment-1.9.0.tgz",
+      "integrity": "sha512-ybPjYizpKVWAI2Z4AqxAS6s3FMkF3+zRpfvxX1wIdSJUFjl83XxQ5f2yn7retX68NSYZZ/JTK9KGnvOzZfrIZw==",
+      "requires": {
+        "moment": "2.22.1"
+      }
+    },
+    "ansi-regex": {
+      "version": "2.1.1",
+      "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz",
+      "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=",
+      "dev": true
+    },
+    "ansi-styles": {
+      "version": "2.2.1",
+      "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-2.2.1.tgz",
+      "integrity": "sha1-tDLdM1i2NM914eRmQ2gkBTPB3b4=",
+      "dev": true
+    },
+    "aproba": {
+      "version": "1.2.0",
+      "resolved": "https://registry.npmjs.org/aproba/-/aproba-1.2.0.tgz",
+      "integrity": "sha1-aALmJk79GMeQobDVF/DyYnvyyUo=",
+      "dev": true
+    },
+    "archiver": {
+      "version": "1.3.0",
+      "resolved": "https://registry.npmjs.org/archiver/-/archiver-1.3.0.tgz",
+      "integrity": "sha1-TyGU1tj5nfP1MeaIHxTxXVX6ryI=",
+      "dev": true,
+      "requires": {
+        "archiver-utils": "1.3.0",
+        "async": "2.6.0",
+        "buffer-crc32": "0.2.13",
+        "glob": "7.1.2",
+        "lodash": "4.17.10",
+        "readable-stream": "2.3.6",
+        "tar-stream": "1.5.5",
+        "walkdir": "0.0.11",
+        "zip-stream": "1.2.0"
+      },
+      "dependencies": {
+        "async": {
+          "version": "2.6.0",
+          "resolved": "https://registry.npmjs.org/async/-/async-2.6.0.tgz",
+          "integrity": "sha1-YaKau2/MAm/qd+VtHG7FOnlZUfQ=",
+          "dev": true,
+          "requires": {
+            "lodash": "4.17.10"
+          }
+        },
+        "glob": {
+          "version": "7.1.2",
+          "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.2.tgz",
+          "integrity": "sha1-wZyd+aAocC1nhhI4SmVSQExjbRU=",
+          "dev": true,
+          "requires": {
+            "fs.realpath": "1.0.0",
+            "inflight": "1.0.6",
+            "inherits": "2.0.3",
+            "minimatch": "3.0.4",
+            "once": "1.4.0",
+            "path-is-absolute": "1.0.1"
+          }
+        },
+        "minimatch": {
+          "version": "3.0.4",
+          "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz",
+          "integrity": "sha1-UWbihkV/AzBgZL5Ul+jbsMPTIIM=",
+          "dev": true,
+          "requires": {
+            "brace-expansion": "1.1.11"
+          }
+        }
+      }
+    },
+    "archiver-utils": {
+      "version": "1.3.0",
+      "resolved": "https://registry.npmjs.org/archiver-utils/-/archiver-utils-1.3.0.tgz",
+      "integrity": "sha1-5QtMCccL89aA4y/xt5lOn52JUXQ=",
+      "dev": true,
+      "requires": {
+        "glob": "7.1.2",
+        "graceful-fs": "4.1.11",
+        "lazystream": "1.0.0",
+        "lodash": "4.17.10",
+        "normalize-path": "2.1.1",
+        "readable-stream": "2.3.6"
+      },
+      "dependencies": {
+        "glob": {
+          "version": "7.1.2",
+          "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.2.tgz",
+          "integrity": "sha1-wZyd+aAocC1nhhI4SmVSQExjbRU=",
+          "dev": true,
+          "requires": {
+            "fs.realpath": "1.0.0",
+            "inflight": "1.0.6",
+            "inherits": "2.0.3",
+            "minimatch": "3.0.4",
+            "once": "1.4.0",
+            "path-is-absolute": "1.0.1"
+          }
+        },
+        "graceful-fs": {
+          "version": "4.1.11",
+          "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.1.11.tgz",
+          "integrity": "sha1-Dovf5NHduIVNZOBOp8AOKgJuVlg=",
+          "dev": true
+        },
+        "minimatch": {
+          "version": "3.0.4",
+          "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz",
+          "integrity": "sha1-UWbihkV/AzBgZL5Ul+jbsMPTIIM=",
+          "dev": true,
+          "requires": {
+            "brace-expansion": "1.1.11"
+          }
+        }
+      }
+    },
+    "are-we-there-yet": {
+      "version": "1.1.4",
+      "resolved": "https://registry.npmjs.org/are-we-there-yet/-/are-we-there-yet-1.1.4.tgz",
+      "integrity": "sha1-u13KOCu5TwXhUZQ3PRb9O6HKEQ0=",
+      "dev": true,
+      "requires": {
+        "delegates": "1.0.0",
+        "readable-stream": "2.3.6"
+      }
+    },
+    "arr-diff": {
+      "version": "4.0.0",
+      "resolved": "https://registry.npmjs.org/arr-diff/-/arr-diff-4.0.0.tgz",
+      "integrity": "sha1-1kYQdP6/7HHn4VI1dhoyml3HxSA=",
+      "dev": true
+    },
+    "arr-flatten": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmjs.org/arr-flatten/-/arr-flatten-1.1.0.tgz",
+      "integrity": "sha1-NgSLv/TntH4TZkQxbJlmnqWukfE=",
+      "dev": true
+    },
+    "arr-union": {
+      "version": "3.1.0",
+      "resolved": "https://registry.npmjs.org/arr-union/-/arr-union-3.1.0.tgz",
+      "integrity": "sha1-45sJrqne+Gao8gbiiK9jkZuuOcQ=",
+      "dev": true
+    },
+    "array-differ": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/array-differ/-/array-differ-1.0.0.tgz",
+      "integrity": "sha1-7/UuN1gknTO+QCuLuOVkuytdQDE=",
+      "dev": true
+    },
+    "array-each": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/array-each/-/array-each-1.0.1.tgz",
+      "integrity": "sha1-p5SvDAWrF1KEbudTofIRoFugxE8=",
+      "dev": true
+    },
+    "array-find-index": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmjs.org/array-find-index/-/array-find-index-1.0.2.tgz",
+      "integrity": "sha1-3wEKoSh+Fku9pvlyOwqWoexBh6E=",
+      "dev": true
+    },
+    "array-slice": {
+      "version": "0.2.3",
+      "resolved": "https://registry.npmjs.org/array-slice/-/array-slice-0.2.3.tgz",
+      "integrity": "sha1-3Tz7gO15c6dRF82sabC5nshhhvU=",
+      "dev": true
+    },
+    "array-union": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmjs.org/array-union/-/array-union-1.0.2.tgz",
+      "integrity": "sha1-mjRBDk9OPaI96jdb5b5w8kd47Dk=",
+      "dev": true,
+      "requires": {
+        "array-uniq": "1.0.3"
+      }
+    },
+    "array-uniq": {
+      "version": "1.0.3",
+      "resolved": "https://registry.npmjs.org/array-uniq/-/array-uniq-1.0.3.tgz",
+      "integrity": "sha1-r2rId6Jcx/dOBYiUdThY39sk/bY=",
+      "dev": true
+    },
+    "array-unique": {
+      "version": "0.2.1",
+      "resolved": "https://registry.npmjs.org/array-unique/-/array-unique-0.2.1.tgz",
+      "integrity": "sha1-odl8yvy8JiXMcPrc6zalDFiwGlM=",
+      "dev": true
+    },
+    "arraybuffer.slice": {
+      "version": "0.0.7",
+      "resolved": "https://registry.npmjs.org/arraybuffer.slice/-/arraybuffer.slice-0.0.7.tgz",
+      "integrity": "sha512-wGUIVQXuehL5TCqQun8OW81jGzAWycqzFF8lFp+GOM5BXLYj3bKNsYC4daB7n6XjCqxQA/qgTJ+8ANR3acjrog==",
+      "dev": true
+    },
+    "arrify": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/arrify/-/arrify-1.0.1.tgz",
+      "integrity": "sha1-iYUI2iIm84DfkEcoRWhJwVAaSw0=",
+      "dev": true
+    },
+    "asn1": {
+      "version": "0.2.3",
+      "resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.3.tgz",
+      "integrity": "sha1-2sh4dxPJlmhJ/IGAd36+nB3fO4Y=",
+      "dev": true
+    },
+    "assert-plus": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz",
+      "integrity": "sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU=",
+      "dev": true
+    },
+    "assign-symbols": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/assign-symbols/-/assign-symbols-1.0.0.tgz",
+      "integrity": "sha1-WWZ/QfrdTyDMvCu5a41Pf3jsA2c=",
+      "dev": true
+    },
+    "async-each": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/async-each/-/async-each-1.0.1.tgz",
+      "integrity": "sha1-GdOGodntxufByF04iu28xW0zYC0=",
+      "dev": true
+    },
+    "async-foreach": {
+      "version": "0.1.3",
+      "resolved": "https://registry.npmjs.org/async-foreach/-/async-foreach-0.1.3.tgz",
+      "integrity": "sha1-NhIfhFwFeBct5Bmpfb6x0W7DRUI=",
+      "dev": true
+    },
+    "async-limiter": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/async-limiter/-/async-limiter-1.0.0.tgz",
+      "integrity": "sha512-jp/uFnooOiO+L211eZOoSyzpOITMXx1rBITauYykG3BRYPu8h0UcxsPNB04RR5vo4Tyz3+ay17tR6JVf9qzYWg==",
+      "dev": true
+    },
+    "asynckit": {
+      "version": "0.4.0",
+      "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
+      "integrity": "sha1-x57Zf380y48robyXkLzDZkdLS3k="
+    },
+    "atob": {
+      "version": "2.1.1",
+      "resolved": "https://registry.npmjs.org/atob/-/atob-2.1.1.tgz",
+      "integrity": "sha1-ri1acpR38onWDdf5amMUoi3Wwio=",
+      "dev": true
+    },
+    "aws-sign2": {
+      "version": "0.7.0",
+      "resolved": "https://registry.npmjs.org/aws-sign2/-/aws-sign2-0.7.0.tgz",
+      "integrity": "sha1-tG6JCTSpWR8tL2+G1+ap8bP+dqg=",
+      "dev": true
+    },
+    "aws4": {
+      "version": "1.7.0",
+      "resolved": "https://registry.npmjs.org/aws4/-/aws4-1.7.0.tgz",
+      "integrity": "sha1-1NDpudv8p3vwjusKikcVUP454ok=",
+      "dev": true
+    },
+    "babel-code-frame": {
+      "version": "6.26.0",
+      "resolved": "https://registry.npmjs.org/babel-code-frame/-/babel-code-frame-6.26.0.tgz",
+      "integrity": "sha1-Y/1D99weO7fONZR9uP42mj9Yx0s=",
+      "dev": true,
+      "requires": {
+        "chalk": "1.1.3",
+        "esutils": "2.0.2",
+        "js-tokens": "3.0.2"
+      }
+    },
+    "babel-core": {
+      "version": "6.26.0",
+      "resolved": "https://registry.npmjs.org/babel-core/-/babel-core-6.26.0.tgz",
+      "integrity": "sha1-rzL3izGm/O8RnIew/Y2XU/A6C7g=",
+      "dev": true,
+      "requires": {
+        "babel-code-frame": "6.26.0",
+        "babel-generator": "6.26.1",
+        "babel-helpers": "6.24.1",
+        "babel-messages": "6.23.0",
+        "babel-register": "6.26.0",
+        "babel-runtime": "6.26.0",
+        "babel-template": "6.26.0",
+        "babel-traverse": "6.26.0",
+        "babel-types": "6.26.0",
+        "babylon": "6.18.0",
+        "convert-source-map": "1.5.1",
+        "debug": "2.6.9",
+        "json5": "0.5.1",
+        "lodash": "4.17.5",
+        "minimatch": "3.0.4",
+        "path-is-absolute": "1.0.1",
+        "private": "0.1.8",
+        "slash": "1.0.0",
+        "source-map": "0.5.7"
+      },
+      "dependencies": {
+        "debug": {
+          "version": "2.6.9",
+          "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
+          "integrity": "sha1-XRKFFd8TT/Mn6QpMk/Tgd6U2NB8=",
+          "dev": true,
+          "requires": {
+            "ms": "2.0.0"
+          }
+        },
+        "lodash": {
+          "version": "4.17.5",
+          "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.5.tgz",
+          "integrity": "sha1-maktZcAnLevoyWtgV7yPv6O+1RE=",
+          "dev": true
+        },
+        "minimatch": {
+          "version": "3.0.4",
+          "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz",
+          "integrity": "sha1-UWbihkV/AzBgZL5Ul+jbsMPTIIM=",
+          "dev": true,
+          "requires": {
+            "brace-expansion": "1.1.11"
+          }
+        },
+        "source-map": {
+          "version": "0.5.7",
+          "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz",
+          "integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=",
+          "dev": true
+        }
+      }
+    },
+    "babel-generator": {
+      "version": "6.26.1",
+      "resolved": "https://registry.npmjs.org/babel-generator/-/babel-generator-6.26.1.tgz",
+      "integrity": "sha1-GERAjTuPDTWkBOp6wYDwh6YBvZA=",
+      "dev": true,
+      "requires": {
+        "babel-messages": "6.23.0",
+        "babel-runtime": "6.26.0",
+        "babel-types": "6.26.0",
+        "detect-indent": "4.0.0",
+        "jsesc": "1.3.0",
+        "lodash": "4.17.5",
+        "source-map": "0.5.7",
+        "trim-right": "1.0.1"
+      },
+      "dependencies": {
+        "lodash": {
+          "version": "4.17.5",
+          "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.5.tgz",
+          "integrity": "sha1-maktZcAnLevoyWtgV7yPv6O+1RE=",
+          "dev": true
+        },
+        "source-map": {
+          "version": "0.5.7",
+          "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz",
+          "integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=",
+          "dev": true
+        }
+      }
+    },
+    "babel-helper-hoist-variables": {
+      "version": "6.24.1",
+      "resolved": "https://registry.npmjs.org/babel-helper-hoist-variables/-/babel-helper-hoist-variables-6.24.1.tgz",
+      "integrity": "sha1-HssnaJydJVE+rbyZFKc/VAi+enY=",
+      "dev": true,
+      "requires": {
+        "babel-runtime": "6.26.0",
+        "babel-types": "6.26.0"
+      }
+    },
+    "babel-helpers": {
+      "version": "6.24.1",
+      "resolved": "https://registry.npmjs.org/babel-helpers/-/babel-helpers-6.24.1.tgz",
+      "integrity": "sha1-NHHenK7DiOXIUOWX5Yom3fN2ArI=",
+      "dev": true,
+      "requires": {
+        "babel-runtime": "6.26.0",
+        "babel-template": "6.26.0"
+      }
+    },
+    "babel-messages": {
+      "version": "6.23.0",
+      "resolved": "https://registry.npmjs.org/babel-messages/-/babel-messages-6.23.0.tgz",
+      "integrity": "sha1-8830cDhYA1sqKVHG7F7fbGLyYw4=",
+      "dev": true,
+      "requires": {
+        "babel-runtime": "6.26.0"
+      }
+    },
+    "babel-plugin-syntax-dynamic-import": {
+      "version": "6.18.0",
+      "resolved": "https://registry.npmjs.org/babel-plugin-syntax-dynamic-import/-/babel-plugin-syntax-dynamic-import-6.18.0.tgz",
+      "integrity": "sha1-jWomIpyDdFqZgqRBBRVyyqF5sdo=",
+      "dev": true
+    },
+    "babel-plugin-transform-amd-system-wrapper": {
+      "version": "0.3.7",
+      "resolved": "https://registry.npmjs.org/babel-plugin-transform-amd-system-wrapper/-/babel-plugin-transform-amd-system-wrapper-0.3.7.tgz",
+      "integrity": "sha1-Uhx4LTVkRJHJeepoPopeHK/wukI=",
+      "dev": true,
+      "requires": {
+        "babel-template": "6.26.0"
+      }
+    },
+    "babel-plugin-transform-cjs-system-wrapper": {
+      "version": "0.6.2",
+      "resolved": "https://registry.npmjs.org/babel-plugin-transform-cjs-system-wrapper/-/babel-plugin-transform-cjs-system-wrapper-0.6.2.tgz",
+      "integrity": "sha1-vXSUd1KJQk/0k7btRV3klb1xuh0=",
+      "dev": true,
+      "requires": {
+        "babel-template": "6.26.0"
+      }
+    },
+    "babel-plugin-transform-es2015-modules-systemjs": {
+      "version": "6.24.1",
+      "resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-modules-systemjs/-/babel-plugin-transform-es2015-modules-systemjs-6.24.1.tgz",
+      "integrity": "sha1-/4mhQrkRmpBhlfXxBuzzBdlAfSM=",
+      "dev": true,
+      "requires": {
+        "babel-helper-hoist-variables": "6.24.1",
+        "babel-runtime": "6.26.0",
+        "babel-template": "6.26.0"
+      }
+    },
+    "babel-plugin-transform-global-system-wrapper": {
+      "version": "0.3.4",
+      "resolved": "https://registry.npmjs.org/babel-plugin-transform-global-system-wrapper/-/babel-plugin-transform-global-system-wrapper-0.3.4.tgz",
+      "integrity": "sha1-lI3X0p/CFEfjm9NEfy3rx/L3Oqw=",
+      "dev": true,
+      "requires": {
+        "babel-template": "6.26.0"
+      }
+    },
+    "babel-plugin-transform-system-register": {
+      "version": "0.0.1",
+      "resolved": "https://registry.npmjs.org/babel-plugin-transform-system-register/-/babel-plugin-transform-system-register-0.0.1.tgz",
+      "integrity": "sha1-nf9AOQwnY6xRjwsq18XqT2WlviU=",
+      "dev": true
+    },
+    "babel-register": {
+      "version": "6.26.0",
+      "resolved": "https://registry.npmjs.org/babel-register/-/babel-register-6.26.0.tgz",
+      "integrity": "sha1-btAhFz4vy0htestFxgCahW9kcHE=",
+      "dev": true,
+      "requires": {
+        "babel-core": "6.26.0",
+        "babel-runtime": "6.26.0",
+        "core-js": "2.5.5",
+        "home-or-tmp": "2.0.0",
+        "lodash": "4.17.5",
+        "mkdirp": "0.5.1",
+        "source-map-support": "0.4.18"
+      },
+      "dependencies": {
+        "lodash": {
+          "version": "4.17.5",
+          "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.5.tgz",
+          "integrity": "sha1-maktZcAnLevoyWtgV7yPv6O+1RE=",
+          "dev": true
+        }
+      }
+    },
+    "babel-runtime": {
+      "version": "6.26.0",
+      "resolved": "https://registry.npmjs.org/babel-runtime/-/babel-runtime-6.26.0.tgz",
+      "integrity": "sha1-llxwWGaOgrVde/4E/yM3vItWR/4=",
+      "dev": true,
+      "requires": {
+        "core-js": "2.5.5",
+        "regenerator-runtime": "0.11.1"
+      }
+    },
+    "babel-template": {
+      "version": "6.26.0",
+      "resolved": "https://registry.npmjs.org/babel-template/-/babel-template-6.26.0.tgz",
+      "integrity": "sha1-3gPi0WOWsGn0bdn/+FIfsaDjXgI=",
+      "dev": true,
+      "requires": {
+        "babel-runtime": "6.26.0",
+        "babel-traverse": "6.26.0",
+        "babel-types": "6.26.0",
+        "babylon": "6.18.0",
+        "lodash": "4.17.5"
+      },
+      "dependencies": {
+        "lodash": {
+          "version": "4.17.5",
+          "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.5.tgz",
+          "integrity": "sha1-maktZcAnLevoyWtgV7yPv6O+1RE=",
+          "dev": true
+        }
+      }
+    },
+    "babel-traverse": {
+      "version": "6.26.0",
+      "resolved": "https://registry.npmjs.org/babel-traverse/-/babel-traverse-6.26.0.tgz",
+      "integrity": "sha1-RqnL1+3MYsjlwGTi0tjQ9ANXZu4=",
+      "dev": true,
+      "requires": {
+        "babel-code-frame": "6.26.0",
+        "babel-messages": "6.23.0",
+        "babel-runtime": "6.26.0",
+        "babel-types": "6.26.0",
+        "babylon": "6.18.0",
+        "debug": "2.6.9",
+        "globals": "9.18.0",
+        "invariant": "2.2.4",
+        "lodash": "4.17.5"
+      },
+      "dependencies": {
+        "debug": {
+          "version": "2.6.9",
+          "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
+          "integrity": "sha1-XRKFFd8TT/Mn6QpMk/Tgd6U2NB8=",
+          "dev": true,
+          "requires": {
+            "ms": "2.0.0"
+          }
+        },
+        "lodash": {
+          "version": "4.17.5",
+          "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.5.tgz",
+          "integrity": "sha1-maktZcAnLevoyWtgV7yPv6O+1RE=",
+          "dev": true
+        }
+      }
+    },
+    "babel-types": {
+      "version": "6.26.0",
+      "resolved": "https://registry.npmjs.org/babel-types/-/babel-types-6.26.0.tgz",
+      "integrity": "sha1-o7Bz+Uq0nrb6Vc1lInozQ4BjJJc=",
+      "dev": true,
+      "requires": {
+        "babel-runtime": "6.26.0",
+        "esutils": "2.0.2",
+        "lodash": "4.17.5",
+        "to-fast-properties": "1.0.3"
+      },
+      "dependencies": {
+        "lodash": {
+          "version": "4.17.5",
+          "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.5.tgz",
+          "integrity": "sha1-maktZcAnLevoyWtgV7yPv6O+1RE=",
+          "dev": true
+        }
+      }
+    },
+    "babylon": {
+      "version": "6.18.0",
+      "resolved": "https://registry.npmjs.org/babylon/-/babylon-6.18.0.tgz",
+      "integrity": "sha1-ry87iPpvXB5MY00aD46sT1WzleM=",
+      "dev": true
+    },
+    "backo2": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmjs.org/backo2/-/backo2-1.0.2.tgz",
+      "integrity": "sha1-MasayLEpNjRj41s+u2n038+6eUc=",
+      "dev": true
+    },
+    "balanced-match": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz",
+      "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=",
+      "dev": true
+    },
+    "base": {
+      "version": "0.11.2",
+      "resolved": "https://registry.npmjs.org/base/-/base-0.11.2.tgz",
+      "integrity": "sha512-5T6P4xPgpp0YDFvSWwEZ4NoE3aM4QBQXDzmVbraCkFj8zHM+mba8SyqB5DbZWyR7mYHo6Y7BdQo3MoA4m0TeQg==",
+      "dev": true,
+      "requires": {
+        "cache-base": "1.0.1",
+        "class-utils": "0.3.6",
+        "component-emitter": "1.2.1",
+        "define-property": "1.0.0",
+        "isobject": "3.0.1",
+        "mixin-deep": "1.3.1",
+        "pascalcase": "0.1.1"
+      },
+      "dependencies": {
+        "define-property": {
+          "version": "1.0.0",
+          "resolved": "https://registry.npmjs.org/define-property/-/define-property-1.0.0.tgz",
+          "integrity": "sha1-dp66rz9KY6rTr56NMEybvnm/sOY=",
+          "dev": true,
+          "requires": {
+            "is-descriptor": "1.0.2"
+          }
+        },
+        "is-accessor-descriptor": {
+          "version": "1.0.0",
+          "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz",
+          "integrity": "sha512-m5hnHTkcVsPfqx3AKlyttIPb7J+XykHvJP2B9bZDjlhLIoEq4XoK64Vg7boZlVWYK6LUY94dYPEE7Lh0ZkZKcQ==",
+          "dev": true,
+          "requires": {
+            "kind-of": "6.0.2"
+          }
+        },
+        "is-data-descriptor": {
+          "version": "1.0.0",
+          "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz",
+          "integrity": "sha512-jbRXy1FmtAoCjQkVmIVYwuuqDFUbaOeDjmed1tOGPrsMhtJA4rD9tkgA0F1qJ3gRFRXcHYVkdeaP50Q5rE/jLQ==",
+          "dev": true,
+          "requires": {
+            "kind-of": "6.0.2"
+          }
+        },
+        "is-descriptor": {
+          "version": "1.0.2",
+          "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-1.0.2.tgz",
+          "integrity": "sha512-2eis5WqQGV7peooDyLmNEPUrps9+SXX5c9pL3xEB+4e9HnGuDa7mB7kHxHw4CbqS9k1T2hOH3miL8n8WtiYVtg==",
+          "dev": true,
+          "requires": {
+            "is-accessor-descriptor": "1.0.0",
+            "is-data-descriptor": "1.0.0",
+            "kind-of": "6.0.2"
+          }
+        },
+        "isobject": {
+          "version": "3.0.1",
+          "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz",
+          "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=",
+          "dev": true
+        },
+        "kind-of": {
+          "version": "6.0.2",
+          "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.2.tgz",
+          "integrity": "sha512-s5kLOcnH0XqDO+FvuaLX8DDjZ18CGFk7VygH40QoKPUQhW4e2rvM0rwUq0t8IQDOwYSeLK01U90OjzBTme2QqA==",
+          "dev": true
+        }
+      }
+    },
+    "base64-arraybuffer": {
+      "version": "0.1.5",
+      "resolved": "https://registry.npmjs.org/base64-arraybuffer/-/base64-arraybuffer-0.1.5.tgz",
+      "integrity": "sha1-c5JncZI7Whl0etZmqlzUv5xunOg=",
+      "dev": true
+    },
+    "base64id": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/base64id/-/base64id-1.0.0.tgz",
+      "integrity": "sha1-R2iMuZu2gE8OBtPnY7HDLlfY5rY=",
+      "dev": true
+    },
+    "bcrypt-pbkdf": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.1.tgz",
+      "integrity": "sha1-Y7xdy2EzG5K8Bf1SiVPDNGKgb40=",
+      "dev": true,
+      "optional": true,
+      "requires": {
+        "tweetnacl": "0.14.5"
+      }
+    },
+    "better-assert": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmjs.org/better-assert/-/better-assert-1.0.2.tgz",
+      "integrity": "sha1-QIZrnhueC1W0gYlDEeaPr/rrxSI=",
+      "dev": true,
+      "requires": {
+        "callsite": "1.0.0"
+      }
+    },
+    "binary-extensions": {
+      "version": "1.11.0",
+      "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-1.11.0.tgz",
+      "integrity": "sha1-RqoXUftqL5PuXmibsQh9SxTGwgU=",
+      "dev": true
+    },
+    "bl": {
+      "version": "1.2.2",
+      "resolved": "https://registry.npmjs.org/bl/-/bl-1.2.2.tgz",
+      "integrity": "sha1-oWCRFxcQPAdBDO9j71Gzl8Alr5w=",
+      "dev": true,
+      "requires": {
+        "readable-stream": "2.3.6",
+        "safe-buffer": "5.1.1"
+      }
+    },
+    "blob": {
+      "version": "0.0.4",
+      "resolved": "https://registry.npmjs.org/blob/-/blob-0.0.4.tgz",
+      "integrity": "sha1-vPEwUspURj8w+fx+lbmkdjCpSSE=",
+      "dev": true
+    },
+    "block-stream": {
+      "version": "0.0.9",
+      "resolved": "https://registry.npmjs.org/block-stream/-/block-stream-0.0.9.tgz",
+      "integrity": "sha1-E+v+d4oDIFz+A3UUgeu0szAMEmo=",
+      "dev": true,
+      "requires": {
+        "inherits": "2.0.3"
+      }
+    },
+    "blocking-proxy": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/blocking-proxy/-/blocking-proxy-1.0.1.tgz",
+      "integrity": "sha512-KE8NFMZr3mN2E0HcvCgRtX7DjhiIQrwle+nSVJVC/yqFb9+xznHl2ZcoBp2L9qzkI4t4cBFJ1efXF8Dwi132RA==",
+      "dev": true,
+      "requires": {
+        "minimist": "1.2.0"
+      },
+      "dependencies": {
+        "minimist": {
+          "version": "1.2.0",
+          "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz",
+          "integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=",
+          "dev": true
+        }
+      }
+    },
+    "bluebird": {
+      "version": "3.5.1",
+      "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.5.1.tgz",
+      "integrity": "sha1-2VUfnemPH82h5oPRfukaBgLuLrk=",
+      "dev": true
+    },
+    "body-parser": {
+      "version": "1.18.2",
+      "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.18.2.tgz",
+      "integrity": "sha1-h2eKGdhLR9hZuDGZvVm84iKxBFQ=",
+      "dev": true,
+      "requires": {
+        "bytes": "3.0.0",
+        "content-type": "1.0.4",
+        "debug": "2.6.9",
+        "depd": "1.1.2",
+        "http-errors": "1.6.3",
+        "iconv-lite": "0.4.19",
+        "on-finished": "2.3.0",
+        "qs": "6.5.1",
+        "raw-body": "2.3.2",
+        "type-is": "1.6.16"
+      },
+      "dependencies": {
+        "debug": {
+          "version": "2.6.9",
+          "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
+          "integrity": "sha1-XRKFFd8TT/Mn6QpMk/Tgd6U2NB8=",
+          "dev": true,
+          "requires": {
+            "ms": "2.0.0"
+          }
+        },
+        "iconv-lite": {
+          "version": "0.4.19",
+          "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.19.tgz",
+          "integrity": "sha1-90aPYBNfXl2tM5nAqBvpoWA6CCs=",
+          "dev": true
+        }
+      }
+    },
+    "boom": {
+      "version": "4.3.1",
+      "resolved": "https://registry.npmjs.org/boom/-/boom-4.3.1.tgz",
+      "integrity": "sha1-T4owBctKfjiJ90kDD9JbluAdLjE=",
+      "dev": true,
+      "optional": true,
+      "dependencies": {
+        "hoek": {
+          "version": "5.0.3",
+          "resolved": "https://registry.npmjs.org/hoek/-/hoek-5.0.3.tgz",
+          "integrity": "sha512-Bmr56pxML1c9kU+NS51SMFkiVQAb+9uFfXwyqR2tn4w2FPvmPt65eZ9aCcEfRXd9G74HkZnILC6p967pED4aiw=="
+        }
+      }
+    },
+    "brace-expansion": {
+      "version": "1.1.11",
+      "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
+      "integrity": "sha1-PH/L9SnYcibz0vUrlm/1Jx60Qd0=",
+      "dev": true,
+      "requires": {
+        "balanced-match": "1.0.0",
+        "concat-map": "0.0.1"
+      }
+    },
+    "braces": {
+      "version": "2.3.2",
+      "resolved": "https://registry.npmjs.org/braces/-/braces-2.3.2.tgz",
+      "integrity": "sha512-aNdbnj9P8PjdXU4ybaWLK2IF3jc/EoDYbC7AazW6to3TRsfXxscC9UXOB5iDiEQrkyIbWp2SLQda4+QAa7nc3w==",
+      "dev": true,
+      "requires": {
+        "arr-flatten": "1.1.0",
+        "array-unique": "0.3.2",
+        "extend-shallow": "2.0.1",
+        "fill-range": "4.0.0",
+        "isobject": "3.0.1",
+        "repeat-element": "1.1.2",
+        "snapdragon": "0.8.2",
+        "snapdragon-node": "2.1.1",
+        "split-string": "3.1.0",
+        "to-regex": "3.0.2"
+      },
+      "dependencies": {
+        "array-unique": {
+          "version": "0.3.2",
+          "resolved": "https://registry.npmjs.org/array-unique/-/array-unique-0.3.2.tgz",
+          "integrity": "sha1-qJS3XUvE9s1nnvMkSp/Y9Gri1Cg=",
+          "dev": true
+        },
+        "extend-shallow": {
+          "version": "2.0.1",
+          "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz",
+          "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=",
+          "dev": true,
+          "requires": {
+            "is-extendable": "0.1.1"
+          }
+        }
+      }
+    },
+    "browserstack": {
+      "version": "1.5.1",
+      "resolved": "https://registry.npmjs.org/browserstack/-/browserstack-1.5.1.tgz",
+      "integrity": "sha512-O8VMT64P9NOLhuIoD4YngyxBURefaSdR4QdhG8l6HZ9VxtU7jc3m6jLufFwKA5gaf7fetfB2TnRJnMxyob+heg==",
+      "dev": true,
+      "requires": {
+        "https-proxy-agent": "2.2.1"
+      }
+    },
+    "buffer-crc32": {
+      "version": "0.2.13",
+      "resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz",
+      "integrity": "sha1-DTM+PwDqxQqhRUq9MO+MKl2ackI=",
+      "dev": true
+    },
+    "builtin-modules": {
+      "version": "1.1.1",
+      "resolved": "https://registry.npmjs.org/builtin-modules/-/builtin-modules-1.1.1.tgz",
+      "integrity": "sha1-Jw8HbFpywC9bZaR9+Uxf46J4iS8=",
+      "dev": true
+    },
+    "bytes": {
+      "version": "3.0.0",
+      "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.0.0.tgz",
+      "integrity": "sha1-0ygVQE1olpn4Wk6k+odV3ROpYEg=",
+      "dev": true
+    },
+    "cache-base": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/cache-base/-/cache-base-1.0.1.tgz",
+      "integrity": "sha512-AKcdTnFSWATd5/GCPRxr2ChwIJ85CeyrEyjRHlKxQ56d4XJMGym0uAiKn0xbLOGOl3+yRpOTi484dVCEc5AUzQ==",
+      "dev": true,
+      "requires": {
+        "collection-visit": "1.0.0",
+        "component-emitter": "1.2.1",
+        "get-value": "2.0.6",
+        "has-value": "1.0.0",
+        "isobject": "3.0.1",
+        "set-value": "2.0.0",
+        "to-object-path": "0.3.0",
+        "union-value": "1.0.0",
+        "unset-value": "1.0.0"
+      },
+      "dependencies": {
+        "isobject": {
+          "version": "3.0.1",
+          "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz",
+          "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=",
+          "dev": true
+        }
+      }
+    },
+    "callsite": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/callsite/-/callsite-1.0.0.tgz",
+      "integrity": "sha1-KAOY5dZkvXQDi28JBRU+borxvCA=",
+      "dev": true
+    },
+    "camelcase": {
+      "version": "2.1.1",
+      "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-2.1.1.tgz",
+      "integrity": "sha1-fB0W1nmhu+WcoCys7PsBHiAfWh8=",
+      "dev": true
+    },
+    "camelcase-keys": {
+      "version": "2.1.0",
+      "resolved": "https://registry.npmjs.org/camelcase-keys/-/camelcase-keys-2.1.0.tgz",
+      "integrity": "sha1-MIvur/3ygRkFHvodkyITyRuPkuc=",
+      "dev": true,
+      "requires": {
+        "camelcase": "2.1.1",
+        "map-obj": "1.0.1"
+      }
+    },
+    "caseless": {
+      "version": "0.12.0",
+      "resolved": "https://registry.npmjs.org/caseless/-/caseless-0.12.0.tgz",
+      "integrity": "sha1-G2gcIf+EAzyCZUMJBolCDRhxUdw=",
+      "dev": true
+    },
+    "center-align": {
+      "version": "0.1.3",
+      "resolved": "https://registry.npmjs.org/center-align/-/center-align-0.1.3.tgz",
+      "integrity": "sha1-qg0yYptu6XIgBBHL1EYckHvCt60=",
+      "dev": true,
+      "requires": {
+        "align-text": "0.1.4",
+        "lazy-cache": "1.0.4"
+      }
+    },
+    "chalk": {
+      "version": "1.1.3",
+      "resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz",
+      "integrity": "sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg=",
+      "dev": true,
+      "requires": {
+        "ansi-styles": "2.2.1",
+        "escape-string-regexp": "1.0.5",
+        "has-ansi": "2.0.0",
+        "strip-ansi": "3.0.1",
+        "supports-color": "2.0.0"
+      }
+    },
+    "chownr": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.0.1.tgz",
+      "integrity": "sha1-4qdQQqlVGQi+vSW4Uj1fl2nXkYE=",
+      "dev": true,
+      "optional": true
+    },
+    "circular-json": {
+      "version": "0.5.5",
+      "resolved": "https://registry.npmjs.org/circular-json/-/circular-json-0.5.5.tgz",
+      "integrity": "sha512-13YaR6kiz0kBNmIVM87Io8Hp7bWOo4r61vkEANy8iH9R9bc6avud/1FT0SBpqR1RpIQADOh/Q+yHZDA1iL6ysA==",
+      "dev": true
+    },
+    "class-utils": {
+      "version": "0.3.6",
+      "resolved": "https://registry.npmjs.org/class-utils/-/class-utils-0.3.6.tgz",
+      "integrity": "sha512-qOhPa/Fj7s6TY8H8esGu5QNpMMQxz79h+urzrNYN6mn+9BnxlDGf5QZ+XeCDsxSjPqsSR56XOZOJmpeurnLMeg==",
+      "dev": true,
+      "requires": {
+        "arr-union": "3.1.0",
+        "define-property": "0.2.5",
+        "isobject": "3.0.1",
+        "static-extend": "0.1.2"
+      },
+      "dependencies": {
+        "define-property": {
+          "version": "0.2.5",
+          "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz",
+          "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=",
+          "dev": true,
+          "requires": {
+            "is-descriptor": "0.1.6"
+          }
+        },
+        "isobject": {
+          "version": "3.0.1",
+          "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz",
+          "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=",
+          "dev": true
+        }
+      }
+    },
+    "cliui": {
+      "version": "3.2.0",
+      "resolved": "https://registry.npmjs.org/cliui/-/cliui-3.2.0.tgz",
+      "integrity": "sha1-EgYBU3qRbSmUD5NNo7SNWFo5IT0=",
+      "dev": true,
+      "requires": {
+        "string-width": "1.0.2",
+        "strip-ansi": "3.0.1",
+        "wrap-ansi": "2.1.0"
+      }
+    },
+    "co": {
+      "version": "4.6.0",
+      "resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz",
+      "integrity": "sha1-bqa989hTrlTMuOR7+gvz+QMfsYQ=",
+      "dev": true
+    },
+    "code-point-at": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmjs.org/code-point-at/-/code-point-at-1.1.0.tgz",
+      "integrity": "sha1-DQcLTQQ6W+ozovGkDi7bPZpMz3c=",
+      "dev": true
+    },
+    "coffeescript": {
+      "version": "1.10.0",
+      "resolved": "https://registry.npmjs.org/coffeescript/-/coffeescript-1.10.0.tgz",
+      "integrity": "sha1-56qDAZF+9iGzXYo580jc3R234z4=",
+      "dev": true
+    },
+    "collection-visit": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/collection-visit/-/collection-visit-1.0.0.tgz",
+      "integrity": "sha1-S8A3PBZLwykbTTaMgpzxqApZ3KA=",
+      "dev": true,
+      "requires": {
+        "map-visit": "1.0.0",
+        "object-visit": "1.0.1"
+      }
+    },
+    "color-convert": {
+      "version": "1.9.2",
+      "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.2.tgz",
+      "integrity": "sha512-3NUJZdhMhcdPn8vJ9v2UQJoH0qqoGUkYTgFEPZaPjEtwmmKUfNV46zZmgB2M5M4DCEQHMaCfWHCxiBflLm04Tg==",
+      "dev": true,
+      "requires": {
+        "color-name": "1.1.1"
+      }
+    },
+    "color-name": {
+      "version": "1.1.1",
+      "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.1.tgz",
+      "integrity": "sha1-SxQVMEz1ACjqgWQ2Q72C6gWANok=",
+      "dev": true
+    },
+    "combine-lists": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/combine-lists/-/combine-lists-1.0.1.tgz",
+      "integrity": "sha1-RYwH4J4NkA/Ci3Cj/sLazR0st/Y=",
+      "dev": true,
+      "requires": {
+        "lodash": "4.17.10"
+      }
+    },
+    "combined-stream": {
+      "version": "1.0.6",
+      "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.6.tgz",
+      "integrity": "sha1-cj599ugBrFYTETp+RFqbactjKBg=",
+      "requires": {
+        "delayed-stream": "1.0.0"
+      }
+    },
+    "component-bind": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/component-bind/-/component-bind-1.0.0.tgz",
+      "integrity": "sha1-AMYIq33Nk4l8AAllGx06jh5zu9E=",
+      "dev": true
+    },
+    "component-emitter": {
+      "version": "1.2.1",
+      "resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.2.1.tgz",
+      "integrity": "sha1-E3kY1teCg/ffemt8WmPhQOaUJeY="
+    },
+    "component-inherit": {
+      "version": "0.0.3",
+      "resolved": "https://registry.npmjs.org/component-inherit/-/component-inherit-0.0.3.tgz",
+      "integrity": "sha1-ZF/ErfWLcrZJ1crmUTVhnbJv8UM=",
+      "dev": true
+    },
+    "compress-commons": {
+      "version": "1.2.2",
+      "resolved": "https://registry.npmjs.org/compress-commons/-/compress-commons-1.2.2.tgz",
+      "integrity": "sha1-UkqfEJA/OoEzibAiXSfEi7dRiQ8=",
+      "dev": true,
+      "requires": {
+        "buffer-crc32": "0.2.13",
+        "crc32-stream": "2.0.0",
+        "normalize-path": "2.1.1",
+        "readable-stream": "2.3.6"
+      }
+    },
+    "concat-map": {
+      "version": "0.0.1",
+      "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
+      "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=",
+      "dev": true
+    },
+    "connect": {
+      "version": "3.6.6",
+      "resolved": "https://registry.npmjs.org/connect/-/connect-3.6.6.tgz",
+      "integrity": "sha1-Ce/2xVr3I24TcTWnJXSFi2eG9SQ=",
+      "dev": true,
+      "requires": {
+        "debug": "2.6.9",
+        "finalhandler": "1.1.0",
+        "parseurl": "1.3.2",
+        "utils-merge": "1.0.1"
+      },
+      "dependencies": {
+        "debug": {
+          "version": "2.6.9",
+          "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
+          "integrity": "sha1-XRKFFd8TT/Mn6QpMk/Tgd6U2NB8=",
+          "dev": true,
+          "requires": {
+            "ms": "2.0.0"
+          }
+        }
+      }
+    },
+    "console-control-strings": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmjs.org/console-control-strings/-/console-control-strings-1.1.0.tgz",
+      "integrity": "sha1-PXz0Rk22RG6mRL9LOVB/mFEAjo4=",
+      "dev": true
+    },
+    "content-type": {
+      "version": "1.0.4",
+      "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.4.tgz",
+      "integrity": "sha1-4TjMdeBAxyexlm/l5fjJruJW/js=",
+      "dev": true
+    },
+    "convert-source-map": {
+      "version": "1.5.1",
+      "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.5.1.tgz",
+      "integrity": "sha1-uCeAl7m8IpNl3lxiz1/K7YtVmeU=",
+      "dev": true
+    },
+    "cookie": {
+      "version": "0.3.1",
+      "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.3.1.tgz",
+      "integrity": "sha1-5+Ch+e9DtMi6klxcWpboBtFoc7s=",
+      "dev": true
+    },
+    "cookiejar": {
+      "version": "2.1.1",
+      "resolved": "https://registry.npmjs.org/cookiejar/-/cookiejar-2.1.1.tgz",
+      "integrity": "sha1-Qa1XsbVVlR7BcUEqgZQrHoIA00o="
+    },
+    "copy-descriptor": {
+      "version": "0.1.1",
+      "resolved": "https://registry.npmjs.org/copy-descriptor/-/copy-descriptor-0.1.1.tgz",
+      "integrity": "sha1-Z29us8OZl8LuGsOpJP1hJHSPV40=",
+      "dev": true
+    },
+    "core-js": {
+      "version": "2.5.5",
+      "resolved": "https://registry.npmjs.org/core-js/-/core-js-2.5.5.tgz",
+      "integrity": "sha1-sU3ek2xkDAV5prUMq8wTLdYSfjs=",
+      "dev": true
+    },
+    "core-util-is": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz",
+      "integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac="
+    },
+    "crc": {
+      "version": "3.5.0",
+      "resolved": "https://registry.npmjs.org/crc/-/crc-3.5.0.tgz",
+      "integrity": "sha1-mLi6fUiWZbo5efWbITgTdBAaGWQ=",
+      "dev": true
+    },
+    "crc32-stream": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/crc32-stream/-/crc32-stream-2.0.0.tgz",
+      "integrity": "sha1-483TtN8xaN10494/u8t7KX/pCPQ=",
+      "dev": true,
+      "requires": {
+        "crc": "3.5.0",
+        "readable-stream": "2.3.6"
+      }
+    },
+    "cross-spawn": {
+      "version": "3.0.1",
+      "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-3.0.1.tgz",
+      "integrity": "sha1-ElYDfsufDF9549bvE14wdwGEuYI=",
+      "dev": true,
+      "requires": {
+        "lru-cache": "4.1.2",
+        "which": "1.3.0"
+      },
+      "dependencies": {
+        "lru-cache": {
+          "version": "4.1.2",
+          "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-4.1.2.tgz",
+          "integrity": "sha1-RSNLLm4vKzPaElYkxGZJKaAiTD8=",
+          "dev": true,
+          "requires": {
+            "pseudomap": "1.0.2",
+            "yallist": "2.1.2"
+          }
+        },
+        "which": {
+          "version": "1.3.0",
+          "resolved": "https://registry.npmjs.org/which/-/which-1.3.0.tgz",
+          "integrity": "sha1-/wS9/AEO5UfXgL7DjhrBwnd9JTo=",
+          "dev": true,
+          "requires": {
+            "isexe": "2.0.0"
+          }
+        }
+      }
+    },
+    "cryptiles": {
+      "version": "3.1.2",
+      "resolved": "https://registry.npmjs.org/cryptiles/-/cryptiles-3.1.2.tgz",
+      "integrity": "sha1-qJ+7Ig9c4l7FboxKqKT9e1sNKf4=",
+      "dev": true,
+      "optional": true,
+      "requires": {
+        "boom": "5.2.0"
+      },
+      "dependencies": {
+        "boom": {
+          "version": "5.2.0",
+          "resolved": "https://registry.npmjs.org/boom/-/boom-5.2.0.tgz",
+          "integrity": "sha1-XdnabuOl8wIHdDYpDLcX0/SlTgI=",
+          "dev": true,
+          "optional": true,
+          "dependencies": {
+            "hoek": {
+              "version": "5.0.3",
+              "resolved": "https://registry.npmjs.org/hoek/-/hoek-5.0.3.tgz",
+              "integrity": "sha512-Bmr56pxML1c9kU+NS51SMFkiVQAb+9uFfXwyqR2tn4w2FPvmPt65eZ9aCcEfRXd9G74HkZnILC6p967pED4aiw=="
+            }
+          }
+        }
+      }
+    },
+    "currently-unhandled": {
+      "version": "0.4.1",
+      "resolved": "https://registry.npmjs.org/currently-unhandled/-/currently-unhandled-0.4.1.tgz",
+      "integrity": "sha1-mI3zP+qxke95mmE2nddsF635V+o=",
+      "dev": true,
+      "requires": {
+        "array-find-index": "1.0.2"
+      }
+    },
+    "custom-event": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/custom-event/-/custom-event-1.0.1.tgz",
+      "integrity": "sha1-XQKkaFCt8bSjF5RqOSj8y1v9BCU=",
+      "dev": true
+    },
+    "d": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/d/-/d-1.0.0.tgz",
+      "integrity": "sha1-dUu1v+VUUdpppYuU1F9MWwRi1Y8=",
+      "dev": true,
+      "requires": {
+        "es5-ext": "0.10.42"
+      }
+    },
+    "dashdash": {
+      "version": "1.14.1",
+      "resolved": "https://registry.npmjs.org/dashdash/-/dashdash-1.14.1.tgz",
+      "integrity": "sha1-hTz6D3y+L+1d4gMmuN1YEDX24vA=",
+      "dev": true,
+      "requires": {
+        "assert-plus": "1.0.0"
+      }
+    },
+    "data-uri-to-buffer": {
+      "version": "0.0.4",
+      "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-0.0.4.tgz",
+      "integrity": "sha1-RuE6udqOMJdFyNAc5UchPr2y/j8=",
+      "dev": true
+    },
+    "date-format": {
+      "version": "1.2.0",
+      "resolved": "https://registry.npmjs.org/date-format/-/date-format-1.2.0.tgz",
+      "integrity": "sha1-YV6CjiM90aubua4JUODOzPpuytg=",
+      "dev": true
+    },
+    "debug": {
+      "version": "3.1.0",
+      "resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz",
+      "integrity": "sha1-W7WgZyYotkFJVmuhaBnmFRjGcmE=",
+      "requires": {
+        "ms": "2.0.0"
+      }
+    },
+    "decamelize": {
+      "version": "1.2.0",
+      "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz",
+      "integrity": "sha1-9lNNFRSCabIDUue+4m9QH5oZEpA=",
+      "dev": true
+    },
+    "decode-uri-component": {
+      "version": "0.2.0",
+      "resolved": "https://registry.npmjs.org/decode-uri-component/-/decode-uri-component-0.2.0.tgz",
+      "integrity": "sha1-6zkTMzRYd1y4TNGh+uBiEGu4dUU=",
+      "dev": true
+    },
+    "decompress-response": {
+      "version": "3.3.0",
+      "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-3.3.0.tgz",
+      "integrity": "sha1-gKTdMjdIOEv6JICDYirt7Jgq3/M=",
+      "dev": true,
+      "optional": true,
+      "requires": {
+        "mimic-response": "1.0.0"
+      }
+    },
+    "deep-extend": {
+      "version": "0.4.2",
+      "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.4.2.tgz",
+      "integrity": "sha1-SLaZwn4zS/ifEIkr5DL25MfTSn8=",
+      "dev": true,
+      "optional": true
+    },
+    "deep-is": {
+      "version": "0.1.3",
+      "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.3.tgz",
+      "integrity": "sha1-s2nW+128E+7PUk+RsHD+7cNXzzQ=",
+      "dev": true
+    },
+    "define-property": {
+      "version": "2.0.2",
+      "resolved": "https://registry.npmjs.org/define-property/-/define-property-2.0.2.tgz",
+      "integrity": "sha512-jwK2UV4cnPpbcG7+VRARKTZPUWowwXA8bzH5NP6ud0oeAxyYPuGZUAC7hMugpCdz4BeSZl2Dl9k66CHJ/46ZYQ==",
+      "dev": true,
+      "requires": {
+        "is-descriptor": "1.0.2",
+        "isobject": "3.0.1"
+      },
+      "dependencies": {
+        "is-accessor-descriptor": {
+          "version": "1.0.0",
+          "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz",
+          "integrity": "sha512-m5hnHTkcVsPfqx3AKlyttIPb7J+XykHvJP2B9bZDjlhLIoEq4XoK64Vg7boZlVWYK6LUY94dYPEE7Lh0ZkZKcQ==",
+          "dev": true,
+          "requires": {
+            "kind-of": "6.0.2"
+          }
+        },
+        "is-data-descriptor": {
+          "version": "1.0.0",
+          "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz",
+          "integrity": "sha512-jbRXy1FmtAoCjQkVmIVYwuuqDFUbaOeDjmed1tOGPrsMhtJA4rD9tkgA0F1qJ3gRFRXcHYVkdeaP50Q5rE/jLQ==",
+          "dev": true,
+          "requires": {
+            "kind-of": "6.0.2"
+          }
+        },
+        "is-descriptor": {
+          "version": "1.0.2",
+          "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-1.0.2.tgz",
+          "integrity": "sha512-2eis5WqQGV7peooDyLmNEPUrps9+SXX5c9pL3xEB+4e9HnGuDa7mB7kHxHw4CbqS9k1T2hOH3miL8n8WtiYVtg==",
+          "dev": true,
+          "requires": {
+            "is-accessor-descriptor": "1.0.0",
+            "is-data-descriptor": "1.0.0",
+            "kind-of": "6.0.2"
+          }
+        },
+        "isobject": {
+          "version": "3.0.1",
+          "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz",
+          "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=",
+          "dev": true
+        },
+        "kind-of": {
+          "version": "6.0.2",
+          "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.2.tgz",
+          "integrity": "sha512-s5kLOcnH0XqDO+FvuaLX8DDjZ18CGFk7VygH40QoKPUQhW4e2rvM0rwUq0t8IQDOwYSeLK01U90OjzBTme2QqA==",
+          "dev": true
+        }
+      }
+    },
+    "del": {
+      "version": "2.2.2",
+      "resolved": "https://registry.npmjs.org/del/-/del-2.2.2.tgz",
+      "integrity": "sha1-wSyYHQZ4RshLyvhiz/kw2Qf/0ag=",
+      "dev": true,
+      "requires": {
+        "globby": "5.0.0",
+        "is-path-cwd": "1.0.0",
+        "is-path-in-cwd": "1.0.1",
+        "object-assign": "4.1.1",
+        "pify": "2.3.0",
+        "pinkie-promise": "2.0.1",
+        "rimraf": "2.2.8"
+      }
+    },
+    "delayed-stream": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
+      "integrity": "sha1-3zrhmayt+31ECqrgsp4icrJOxhk="
+    },
+    "delegates": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/delegates/-/delegates-1.0.0.tgz",
+      "integrity": "sha1-hMbhWbgZBP3KWaDvRM2HDTElD5o=",
+      "dev": true
+    },
+    "depd": {
+      "version": "1.1.2",
+      "resolved": "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz",
+      "integrity": "sha1-m81S4UwJd2PnSbJ0xDRu0uVgtak=",
+      "dev": true
+    },
+    "detect-file": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/detect-file/-/detect-file-1.0.0.tgz",
+      "integrity": "sha1-8NZtA2cqglyxtzvbP+YjEMjlUrc=",
+      "dev": true
+    },
+    "detect-indent": {
+      "version": "4.0.0",
+      "resolved": "https://registry.npmjs.org/detect-indent/-/detect-indent-4.0.0.tgz",
+      "integrity": "sha1-920GQ1LN9Docts5hnE7jqUdd4gg=",
+      "dev": true,
+      "requires": {
+        "repeating": "2.0.1"
+      }
+    },
+    "detect-libc": {
+      "version": "0.2.0",
+      "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-0.2.0.tgz",
+      "integrity": "sha1-R/31ZzSKF+wl/L8LnkRjSKdvn7U=",
+      "dev": true,
+      "optional": true
+    },
+    "di": {
+      "version": "0.0.1",
+      "resolved": "https://registry.npmjs.org/di/-/di-0.0.1.tgz",
+      "integrity": "sha1-gGZJMmzqp8qjMG112YXqJ0i6kTw=",
+      "dev": true
+    },
+    "dom-serialize": {
+      "version": "2.2.1",
+      "resolved": "https://registry.npmjs.org/dom-serialize/-/dom-serialize-2.2.1.tgz",
+      "integrity": "sha1-ViromZ9Evl6jB29UGdzVnrQ6yVs=",
+      "dev": true,
+      "requires": {
+        "custom-event": "1.0.1",
+        "ent": "2.2.0",
+        "extend": "3.0.1",
+        "void-elements": "2.0.1"
+      }
+    },
+    "ecc-jsbn": {
+      "version": "0.1.1",
+      "resolved": "https://registry.npmjs.org/ecc-jsbn/-/ecc-jsbn-0.1.1.tgz",
+      "integrity": "sha1-D8c6ntXw1Tw4GTOYUj735UN3dQU=",
+      "dev": true,
+      "optional": true,
+      "requires": {
+        "jsbn": "0.1.1"
+      }
+    },
+    "ee-first": {
+      "version": "1.1.1",
+      "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz",
+      "integrity": "sha1-WQxhFWsK4vTwJVcyoViyZrxWsh0=",
+      "dev": true
+    },
+    "encodeurl": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz",
+      "integrity": "sha1-rT/0yG7C0CkyL1oCw6mmBslbP1k=",
+      "dev": true
+    },
+    "end-of-stream": {
+      "version": "1.4.1",
+      "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.1.tgz",
+      "integrity": "sha1-7SljTRm6ukY7bOa4CjchPqtx7EM=",
+      "dev": true,
+      "requires": {
+        "once": "1.4.0"
+      }
+    },
+    "engine.io": {
+      "version": "3.2.0",
+      "resolved": "https://registry.npmjs.org/engine.io/-/engine.io-3.2.0.tgz",
+      "integrity": "sha512-mRbgmAtQ4GAlKwuPnnAvXXwdPhEx+jkc0OBCLrXuD/CRvwNK3AxRSnqK4FSqmAMRRHryVJP8TopOvmEaA64fKw==",
+      "dev": true,
+      "requires": {
+        "accepts": "1.3.5",
+        "base64id": "1.0.0",
+        "cookie": "0.3.1",
+        "debug": "3.1.0",
+        "engine.io-parser": "2.1.2",
+        "ws": "3.3.3"
+      }
+    },
+    "engine.io-client": {
+      "version": "3.2.1",
+      "resolved": "https://registry.npmjs.org/engine.io-client/-/engine.io-client-3.2.1.tgz",
+      "integrity": "sha512-y5AbkytWeM4jQr7m/koQLc5AxpRKC1hEVUb/s1FUAWEJq5AzJJ4NLvzuKPuxtDi5Mq755WuDvZ6Iv2rXj4PTzw==",
+      "dev": true,
+      "requires": {
+        "component-emitter": "1.2.1",
+        "component-inherit": "0.0.3",
+        "debug": "3.1.0",
+        "engine.io-parser": "2.1.2",
+        "has-cors": "1.1.0",
+        "indexof": "0.0.1",
+        "parseqs": "0.0.5",
+        "parseuri": "0.0.5",
+        "ws": "3.3.3",
+        "xmlhttprequest-ssl": "1.5.5",
+        "yeast": "0.1.2"
+      }
+    },
+    "engine.io-parser": {
+      "version": "2.1.2",
+      "resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-2.1.2.tgz",
+      "integrity": "sha512-dInLFzr80RijZ1rGpx1+56/uFoH7/7InhH3kZt+Ms6hT8tNx3NGW/WNSA/f8As1WkOfkuyb3tnRyuXGxusclMw==",
+      "dev": true,
+      "requires": {
+        "after": "0.8.2",
+        "arraybuffer.slice": "0.0.7",
+        "base64-arraybuffer": "0.1.5",
+        "blob": "0.0.4",
+        "has-binary2": "1.0.3"
+      }
+    },
+    "ent": {
+      "version": "2.2.0",
+      "resolved": "https://registry.npmjs.org/ent/-/ent-2.2.0.tgz",
+      "integrity": "sha1-6WQhkyWiHQX0RGai9obtbOX13R0=",
+      "dev": true
+    },
+    "error-ex": {
+      "version": "1.3.1",
+      "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.1.tgz",
+      "integrity": "sha1-+FWobOYa3E6GIcPNoh56dhLDqNw=",
+      "dev": true,
+      "requires": {
+        "is-arrayish": "0.2.1"
+      }
+    },
+    "es5-ext": {
+      "version": "0.10.42",
+      "resolved": "https://registry.npmjs.org/es5-ext/-/es5-ext-0.10.42.tgz",
+      "integrity": "sha1-jAfdM68E1dzRMQtc7xO+pjqJuo0=",
+      "dev": true,
+      "requires": {
+        "es6-iterator": "2.0.3",
+        "es6-symbol": "3.1.1",
+        "next-tick": "1.0.0"
+      }
+    },
+    "es6-iterator": {
+      "version": "2.0.3",
+      "resolved": "https://registry.npmjs.org/es6-iterator/-/es6-iterator-2.0.3.tgz",
+      "integrity": "sha1-p96IkUGgWpSwhUQDstCg+/qY87c=",
+      "dev": true,
+      "requires": {
+        "d": "1.0.0",
+        "es5-ext": "0.10.42",
+        "es6-symbol": "3.1.1"
+      }
+    },
+    "es6-promise": {
+      "version": "4.2.4",
+      "resolved": "https://registry.npmjs.org/es6-promise/-/es6-promise-4.2.4.tgz",
+      "integrity": "sha512-/NdNZVJg+uZgtm9eS3O6lrOLYmQag2DjdEXuPaHlZ6RuVqgqaVZfgYCepEIKsLqwdQArOPtC3XzRLqGGfT8KQQ==",
+      "dev": true
+    },
+    "es6-promisify": {
+      "version": "5.0.0",
+      "resolved": "https://registry.npmjs.org/es6-promisify/-/es6-promisify-5.0.0.tgz",
+      "integrity": "sha1-UQnWLz5W6pZ8S2NQWu8IKRyKUgM=",
+      "dev": true,
+      "requires": {
+        "es6-promise": "4.2.4"
+      }
+    },
+    "es6-symbol": {
+      "version": "3.1.1",
+      "resolved": "https://registry.npmjs.org/es6-symbol/-/es6-symbol-3.1.1.tgz",
+      "integrity": "sha1-vwDvT9q2uhtG7Le2KbTH7VcVzHc=",
+      "dev": true,
+      "requires": {
+        "d": "1.0.0",
+        "es5-ext": "0.10.42"
+      }
+    },
+    "es6-template-strings": {
+      "version": "2.0.1",
+      "resolved": "https://registry.npmjs.org/es6-template-strings/-/es6-template-strings-2.0.1.tgz",
+      "integrity": "sha1-sWbGpiVi9Hi7d3X2ypYQOlmbSyw=",
+      "dev": true,
+      "requires": {
+        "es5-ext": "0.10.42",
+        "esniff": "1.1.0"
+      }
+    },
+    "escape-html": {
+      "version": "1.0.3",
+      "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz",
+      "integrity": "sha1-Aljq5NPQwJdN4cFpGI7wBR0dGYg=",
+      "dev": true
+    },
+    "escape-string-regexp": {
+      "version": "1.0.5",
+      "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz",
+      "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=",
+      "dev": true
+    },
+    "esniff": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmjs.org/esniff/-/esniff-1.1.0.tgz",
+      "integrity": "sha1-xmhJIp+RRk3t4uDUAgHtar9l8qw=",
+      "dev": true,
+      "requires": {
+        "d": "1.0.0",
+        "es5-ext": "0.10.42"
+      }
+    },
+    "esutils": {
+      "version": "2.0.2",
+      "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.2.tgz",
+      "integrity": "sha1-Cr9PHKpbyx96nYrMbepPqqBLrJs=",
+      "dev": true
+    },
+    "eventemitter2": {
+      "version": "0.4.14",
+      "resolved": "https://registry.npmjs.org/eventemitter2/-/eventemitter2-0.4.14.tgz",
+      "integrity": "sha1-j2G3XN4BKy6esoTUVFWDtWQ7Yas=",
+      "dev": true
+    },
+    "eventemitter3": {
+      "version": "1.2.0",
+      "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-1.2.0.tgz",
+      "integrity": "sha1-HIaZHYFq0eUEdQ5zh0Ik7PO+xQg=",
+      "dev": true
+    },
+    "exit": {
+      "version": "0.1.2",
+      "resolved": "https://registry.npmjs.org/exit/-/exit-0.1.2.tgz",
+      "integrity": "sha1-BjJjj42HfMghB9MKD/8aF8uhzQw=",
+      "dev": true
+    },
+    "expand-braces": {
+      "version": "0.1.2",
+      "resolved": "https://registry.npmjs.org/expand-braces/-/expand-braces-0.1.2.tgz",
+      "integrity": "sha1-SIsdHSRRyz06axks/AMPRMWFX+o=",
+      "dev": true,
+      "requires": {
+        "array-slice": "0.2.3",
+        "array-unique": "0.2.1",
+        "braces": "0.1.5"
+      },
+      "dependencies": {
+        "braces": {
+          "version": "0.1.5",
+          "resolved": "https://registry.npmjs.org/braces/-/braces-0.1.5.tgz",
+          "integrity": "sha1-wIVxEIUpHYt1/ddOqw+FlygHEeY=",
+          "dev": true,
+          "requires": {
+            "expand-range": "0.1.1"
+          }
+        },
+        "expand-range": {
+          "version": "0.1.1",
+          "resolved": "https://registry.npmjs.org/expand-range/-/expand-range-0.1.1.tgz",
+          "integrity": "sha1-TLjtoJk8pW+k9B/ELzy7TMrf8EQ=",
+          "dev": true,
+          "requires": {
+            "is-number": "0.1.1",
+            "repeat-string": "0.2.2"
+          }
+        },
+        "is-number": {
+          "version": "0.1.1",
+          "resolved": "https://registry.npmjs.org/is-number/-/is-number-0.1.1.tgz",
+          "integrity": "sha1-aaevEWlj1HIG7JvZtIoUIW8eOAY=",
+          "dev": true
+        },
+        "repeat-string": {
+          "version": "0.2.2",
+          "resolved": "https://registry.npmjs.org/repeat-string/-/repeat-string-0.2.2.tgz",
+          "integrity": "sha1-x6jTI2BoNiBZp+RlH8aITosftK4=",
+          "dev": true
+        }
+      }
+    },
+    "expand-brackets": {
+      "version": "2.1.4",
+      "resolved": "https://registry.npmjs.org/expand-brackets/-/expand-brackets-2.1.4.tgz",
+      "integrity": "sha1-t3c14xXOMPa27/D4OwQVGiJEliI=",
+      "dev": true,
+      "requires": {
+        "debug": "2.6.9",
+        "define-property": "0.2.5",
+        "extend-shallow": "2.0.1",
+        "posix-character-classes": "0.1.1",
+        "regex-not": "1.0.2",
+        "snapdragon": "0.8.2",
+        "to-regex": "3.0.2"
+      },
+      "dependencies": {
+        "debug": {
+          "version": "2.6.9",
+          "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
+          "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
+          "dev": true,
+          "requires": {
+            "ms": "2.0.0"
+          }
+        },
+        "define-property": {
+          "version": "0.2.5",
+          "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz",
+          "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=",
+          "dev": true,
+          "requires": {
+            "is-descriptor": "0.1.6"
+          }
+        },
+        "extend-shallow": {
+          "version": "2.0.1",
+          "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz",
+          "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=",
+          "dev": true,
+          "requires": {
+            "is-extendable": "0.1.1"
+          }
+        }
+      }
+    },
+    "expand-template": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmjs.org/expand-template/-/expand-template-1.1.0.tgz",
+      "integrity": "sha1-4J77qXe/mPnuDtJavQxpLgKuw/w=",
+      "dev": true,
+      "optional": true
+    },
+    "expand-tilde": {
+      "version": "2.0.2",
+      "resolved": "https://registry.npmjs.org/expand-tilde/-/expand-tilde-2.0.2.tgz",
+      "integrity": "sha1-l+gBqgUt8CRU3kawK/YhZCzchQI=",
+      "dev": true,
+      "requires": {
+        "homedir-polyfill": "1.0.1"
+      }
+    },
+    "extend": {
+      "version": "3.0.1",
+      "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.1.tgz",
+      "integrity": "sha1-p1Xqe8Gt/MWjHOfnYtuq3F5jZEQ="
+    },
+    "extend-shallow": {
+      "version": "3.0.2",
+      "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-3.0.2.tgz",
+      "integrity": "sha1-Jqcarwc7OfshJxcnRhMcJwQCjbg=",
+      "dev": true,
+      "requires": {
+        "assign-symbols": "1.0.0",
+        "is-extendable": "1.0.1"
+      },
+      "dependencies": {
+        "is-extendable": {
+          "version": "1.0.1",
+          "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-1.0.1.tgz",
+          "integrity": "sha512-arnXMxT1hhoKo9k1LZdmlNyJdDDfy2v0fXjFlmok4+i8ul/6WlbVge9bhM74OpNPQPMGUToDtz+KXa1PneJxOA==",
+          "dev": true,
+          "requires": {
+            "is-plain-object": "2.0.4"
+          }
+        }
+      }
+    },
+    "extglob": {
+      "version": "2.0.4",
+      "resolved": "https://registry.npmjs.org/extglob/-/extglob-2.0.4.tgz",
+      "integrity": "sha512-Nmb6QXkELsuBr24CJSkilo6UHHgbekK5UiZgfE6UHD3Eb27YC6oD+bhcT+tJ6cl8dmsgdQxnWlcry8ksBIBLpw==",
+      "dev": true,
+      "requires": {
+        "array-unique": "0.3.2",
+        "define-property": "1.0.0",
+        "expand-brackets": "2.1.4",
+        "extend-shallow": "2.0.1",
+        "fragment-cache": "0.2.1",
+        "regex-not": "1.0.2",
+        "snapdragon": "0.8.2",
+        "to-regex": "3.0.2"
+      },
+      "dependencies": {
+        "array-unique": {
+          "version": "0.3.2",
+          "resolved": "https://registry.npmjs.org/array-unique/-/array-unique-0.3.2.tgz",
+          "integrity": "sha1-qJS3XUvE9s1nnvMkSp/Y9Gri1Cg=",
+          "dev": true
+        },
+        "define-property": {
+          "version": "1.0.0",
+          "resolved": "https://registry.npmjs.org/define-property/-/define-property-1.0.0.tgz",
+          "integrity": "sha1-dp66rz9KY6rTr56NMEybvnm/sOY=",
+          "dev": true,
+          "requires": {
+            "is-descriptor": "1.0.2"
+          }
+        },
+        "extend-shallow": {
+          "version": "2.0.1",
+          "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz",
+          "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=",
+          "dev": true,
+          "requires": {
+            "is-extendable": "0.1.1"
+          }
+        },
+        "is-accessor-descriptor": {
+          "version": "1.0.0",
+          "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz",
+          "integrity": "sha512-m5hnHTkcVsPfqx3AKlyttIPb7J+XykHvJP2B9bZDjlhLIoEq4XoK64Vg7boZlVWYK6LUY94dYPEE7Lh0ZkZKcQ==",
+          "dev": true,
+          "requires": {
+            "kind-of": "6.0.2"
+          }
+        },
+        "is-data-descriptor": {
+          "version": "1.0.0",
+          "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz",
+          "integrity": "sha512-jbRXy1FmtAoCjQkVmIVYwuuqDFUbaOeDjmed1tOGPrsMhtJA4rD9tkgA0F1qJ3gRFRXcHYVkdeaP50Q5rE/jLQ==",
+          "dev": true,
+          "requires": {
+            "kind-of": "6.0.2"
+          }
+        },
+        "is-descriptor": {
+          "version": "1.0.2",
+          "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-1.0.2.tgz",
+          "integrity": "sha512-2eis5WqQGV7peooDyLmNEPUrps9+SXX5c9pL3xEB+4e9HnGuDa7mB7kHxHw4CbqS9k1T2hOH3miL8n8WtiYVtg==",
+          "dev": true,
+          "requires": {
+            "is-accessor-descriptor": "1.0.0",
+            "is-data-descriptor": "1.0.0",
+            "kind-of": "6.0.2"
+          }
+        },
+        "kind-of": {
+          "version": "6.0.2",
+          "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.2.tgz",
+          "integrity": "sha512-s5kLOcnH0XqDO+FvuaLX8DDjZ18CGFk7VygH40QoKPUQhW4e2rvM0rwUq0t8IQDOwYSeLK01U90OjzBTme2QqA==",
+          "dev": true
+        }
+      }
+    },
+    "extsprintf": {
+      "version": "1.3.0",
+      "resolved": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.3.0.tgz",
+      "integrity": "sha1-lpGEQOMEGnpBT4xS48V06zw+HgU=",
+      "dev": true
+    },
+    "fast-deep-equal": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-1.1.0.tgz",
+      "integrity": "sha1-wFNHeBfIa1HaqFPIHgWbcz0CNhQ=",
+      "dev": true
+    },
+    "fast-json-stable-stringify": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.0.0.tgz",
+      "integrity": "sha1-1RQsDK7msRifh9OnYREGT4bIu/I=",
+      "dev": true
+    },
+    "fast-levenshtein": {
+      "version": "2.0.6",
+      "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz",
+      "integrity": "sha1-PYpcZog6FqMMqGQ+hR8Zuqd5eRc=",
+      "dev": true
+    },
+    "fill-range": {
+      "version": "4.0.0",
+      "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-4.0.0.tgz",
+      "integrity": "sha1-1USBHUKPmOsGpj3EAtJAPDKMOPc=",
+      "dev": true,
+      "requires": {
+        "extend-shallow": "2.0.1",
+        "is-number": "3.0.0",
+        "repeat-string": "1.6.1",
+        "to-regex-range": "2.1.1"
+      },
+      "dependencies": {
+        "extend-shallow": {
+          "version": "2.0.1",
+          "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz",
+          "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=",
+          "dev": true,
+          "requires": {
+            "is-extendable": "0.1.1"
+          }
+        }
+      }
+    },
+    "finalhandler": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.1.0.tgz",
+      "integrity": "sha1-zgtoVbRYU+eRsvzGgARtiCU91/U=",
+      "dev": true,
+      "requires": {
+        "debug": "2.6.9",
+        "encodeurl": "1.0.2",
+        "escape-html": "1.0.3",
+        "on-finished": "2.3.0",
+        "parseurl": "1.3.2",
+        "statuses": "1.3.1",
+        "unpipe": "1.0.0"
+      },
+      "dependencies": {
+        "debug": {
+          "version": "2.6.9",
+          "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
+          "integrity": "sha1-XRKFFd8TT/Mn6QpMk/Tgd6U2NB8=",
+          "dev": true,
+          "requires": {
+            "ms": "2.0.0"
+          }
+        },
+        "statuses": {
+          "version": "1.3.1",
+          "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.3.1.tgz",
+          "integrity": "sha1-+vUbnrdKrvOzrPStX2Gr8ky3uT4=",
+          "dev": true
+        }
+      }
+    },
+    "find-up": {
+      "version": "1.1.2",
+      "resolved": "https://registry.npmjs.org/find-up/-/find-up-1.1.2.tgz",
+      "integrity": "sha1-ay6YIrGizgpgq2TWEOzK1TyyTQ8=",
+      "dev": true,
+      "requires": {
+        "path-exists": "2.1.0",
+        "pinkie-promise": "2.0.1"
+      }
+    },
+    "fined": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmjs.org/fined/-/fined-1.1.0.tgz",
+      "integrity": "sha1-s33IRLdqL15wgeiE98CuNE8VNHY=",
+      "dev": true,
+      "requires": {
+        "expand-tilde": "2.0.2",
+        "is-plain-object": "2.0.4",
+        "object.defaults": "1.1.0",
+        "object.pick": "1.3.0",
+        "parse-filepath": "1.0.2"
+      }
+    },
+    "flagged-respawn": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/flagged-respawn/-/flagged-respawn-1.0.0.tgz",
+      "integrity": "sha1-Tnmumy6zi/hrO7Vr8+ClaqX8q9c=",
+      "dev": true
+    },
+    "font-awesome": {
+      "version": "4.7.0",
+      "resolved": "https://registry.npmjs.org/font-awesome/-/font-awesome-4.7.0.tgz",
+      "integrity": "sha1-j6jPBBGhoxr9B7BtKQK7n8gVoTM="
+    },
+    "for-in": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmjs.org/for-in/-/for-in-1.0.2.tgz",
+      "integrity": "sha1-gQaNKVqBQuwKxybG4iAMMPttXoA=",
+      "dev": true
+    },
+    "for-own": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/for-own/-/for-own-1.0.0.tgz",
+      "integrity": "sha1-xjMy9BXO3EsE2/5wz4NklMU8tEs=",
+      "dev": true,
+      "requires": {
+        "for-in": "1.0.2"
+      }
+    },
+    "forever-agent": {
+      "version": "0.6.1",
+      "resolved": "https://registry.npmjs.org/forever-agent/-/forever-agent-0.6.1.tgz",
+      "integrity": "sha1-+8cfDEGt6zf5bFd60e1C2P2sypE=",
+      "dev": true
+    },
+    "form-data": {
+      "version": "2.3.2",
+      "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.3.2.tgz",
+      "integrity": "sha1-SXBJi+YEwgwAXU9cI67NIda0kJk=",
+      "requires": {
+        "asynckit": "0.4.0",
+        "combined-stream": "1.0.6",
+        "mime-types": "2.1.18"
+      }
+    },
+    "formidable": {
+      "version": "1.2.1",
+      "resolved": "https://registry.npmjs.org/formidable/-/formidable-1.2.1.tgz",
+      "integrity": "sha1-cPt8oCkO5v+WEJBBX0s989IIJlk="
+    },
+    "fragment-cache": {
+      "version": "0.2.1",
+      "resolved": "https://registry.npmjs.org/fragment-cache/-/fragment-cache-0.2.1.tgz",
+      "integrity": "sha1-QpD60n8T6Jvn8zeZxrxaCr//DRk=",
+      "dev": true,
+      "requires": {
+        "map-cache": "0.2.2"
+      }
+    },
+    "fs-access": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/fs-access/-/fs-access-1.0.1.tgz",
+      "integrity": "sha1-1qh/JiJxzv6+wwxVNAf7mV2od3o=",
+      "dev": true,
+      "requires": {
+        "null-check": "1.0.0"
+      }
+    },
+    "fs.realpath": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz",
+      "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=",
+      "dev": true
+    },
+    "fstream": {
+      "version": "1.0.11",
+      "resolved": "https://registry.npmjs.org/fstream/-/fstream-1.0.11.tgz",
+      "integrity": "sha1-XB+x8RdHcRTwYyoOtLcbPLD9MXE=",
+      "dev": true,
+      "requires": {
+        "graceful-fs": "4.1.11",
+        "inherits": "2.0.3",
+        "mkdirp": "0.5.1",
+        "rimraf": "2.2.8"
+      },
+      "dependencies": {
+        "graceful-fs": {
+          "version": "4.1.11",
+          "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.1.11.tgz",
+          "integrity": "sha1-Dovf5NHduIVNZOBOp8AOKgJuVlg=",
+          "dev": true
+        }
+      }
+    },
+    "gauge": {
+      "version": "2.7.4",
+      "resolved": "https://registry.npmjs.org/gauge/-/gauge-2.7.4.tgz",
+      "integrity": "sha1-LANAXHU4w51+s3sxcCLjJfsBi/c=",
+      "dev": true,
+      "requires": {
+        "aproba": "1.2.0",
+        "console-control-strings": "1.1.0",
+        "has-unicode": "2.0.1",
+        "object-assign": "4.1.1",
+        "signal-exit": "3.0.2",
+        "string-width": "1.0.2",
+        "strip-ansi": "3.0.1",
+        "wide-align": "1.1.2"
+      }
+    },
+    "gaze": {
+      "version": "1.1.2",
+      "resolved": "https://registry.npmjs.org/gaze/-/gaze-1.1.2.tgz",
+      "integrity": "sha1-hHIkZ3rbiHDWeSV+0ziP22HkAQU=",
+      "dev": true,
+      "requires": {
+        "globule": "1.2.0"
+      }
+    },
+    "get-caller-file": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-1.0.2.tgz",
+      "integrity": "sha1-9wLmMSfn4jHBYKgMFVSstw1QR+U=",
+      "dev": true
+    },
+    "get-stdin": {
+      "version": "4.0.1",
+      "resolved": "https://registry.npmjs.org/get-stdin/-/get-stdin-4.0.1.tgz",
+      "integrity": "sha1-uWjGsKBDhDJJAui/Gl3zJXmkUP4=",
+      "dev": true
+    },
+    "get-value": {
+      "version": "2.0.6",
+      "resolved": "https://registry.npmjs.org/get-value/-/get-value-2.0.6.tgz",
+      "integrity": "sha1-3BXKHGcjh8p2vTesCjlbogQqLCg=",
+      "dev": true
+    },
+    "getobject": {
+      "version": "0.1.0",
+      "resolved": "https://registry.npmjs.org/getobject/-/getobject-0.1.0.tgz",
+      "integrity": "sha1-BHpEl4n6Fg0Bj1SG7ZEyC27HiFw=",
+      "dev": true
+    },
+    "getpass": {
+      "version": "0.1.7",
+      "resolved": "https://registry.npmjs.org/getpass/-/getpass-0.1.7.tgz",
+      "integrity": "sha1-Xv+OPmhNVprkyysSgmBOi6YhSfo=",
+      "dev": true,
+      "requires": {
+        "assert-plus": "1.0.0"
+      }
+    },
+    "github-from-package": {
+      "version": "0.0.0",
+      "resolved": "https://registry.npmjs.org/github-from-package/-/github-from-package-0.0.0.tgz",
+      "integrity": "sha1-l/tdlr/eiXMxPyDoKI75oWf6ZM4=",
+      "dev": true,
+      "optional": true
+    },
+    "global-modules": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/global-modules/-/global-modules-1.0.0.tgz",
+      "integrity": "sha512-sKzpEkf11GpOFuw0Zzjzmt4B4UZwjOcG757PPvrfhxcLFbq0wpsgpOqxpxtxFiCG4DtG93M6XRVbF2oGdev7bg==",
+      "dev": true,
+      "requires": {
+        "global-prefix": "1.0.2",
+        "is-windows": "1.0.2",
+        "resolve-dir": "1.0.1"
+      }
+    },
+    "global-prefix": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmjs.org/global-prefix/-/global-prefix-1.0.2.tgz",
+      "integrity": "sha1-2/dDxsFJklk8ZVVoy2btMsASLr4=",
+      "dev": true,
+      "requires": {
+        "expand-tilde": "2.0.2",
+        "homedir-polyfill": "1.0.1",
+        "ini": "1.3.5",
+        "is-windows": "1.0.2",
+        "which": "1.3.1"
+      },
+      "dependencies": {
+        "which": {
+          "version": "1.3.1",
+          "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz",
+          "integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==",
+          "dev": true,
+          "requires": {
+            "isexe": "2.0.0"
+          }
+        }
+      }
+    },
+    "globals": {
+      "version": "9.18.0",
+      "resolved": "https://registry.npmjs.org/globals/-/globals-9.18.0.tgz",
+      "integrity": "sha1-qjiWs+abSH8X4x7SFD1pqOMMLYo=",
+      "dev": true
+    },
+    "globby": {
+      "version": "5.0.0",
+      "resolved": "https://registry.npmjs.org/globby/-/globby-5.0.0.tgz",
+      "integrity": "sha1-69hGZ8oNuzMLmbz8aOrCvFQ3Dg0=",
+      "dev": true,
+      "requires": {
+        "array-union": "1.0.2",
+        "arrify": "1.0.1",
+        "glob": "7.1.2",
+        "object-assign": "4.1.1",
+        "pify": "2.3.0",
+        "pinkie-promise": "2.0.1"
+      },
+      "dependencies": {
+        "glob": {
+          "version": "7.1.2",
+          "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.2.tgz",
+          "integrity": "sha1-wZyd+aAocC1nhhI4SmVSQExjbRU=",
+          "dev": true,
+          "requires": {
+            "fs.realpath": "1.0.0",
+            "inflight": "1.0.6",
+            "inherits": "2.0.3",
+            "minimatch": "3.0.4",
+            "once": "1.4.0",
+            "path-is-absolute": "1.0.1"
+          }
+        },
+        "minimatch": {
+          "version": "3.0.4",
+          "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz",
+          "integrity": "sha1-UWbihkV/AzBgZL5Ul+jbsMPTIIM=",
+          "dev": true,
+          "requires": {
+            "brace-expansion": "1.1.11"
+          }
+        }
+      }
+    },
+    "globule": {
+      "version": "1.2.0",
+      "resolved": "https://registry.npmjs.org/globule/-/globule-1.2.0.tgz",
+      "integrity": "sha1-HcScaCLdnoovoAuiopUAboZkvQk=",
+      "dev": true,
+      "requires": {
+        "glob": "7.1.2",
+        "lodash": "4.17.5",
+        "minimatch": "3.0.4"
+      },
+      "dependencies": {
+        "glob": {
+          "version": "7.1.2",
+          "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.2.tgz",
+          "integrity": "sha1-wZyd+aAocC1nhhI4SmVSQExjbRU=",
+          "dev": true,
+          "requires": {
+            "fs.realpath": "1.0.0",
+            "inflight": "1.0.6",
+            "inherits": "2.0.3",
+            "minimatch": "3.0.4",
+            "once": "1.4.0",
+            "path-is-absolute": "1.0.1"
+          }
+        },
+        "lodash": {
+          "version": "4.17.5",
+          "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.5.tgz",
+          "integrity": "sha1-maktZcAnLevoyWtgV7yPv6O+1RE=",
+          "dev": true
+        },
+        "minimatch": {
+          "version": "3.0.4",
+          "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz",
+          "integrity": "sha1-UWbihkV/AzBgZL5Ul+jbsMPTIIM=",
+          "dev": true,
+          "requires": {
+            "brace-expansion": "1.1.11"
+          }
+        }
+      }
+    },
+    "graceful-readlink": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/graceful-readlink/-/graceful-readlink-1.0.1.tgz",
+      "integrity": "sha1-TK+tdrxi8C+gObL5Tpo906ORpyU=",
+      "dev": true
+    },
+    "grunt": {
+      "version": "1.0.3",
+      "resolved": "https://registry.npmjs.org/grunt/-/grunt-1.0.3.tgz",
+      "integrity": "sha512-/JzmZNPfKorlCrrmxWqQO4JVodO+DVd5XX4DkocL/1WlLlKVLE9+SdEIempOAxDhWPysLle6afvn/hg7Ck2k9g==",
+      "dev": true,
+      "requires": {
+        "coffeescript": "1.10.0",
+        "dateformat": "1.0.12",
+        "eventemitter2": "0.4.14",
+        "exit": "0.1.2",
+        "findup-sync": "0.3.0",
+        "glob": "7.0.6",
+        "grunt-cli": "1.2.0",
+        "grunt-known-options": "1.1.0",
+        "grunt-legacy-log": "2.0.0",
+        "grunt-legacy-util": "1.1.1",
+        "iconv-lite": "0.4.23",
+        "js-yaml": "3.5.5",
+        "minimatch": "3.0.4",
+        "mkdirp": "0.5.1",
+        "nopt": "3.0.6",
+        "path-is-absolute": "1.0.1",
+        "rimraf": "2.6.2"
+      },
+      "dependencies": {
+        "ansi-styles": {
+          "version": "3.2.1",
+          "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz",
+          "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==",
+          "dev": true,
+          "requires": {
+            "color-convert": "1.9.2"
+          }
+        },
+        "argparse": {
+          "version": "1.0.10",
+          "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz",
+          "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==",
+          "dev": true,
+          "requires": {
+            "sprintf-js": "1.0.3"
+          }
+        },
+        "async": {
+          "version": "1.5.2",
+          "resolved": "https://registry.npmjs.org/async/-/async-1.5.2.tgz",
+          "integrity": "sha1-7GphrlZIDAw8skHJVhjiCJL5Zyo=",
+          "dev": true
+        },
+        "chalk": {
+          "version": "2.4.1",
+          "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.1.tgz",
+          "integrity": "sha512-ObN6h1v2fTJSmUXoS3nMQ92LbDK9be4TV+6G+omQlGJFdcUX5heKi1LZ1YnRMIgwTLEj3E24bT6tYni50rlCfQ==",
+          "dev": true,
+          "requires": {
+            "ansi-styles": "3.2.1",
+            "escape-string-regexp": "1.0.5",
+            "supports-color": "5.4.0"
+          }
+        },
+        "colors": {
+          "version": "1.1.2",
+          "resolved": "https://registry.npmjs.org/colors/-/colors-1.1.2.tgz",
+          "integrity": "sha1-FopHAXVran9RoSzgyXv6KMCE7WM=",
+          "dev": true
+        },
+        "dateformat": {
+          "version": "1.0.12",
+          "resolved": "https://registry.npmjs.org/dateformat/-/dateformat-1.0.12.tgz",
+          "integrity": "sha1-nxJLZ1lMk3/3BpMuSmQsyo27/uk=",
+          "dev": true,
+          "requires": {
+            "get-stdin": "4.0.1",
+            "meow": "3.7.0"
+          }
+        },
+        "esprima": {
+          "version": "2.7.3",
+          "resolved": "https://registry.npmjs.org/esprima/-/esprima-2.7.3.tgz",
+          "integrity": "sha1-luO3DVd59q1JzQMmc9HDEnZ7pYE=",
+          "dev": true
+        },
+        "findup-sync": {
+          "version": "0.3.0",
+          "resolved": "https://registry.npmjs.org/findup-sync/-/findup-sync-0.3.0.tgz",
+          "integrity": "sha1-N5MKpdgWt3fANEXhlmzGeQpMCxY=",
+          "dev": true,
+          "requires": {
+            "glob": "5.0.15"
+          },
+          "dependencies": {
+            "glob": {
+              "version": "5.0.15",
+              "resolv

<TRUNCATED>

[31/51] [partial] nifi-registry git commit: NIFIREG-201 Refactoring project structure to better isolate extensions

Posted by kd...@apache.org.
http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/6f26290d/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/service/DataModelMapper.java
----------------------------------------------------------------------
diff --git a/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/service/DataModelMapper.java b/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/service/DataModelMapper.java
new file mode 100644
index 0000000..3436662
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/service/DataModelMapper.java
@@ -0,0 +1,168 @@
+/*
+ * 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.service;
+
+import org.apache.nifi.registry.bucket.Bucket;
+import org.apache.nifi.registry.db.entity.BucketEntity;
+import org.apache.nifi.registry.db.entity.BucketItemEntityType;
+import org.apache.nifi.registry.db.entity.FlowEntity;
+import org.apache.nifi.registry.db.entity.FlowSnapshotEntity;
+import org.apache.nifi.registry.db.entity.KeyEntity;
+import org.apache.nifi.registry.diff.ComponentDifference;
+import org.apache.nifi.registry.diff.ComponentDifferenceGroup;
+import org.apache.nifi.registry.flow.VersionedComponent;
+import org.apache.nifi.registry.flow.VersionedFlow;
+import org.apache.nifi.registry.flow.VersionedFlowSnapshotMetadata;
+import org.apache.nifi.registry.flow.diff.FlowDifference;
+import org.apache.nifi.registry.security.key.Key;
+
+import java.util.Date;
+
+/**
+ * Utility for mapping between Provider API and the registry data model.
+ */
+public class DataModelMapper {
+
+    // --- Map buckets
+
+    public static BucketEntity map(final Bucket bucket) {
+        final BucketEntity bucketEntity = new BucketEntity();
+        bucketEntity.setId(bucket.getIdentifier());
+        bucketEntity.setName(bucket.getName());
+        bucketEntity.setDescription(bucket.getDescription());
+        bucketEntity.setCreated(new Date(bucket.getCreatedTimestamp()));
+        return bucketEntity;
+    }
+
+    public static Bucket map(final BucketEntity bucketEntity) {
+        final Bucket bucket = new Bucket();
+        bucket.setIdentifier(bucketEntity.getId());
+        bucket.setName(bucketEntity.getName());
+        bucket.setDescription(bucketEntity.getDescription());
+        bucket.setCreatedTimestamp(bucketEntity.getCreated().getTime());
+        return bucket;
+    }
+
+    // --- Map flows
+
+    public static FlowEntity map(final VersionedFlow versionedFlow) {
+        final FlowEntity flowEntity = new FlowEntity();
+        flowEntity.setId(versionedFlow.getIdentifier());
+        flowEntity.setName(versionedFlow.getName());
+        flowEntity.setDescription(versionedFlow.getDescription());
+        flowEntity.setCreated(new Date(versionedFlow.getCreatedTimestamp()));
+        flowEntity.setModified(new Date(versionedFlow.getModifiedTimestamp()));
+        flowEntity.setType(BucketItemEntityType.FLOW);
+        return flowEntity;
+    }
+
+    public static VersionedFlow map(final BucketEntity bucketEntity, final FlowEntity flowEntity) {
+        final VersionedFlow versionedFlow = new VersionedFlow();
+        versionedFlow.setIdentifier(flowEntity.getId());
+        versionedFlow.setBucketIdentifier(flowEntity.getBucketId());
+        versionedFlow.setName(flowEntity.getName());
+        versionedFlow.setDescription(flowEntity.getDescription());
+        versionedFlow.setCreatedTimestamp(flowEntity.getCreated().getTime());
+        versionedFlow.setModifiedTimestamp(flowEntity.getModified().getTime());
+        versionedFlow.setVersionCount(flowEntity.getSnapshotCount());
+
+        if (bucketEntity != null) {
+            versionedFlow.setBucketName(bucketEntity.getName());
+        } else {
+            versionedFlow.setBucketName(flowEntity.getBucketName());
+        }
+
+        return versionedFlow;
+    }
+
+    // --- Map snapshots
+
+    public static FlowSnapshotEntity map(final VersionedFlowSnapshotMetadata versionedFlowSnapshot) {
+        final FlowSnapshotEntity flowSnapshotEntity = new FlowSnapshotEntity();
+        flowSnapshotEntity.setFlowId(versionedFlowSnapshot.getFlowIdentifier());
+        flowSnapshotEntity.setVersion(versionedFlowSnapshot.getVersion());
+        flowSnapshotEntity.setComments(versionedFlowSnapshot.getComments());
+        flowSnapshotEntity.setCreated(new Date(versionedFlowSnapshot.getTimestamp()));
+        flowSnapshotEntity.setCreatedBy(versionedFlowSnapshot.getAuthor());
+        return flowSnapshotEntity;
+    }
+
+    public static VersionedFlowSnapshotMetadata map(final BucketEntity bucketEntity, final FlowSnapshotEntity flowSnapshotEntity) {
+        final VersionedFlowSnapshotMetadata metadata = new VersionedFlowSnapshotMetadata();
+        metadata.setFlowIdentifier(flowSnapshotEntity.getFlowId());
+        metadata.setVersion(flowSnapshotEntity.getVersion());
+        metadata.setComments(flowSnapshotEntity.getComments());
+        metadata.setTimestamp(flowSnapshotEntity.getCreated().getTime());
+        metadata.setAuthor(flowSnapshotEntity.getCreatedBy());
+
+        if (bucketEntity != null) {
+            metadata.setBucketIdentifier(bucketEntity.getId());
+        }
+
+        return metadata;
+    }
+
+    public static ComponentDifference map(final FlowDifference flowDifference){
+        ComponentDifference diff = new ComponentDifference();
+        diff.setChangeDescription(flowDifference.getDescription());
+        diff.setDifferenceType(flowDifference.getDifferenceType().toString());
+        diff.setDifferenceTypeDescription(flowDifference.getDifferenceType().getDescription());
+        diff.setValueA(getValueDescription(flowDifference.getValueA()));
+        diff.setValueB(getValueDescription(flowDifference.getValueB()));
+        return diff;
+    }
+
+    public static ComponentDifferenceGroup map(VersionedComponent versionedComponent){
+        ComponentDifferenceGroup grouping = new ComponentDifferenceGroup();
+        grouping.setComponentId(versionedComponent.getIdentifier());
+        grouping.setComponentName(versionedComponent.getName());
+        grouping.setProcessGroupId(versionedComponent.getGroupIdentifier());
+        grouping.setComponentType(versionedComponent.getComponentType().getTypeName());
+        return grouping;
+    }
+
+    private static String getValueDescription(Object valueA){
+        if(valueA instanceof VersionedComponent){
+            return ((VersionedComponent) valueA).getIdentifier();
+        }
+        if(valueA!= null){
+            return valueA.toString();
+        }
+        return null;
+    }
+
+    // --- Map keys
+
+    public static Key map(final KeyEntity keyEntity) {
+        final Key key = new Key();
+        key.setId(keyEntity.getId());
+        key.setIdentity(keyEntity.getTenantIdentity());
+        key.setKey(keyEntity.getKeyValue());
+        return key;
+    }
+
+    public static KeyEntity map(final Key key) {
+        final KeyEntity keyEntity = new KeyEntity();
+        keyEntity.setId(key.getId());
+        keyEntity.setTenantIdentity(key.getIdentity());
+        keyEntity.setKeyValue(key.getKey());
+        return keyEntity;
+    }
+
+    // map
+
+}

http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/6f26290d/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/service/MetadataService.java
----------------------------------------------------------------------
diff --git a/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/service/MetadataService.java b/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/service/MetadataService.java
new file mode 100644
index 0000000..ea0b214
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/service/MetadataService.java
@@ -0,0 +1,232 @@
+/*
+ * 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.service;
+
+import org.apache.nifi.registry.db.entity.BucketEntity;
+import org.apache.nifi.registry.db.entity.BucketItemEntity;
+import org.apache.nifi.registry.db.entity.FlowEntity;
+import org.apache.nifi.registry.db.entity.FlowSnapshotEntity;
+
+import java.util.List;
+import java.util.Set;
+
+/**
+ * A service for managing metadata about all objects stored by the registry.
+ *
+ */
+public interface MetadataService {
+
+    /**
+     * Creates the given bucket.
+     *
+     * @param bucket the bucket to create
+     * @return the created bucket
+     */
+    BucketEntity createBucket(BucketEntity bucket);
+
+    /**
+     * Retrieves the bucket with the given id.
+     *
+     * @param bucketIdentifier the id of the bucket to retrieve
+     * @return the bucket with the given id, or null if it does not exist
+     */
+    BucketEntity getBucketById(String bucketIdentifier);
+
+    /**
+     * Retrieves the buckets with the given name. The name comparison must be case-insensitive.
+     *
+     * @param name the name of the bucket to retrieve
+     * @return the buckets with the given name, or empty list if none exist
+     */
+    List<BucketEntity> getBucketsByName(String name);
+
+    /**
+     * Updates the given bucket, only the name and description should be allowed to be updated.
+     *
+     * @param bucket the updated bucket to save
+     * @return the updated bucket, or null if no bucket with the given id exists
+     */
+    BucketEntity updateBucket(BucketEntity bucket);
+
+    /**
+     * Deletes the bucket, as well as any objects that reference the bucket.
+     *
+     * @param bucket the bucket to delete
+     */
+    void deleteBucket(BucketEntity bucket);
+
+    /**
+     * Retrieves all buckets with the given ids.
+     *
+     * @param bucketIds the ids of the buckets to retrieve
+     * @return the set of all buckets
+     */
+    List<BucketEntity> getBuckets(Set<String> bucketIds);
+
+    /**
+     * Retrieves all buckets.
+     *
+     * @return the set of all buckets
+     */
+    List<BucketEntity> getAllBuckets();
+
+    // --------------------------------------------------------------------------------------------
+
+    /**
+     * Retrieves items for the given bucket.
+     *
+     * @param bucketId the id of bucket to retrieve items for
+     * @return the set of items for the bucket
+     */
+    List<BucketItemEntity> getBucketItems(String bucketId);
+
+    /**
+     * Retrieves items for the given buckets.
+     *
+     * @param bucketIds the ids of buckets to retrieve items for
+     * @return the set of items for the bucket
+     */
+    List<BucketItemEntity> getBucketItems(Set<String> bucketIds);
+
+    // --------------------------------------------------------------------------------------------
+
+    /**
+     * Creates a versioned flow in the given bucket.
+     *
+     * @param flow the versioned flow to create
+     * @return the created versioned flow
+     * @throws IllegalStateException if no bucket with the given identifier exists
+     */
+    FlowEntity createFlow(FlowEntity flow);
+
+    /**
+     * Retrieves the versioned flow with the given id and DOES NOT populate the versionCount.
+     *
+     * @param flowIdentifier the identifier of the flow to retrieve
+     * @return the versioned flow with the given id, or null if no flow with the given id exists
+     */
+    FlowEntity getFlowById(String flowIdentifier);
+
+    /**
+     * Retrieves the versioned flow with the given id and DOES populate the versionCount.
+     *
+     * @param flowIdentifier the identifier of the flow to retrieve
+     * @return the versioned flow with the given id, or null if no flow with the given id exists
+     */
+    FlowEntity getFlowByIdWithSnapshotCounts(String flowIdentifier);
+
+    /**
+     * Retrieves the versioned flows with the given name. The name comparison must be case-insensitive.
+     *
+     * @param name the name of the flow to retrieve
+     * @return the versioned flows with the given name, or empty list if no flows with the given name exists
+     */
+    List<FlowEntity> getFlowsByName(String name);
+
+    /**
+     * Retrieves the versioned flows with the given name in the given bucket. The name comparison must be case-insensitive.
+     *
+     * @param  bucketIdentifier the identifier of the bucket
+     * @param name the name of the flow to retrieve
+     * @return the versioned flows with the given name in the given bucket, or empty list if no flows with the given name exists
+     */
+    List<FlowEntity> getFlowsByName(String bucketIdentifier, String name);
+
+    /**
+     * Retrieves the versioned flows for the given bucket.
+     *
+     * @param bucketIdentifier the bucket id to retrieve flows for
+     * @return the flows in the given bucket
+     */
+    List<FlowEntity> getFlowsByBucket(String bucketIdentifier);
+
+    /**
+     * Updates the given versioned flow, only the name and description should be allowed to be updated.
+     *
+     * @param flow the updated versioned flow to save
+     * @return the updated versioned flow
+     */
+    FlowEntity updateFlow(FlowEntity flow);
+
+    /**
+     * Deletes the flow if one exists.
+     *
+     * @param flow the flow to delete
+     */
+    void deleteFlow(FlowEntity flow);
+
+    // --------------------------------------------------------------------------------------------
+
+    /**
+     * Creates a versioned flow snapshot.
+     *
+     * @param flowSnapshot the snapshot to create
+     * @return the created snapshot
+     * @throws IllegalStateException if the versioned flow specified by flowSnapshot.getFlowIdentifier() does not exist
+     */
+    FlowSnapshotEntity createFlowSnapshot(FlowSnapshotEntity flowSnapshot);
+
+    /**
+     * Retrieves the snapshot for the given flow identifier and snapshot version.
+     *
+     * @param flowIdentifier the identifier of the flow the snapshot belongs to
+     * @param version the version of the snapshot
+     * @return the versioned flow snapshot for the given flow identifier and version, or null if none exists
+     */
+    FlowSnapshotEntity getFlowSnapshot(String flowIdentifier, Integer version);
+
+    /**
+     * Retrieves the snapshot with the latest version number for the given flow in the given bucket.
+     *
+     * @param flowIdentifier the id of flow to retrieve the latest snapshot for
+     * @return the latest snapshot for the flow, or null if one doesn't exist
+     */
+    FlowSnapshotEntity getLatestSnapshot(String flowIdentifier);
+
+    /**
+     * Retrieves the snapshots for the given flow in the given bucket.
+     *
+     * @param flowIdentifier the id of the flow
+     * @return the snapshots
+     */
+    List<FlowSnapshotEntity> getSnapshots(String flowIdentifier);
+
+    /**
+     * Deletes the flow snapshot.
+     *
+     * @param flowSnapshot the flow snapshot to delete
+     */
+    void deleteFlowSnapshot(FlowSnapshotEntity flowSnapshot);
+
+    // --------------------------------------------------------------------------------------------
+
+    /**
+     * @return the set of field names for Buckets
+     */
+    Set<String> getBucketFields();
+
+    /**
+     * @return the set of field names for BucketItems
+     */
+    Set<String> getBucketItemFields();
+
+    /**
+     * @return the set of field names for Flows
+     */
+    Set<String> getFlowFields();
+
+}
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/6f26290d/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/service/QueryParameters.java
----------------------------------------------------------------------
diff --git a/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/service/QueryParameters.java b/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/service/QueryParameters.java
new file mode 100644
index 0000000..99ef9dd
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/service/QueryParameters.java
@@ -0,0 +1,114 @@
+/*
+ * 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.service;
+
+import org.apache.nifi.registry.params.SortOrder;
+import org.apache.nifi.registry.params.SortParameter;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.List;
+
+/**
+ * Parameters to be passed into service layer for methods that require sorting and paging.
+ */
+public class QueryParameters {
+
+    public static final QueryParameters EMPTY_PARAMETERS = new QueryParameters.Builder().build();
+
+    private final Integer pageNum;
+
+    private final Integer numRows;
+
+    private final List<SortParameter> sortParameters;
+
+    private QueryParameters(final Builder builder) {
+        this.pageNum = builder.pageNum;
+        this.numRows = builder.numRows;
+        this.sortParameters = Collections.unmodifiableList(new ArrayList<>(builder.sortParameters));
+
+        if (this.pageNum != null && this.numRows != null) {
+            if (this.pageNum < 0) {
+                throw new IllegalStateException("Offset cannot be negative");
+            }
+
+            if (this.numRows < 0) {
+                throw new IllegalStateException("Number of rows cannot be negative");
+            }
+        }
+    }
+
+    public Integer getPageNum() {
+        return pageNum;
+    }
+
+    public Integer getNumRows() {
+        return numRows;
+    }
+
+    public List<SortParameter> getSortParameters() {
+        return sortParameters;
+    }
+
+    /**
+     * Builder for QueryParameters.
+     */
+    public static class Builder {
+
+        private Integer pageNum;
+        private Integer numRows;
+        private List<SortParameter> sortParameters = new ArrayList<>();
+
+        public Builder pageNum(Integer pageNum) {
+            this.pageNum = pageNum;
+            return this;
+        }
+
+        public Builder numRows(Integer numRows) {
+            this.numRows = numRows;
+            return this;
+        }
+
+        public Builder addSort(final SortParameter sort) {
+            this.sortParameters.add(sort);
+            return this;
+        }
+
+        public Builder addSort(final String fieldName, final SortOrder order) {
+            this.sortParameters.add(new SortParameter(fieldName, order));
+            return this;
+        }
+
+        public Builder addSorts(final Collection<SortParameter> sorts) {
+            if (sorts != null) {
+                this.sortParameters.addAll(sorts);
+            }
+            return this;
+        }
+
+        public Builder clearSorts() {
+            this.sortParameters.clear();
+            return this;
+        }
+
+        public QueryParameters build() {
+            return new QueryParameters(this);
+        }
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/6f26290d/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/service/RegistryService.java
----------------------------------------------------------------------
diff --git a/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/service/RegistryService.java b/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/service/RegistryService.java
new file mode 100644
index 0000000..23f1d14
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/service/RegistryService.java
@@ -0,0 +1,994 @@
+/*
+ * 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.service;
+
+import org.apache.commons.lang3.ObjectUtils;
+import org.apache.commons.lang3.StringUtils;
+import org.apache.commons.lang3.Validate;
+import org.apache.nifi.registry.bucket.Bucket;
+import org.apache.nifi.registry.bucket.BucketItem;
+import org.apache.nifi.registry.db.entity.BucketEntity;
+import org.apache.nifi.registry.db.entity.BucketItemEntity;
+import org.apache.nifi.registry.db.entity.FlowEntity;
+import org.apache.nifi.registry.db.entity.FlowSnapshotEntity;
+import org.apache.nifi.registry.diff.ComponentDifferenceGroup;
+import org.apache.nifi.registry.diff.VersionedFlowDifference;
+import org.apache.nifi.registry.exception.ResourceNotFoundException;
+import org.apache.nifi.registry.flow.FlowPersistenceProvider;
+import org.apache.nifi.registry.flow.FlowSnapshotContext;
+import org.apache.nifi.registry.flow.VersionedComponent;
+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.flow.VersionedProcessGroup;
+import org.apache.nifi.registry.flow.diff.ComparableDataFlow;
+import org.apache.nifi.registry.flow.diff.ConciseEvolvingDifferenceDescriptor;
+import org.apache.nifi.registry.flow.diff.FlowComparator;
+import org.apache.nifi.registry.flow.diff.FlowComparison;
+import org.apache.nifi.registry.flow.diff.FlowDifference;
+import org.apache.nifi.registry.flow.diff.StandardComparableDataFlow;
+import org.apache.nifi.registry.flow.diff.StandardFlowComparator;
+import org.apache.nifi.registry.provider.flow.StandardFlowSnapshotContext;
+import org.apache.nifi.registry.serialization.Serializer;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+
+import javax.validation.ConstraintViolation;
+import javax.validation.ConstraintViolationException;
+import javax.validation.Validator;
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.InputStream;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.SortedSet;
+import java.util.TreeSet;
+import java.util.UUID;
+import java.util.concurrent.locks.Lock;
+import java.util.concurrent.locks.ReentrantReadWriteLock;
+import java.util.stream.Collectors;
+
+/**
+ * Main service for all back-end operations, REST resources should only interact with this service.
+ *
+ * This service is marked as @Transactional so that Spring will automatically start a transaction upon entering
+ * any method, and will rollback the transaction if any Exception is thrown out of a method.
+ *
+ */
+@Service
+@Transactional(rollbackFor = Exception.class)
+public class RegistryService {
+
+    private static final Logger LOGGER = LoggerFactory.getLogger(RegistryService.class);
+
+    private final MetadataService metadataService;
+    private final FlowPersistenceProvider flowPersistenceProvider;
+    private final Serializer<VersionedProcessGroup> processGroupSerializer;
+    private final Validator validator;
+
+    private final ReentrantReadWriteLock lock = new ReentrantReadWriteLock();
+    private final Lock readLock = lock.readLock();
+    private final Lock writeLock = lock.writeLock();
+
+    @Autowired
+    public RegistryService(final MetadataService metadataService,
+                           final FlowPersistenceProvider flowPersistenceProvider,
+                           final Serializer<VersionedProcessGroup> processGroupSerializer,
+                           final Validator validator) {
+        this.metadataService = metadataService;
+        this.flowPersistenceProvider = flowPersistenceProvider;
+        this.processGroupSerializer = processGroupSerializer;
+        this.validator = validator;
+        Validate.notNull(this.metadataService);
+        Validate.notNull(this.flowPersistenceProvider);
+        Validate.notNull(this.processGroupSerializer);
+        Validate.notNull(this.validator);
+    }
+
+    private <T>  void validate(T t, String invalidMessage) {
+        final Set<ConstraintViolation<T>> violations = validator.validate(t);
+        if (violations.size() > 0) {
+            throw new ConstraintViolationException(invalidMessage, violations);
+        }
+    }
+
+    // ---------------------- Bucket methods ---------------------------------------------
+
+    public Bucket createBucket(final Bucket bucket) {
+        if (bucket == null) {
+            throw new IllegalArgumentException("Bucket cannot be null");
+        }
+
+        // set an id, the created time, and clear out the flows since its read-only
+        bucket.setIdentifier(UUID.randomUUID().toString());
+        bucket.setCreatedTimestamp(System.currentTimeMillis());
+
+        validate(bucket, "Cannot create Bucket");
+
+        writeLock.lock();
+        try {
+            final List<BucketEntity> bucketsWithSameName = metadataService.getBucketsByName(bucket.getName());
+            if (bucketsWithSameName.size() > 0) {
+                throw new IllegalStateException("A bucket with the same name already exists");
+            }
+
+            final BucketEntity createdBucket = metadataService.createBucket(DataModelMapper.map(bucket));
+            return DataModelMapper.map(createdBucket);
+        } finally {
+            writeLock.unlock();
+        }
+    }
+
+    public Bucket getBucket(final String bucketIdentifier) {
+        if (bucketIdentifier == null) {
+            throw new IllegalArgumentException("Bucket identifier cannot be null");
+        }
+
+        readLock.lock();
+        try {
+            final BucketEntity bucket = metadataService.getBucketById(bucketIdentifier);
+            if (bucket == null) {
+                LOGGER.warn("The specified bucket id [{}] does not exist.", bucketIdentifier);
+                throw new ResourceNotFoundException("The specified bucket ID does not exist in this registry.");
+            }
+
+            return DataModelMapper.map(bucket);
+        } finally {
+            readLock.unlock();
+        }
+    }
+
+    public List<Bucket> getBuckets() {
+        readLock.lock();
+        try {
+            final List<BucketEntity> buckets = metadataService.getAllBuckets();
+            return buckets.stream().map(b -> DataModelMapper.map(b)).collect(Collectors.toList());
+        } finally {
+            readLock.unlock();
+        }
+    }
+
+    public List<Bucket> getBuckets(final Set<String> bucketIds) {
+        readLock.lock();
+        try {
+            final List<BucketEntity> buckets = metadataService.getBuckets(bucketIds);
+            return buckets.stream().map(b -> DataModelMapper.map(b)).collect(Collectors.toList());
+        } finally {
+            readLock.unlock();
+        }
+    }
+
+    public Bucket updateBucket(final Bucket bucket) {
+        if (bucket == null) {
+            throw new IllegalArgumentException("Bucket cannot be null");
+        }
+
+        if (bucket.getIdentifier() == null) {
+            throw new IllegalArgumentException("Bucket identifier cannot be null");
+        }
+
+        if (bucket.getName() != null && StringUtils.isBlank(bucket.getName())) {
+            throw new IllegalArgumentException("Bucket name cannot be blank");
+        }
+
+        writeLock.lock();
+        try {
+            // ensure a bucket with the given id exists
+            final BucketEntity existingBucketById = metadataService.getBucketById(bucket.getIdentifier());
+            if (existingBucketById == null) {
+                LOGGER.warn("The specified bucket id [{}] does not exist.", bucket.getIdentifier());
+                throw new ResourceNotFoundException("The specified bucket ID does not exist in this registry.");
+            }
+
+            // ensure a different bucket with the same name does not exist
+            // since we're allowing partial updates here, only check this if a non-null name is provided
+            if (StringUtils.isNotBlank(bucket.getName())) {
+                final List<BucketEntity> bucketsWithSameName = metadataService.getBucketsByName(bucket.getName());
+                if (bucketsWithSameName != null) {
+                    for (final BucketEntity bucketWithSameName : bucketsWithSameName) {
+                        if (!bucketWithSameName.getId().equals(existingBucketById.getId())){
+                            throw new IllegalStateException("A bucket with the same name already exists - " + bucket.getName());
+                        }
+                    }
+                }
+            }
+
+            // transfer over the new values to the existing bucket
+            if (StringUtils.isNotBlank(bucket.getName())) {
+                existingBucketById.setName(bucket.getName());
+            }
+
+            if (bucket.getDescription() != null) {
+                existingBucketById.setDescription(bucket.getDescription());
+            }
+
+            // perform the actual update
+            final BucketEntity updatedBucket = metadataService.updateBucket(existingBucketById);
+            return DataModelMapper.map(updatedBucket);
+        } finally {
+            writeLock.unlock();
+        }
+    }
+
+    public Bucket deleteBucket(final String bucketIdentifier) {
+        if (bucketIdentifier == null) {
+            throw new IllegalArgumentException("Bucket identifier cannot be null");
+        }
+
+        writeLock.lock();
+        try {
+            // ensure the bucket exists
+            final BucketEntity existingBucket = metadataService.getBucketById(bucketIdentifier);
+            if (existingBucket == null) {
+                LOGGER.warn("The specified bucket id [{}] does not exist.", bucketIdentifier);
+                throw new ResourceNotFoundException("The specified bucket ID does not exist in this registry.");
+            }
+
+            // for each flow in the bucket, delete all snapshots from the flow persistence provider
+            for (final FlowEntity flowEntity : metadataService.getFlowsByBucket(existingBucket.getId())) {
+                flowPersistenceProvider.deleteAllFlowContent(bucketIdentifier, flowEntity.getId());
+            }
+
+            // now delete the bucket from the metadata provider, which deletes all flows referencing it
+            metadataService.deleteBucket(existingBucket);
+
+            return DataModelMapper.map(existingBucket);
+        } finally {
+            writeLock.unlock();
+        }
+    }
+
+    // ---------------------- BucketItem methods ---------------------------------------------
+
+    public List<BucketItem> getBucketItems(final String bucketIdentifier) {
+        if (bucketIdentifier == null) {
+            throw new IllegalArgumentException("Bucket identifier cannot be null");
+        }
+
+        readLock.lock();
+        try {
+            final BucketEntity bucket = metadataService.getBucketById(bucketIdentifier);
+            if (bucket == null) {
+                LOGGER.warn("The specified bucket id [{}] does not exist.", bucketIdentifier);
+                throw new ResourceNotFoundException("The specified bucket ID does not exist in this registry.");
+            }
+
+            final List<BucketItem> bucketItems = new ArrayList<>();
+            metadataService.getBucketItems(bucket.getId()).stream().forEach(b -> addBucketItem(bucketItems, b));
+            return bucketItems;
+        } finally {
+            readLock.unlock();
+        }
+    }
+
+    public List<BucketItem> getBucketItems(final Set<String> bucketIdentifiers) {
+        if (bucketIdentifiers == null || bucketIdentifiers.isEmpty()) {
+            throw new IllegalArgumentException("Bucket identifiers cannot be null or empty");
+        }
+
+        readLock.lock();
+        try {
+            final List<BucketItem> bucketItems = new ArrayList<>();
+            metadataService.getBucketItems(bucketIdentifiers).stream().forEach(b -> addBucketItem(bucketItems, b));
+            return bucketItems;
+        } finally {
+            readLock.unlock();
+        }
+    }
+
+    private void addBucketItem(final List<BucketItem> bucketItems, final BucketItemEntity itemEntity) {
+        if (itemEntity instanceof FlowEntity) {
+            final FlowEntity flowEntity = (FlowEntity) itemEntity;
+
+            // Currently we don't populate the bucket name for items
+            bucketItems.add(DataModelMapper.map(null, flowEntity));
+        } else {
+            LOGGER.error("Unknown type of BucketItemEntity: " + itemEntity.getClass().getCanonicalName());
+        }
+    }
+
+    // ---------------------- VersionedFlow methods ---------------------------------------------
+
+    public VersionedFlow createFlow(final String bucketIdentifier, final VersionedFlow versionedFlow) {
+        if (StringUtils.isBlank(bucketIdentifier)) {
+            throw new IllegalArgumentException("Bucket identifier cannot be null or blank");
+        }
+
+        if (versionedFlow == null) {
+            throw new IllegalArgumentException("Versioned flow cannot be null");
+        }
+
+        if (versionedFlow.getBucketIdentifier() != null && !bucketIdentifier.equals(versionedFlow.getBucketIdentifier())) {
+            throw new IllegalArgumentException("Bucket identifiers must match");
+        }
+
+        if (versionedFlow.getBucketIdentifier() == null) {
+            versionedFlow.setBucketIdentifier(bucketIdentifier);
+        }
+
+        versionedFlow.setIdentifier(UUID.randomUUID().toString());
+
+        final long timestamp = System.currentTimeMillis();
+        versionedFlow.setCreatedTimestamp(timestamp);
+        versionedFlow.setModifiedTimestamp(timestamp);
+
+        validate(versionedFlow, "Cannot create versioned flow");
+
+        writeLock.lock();
+        try {
+            // ensure the bucket exists
+            final BucketEntity existingBucket = metadataService.getBucketById(bucketIdentifier);
+            if (existingBucket == null) {
+                LOGGER.warn("The specified bucket id [{}] does not exist.", bucketIdentifier);
+                throw new ResourceNotFoundException("The specified bucket ID does not exist in this registry.");
+            }
+
+            // ensure another flow with the same name doesn't exist
+            final List<FlowEntity> flowsWithSameName = metadataService.getFlowsByName(existingBucket.getId(), versionedFlow.getName());
+            if (flowsWithSameName != null && flowsWithSameName.size() > 0) {
+                throw new IllegalStateException("A versioned flow with the same name already exists in the selected bucket");
+            }
+
+            // convert from dto to entity and set the bucket relationship
+            final FlowEntity flowEntity = DataModelMapper.map(versionedFlow);
+            flowEntity.setBucketId(existingBucket.getId());
+
+            // persist the flow and return the created entity
+            final FlowEntity createdFlow = metadataService.createFlow(flowEntity);
+            return DataModelMapper.map(existingBucket, createdFlow);
+        } finally {
+            writeLock.unlock();
+        }
+    }
+
+    public VersionedFlow getFlow(final String bucketIdentifier, final String flowIdentifier) {
+        if (StringUtils.isBlank(bucketIdentifier)) {
+            throw new IllegalArgumentException("Bucket identifier cannot be null or blank");
+        }
+
+        if (StringUtils.isBlank(flowIdentifier)) {
+            throw new IllegalArgumentException("Versioned flow identifier cannot be null or blank");
+        }
+
+        readLock.lock();
+        try {
+            // ensure the bucket exists
+            final BucketEntity existingBucket = metadataService.getBucketById(bucketIdentifier);
+            if (existingBucket == null) {
+                LOGGER.warn("The specified bucket id [{}] does not exist.", bucketIdentifier);
+                throw new ResourceNotFoundException("The specified bucket ID does not exist in this registry.");
+            }
+
+            final FlowEntity existingFlow = metadataService.getFlowByIdWithSnapshotCounts(flowIdentifier);
+            if (existingFlow == null) {
+                LOGGER.warn("The specified flow id [{}] does not exist.", flowIdentifier);
+                throw new ResourceNotFoundException("The specified flow ID does not exist in this bucket.");
+            }
+
+            if (!existingBucket.getId().equals(existingFlow.getBucketId())) {
+                throw new IllegalStateException("The requested flow is not located in the given bucket");
+            }
+
+            return DataModelMapper.map(existingBucket, existingFlow);
+        } finally {
+            readLock.unlock();
+        }
+    }
+
+    public VersionedFlow getFlow(final String flowIdentifier) {
+        if (StringUtils.isBlank(flowIdentifier)) {
+            throw new IllegalArgumentException("Versioned flow identifier cannot be null or blank");
+        }
+
+        readLock.lock();
+        try {
+            final FlowEntity existingFlow = metadataService.getFlowByIdWithSnapshotCounts(flowIdentifier);
+            if (existingFlow == null) {
+                LOGGER.warn("The specified flow id [{}] does not exist.", flowIdentifier);
+                throw new ResourceNotFoundException("The specified flow ID does not exist.");
+            }
+
+            final BucketEntity existingBucket = metadataService.getBucketById(existingFlow.getBucketId());
+            return DataModelMapper.map(existingBucket, existingFlow);
+        } finally {
+            readLock.unlock();
+        }
+    }
+
+    public List<VersionedFlow> getFlows(final String bucketId) {
+        if (StringUtils.isBlank(bucketId)) {
+            throw new IllegalArgumentException("Bucket identifier cannot be null");
+        }
+
+        readLock.lock();
+        try {
+            final BucketEntity existingBucket = metadataService.getBucketById(bucketId);
+            if (existingBucket == null) {
+                LOGGER.warn("The specified bucket id [{}] does not exist.", bucketId);
+                throw new ResourceNotFoundException("The specified bucket ID does not exist in this registry.");
+            }
+
+            // return non-verbose set of flows for the given bucket
+            final List<FlowEntity> flows = metadataService.getFlowsByBucket(existingBucket.getId());
+            return flows.stream().map(f -> DataModelMapper.map(existingBucket, f)).collect(Collectors.toList());
+        } finally {
+            readLock.unlock();
+        }
+    }
+
+    public VersionedFlow updateFlow(final VersionedFlow versionedFlow) {
+        if (versionedFlow == null) {
+            throw new IllegalArgumentException("Versioned flow cannot be null");
+        }
+
+        if (StringUtils.isBlank(versionedFlow.getIdentifier())) {
+            throw new IllegalArgumentException("Versioned flow identifier cannot be null or blank");
+        }
+
+        if (StringUtils.isBlank(versionedFlow.getBucketIdentifier())) {
+            throw new IllegalArgumentException("Versioned flow bucket identifier cannot be null or blank");
+        }
+
+        if (versionedFlow.getName() != null && StringUtils.isBlank(versionedFlow.getName())) {
+            throw new IllegalArgumentException("Versioned flow name cannot be blank");
+        }
+
+        writeLock.lock();
+        try {
+            // ensure the bucket exists
+            final BucketEntity existingBucket = metadataService.getBucketById(versionedFlow.getBucketIdentifier());
+            if (existingBucket == null) {
+                LOGGER.warn("The specified bucket id [{}] does not exist.", versionedFlow.getBucketIdentifier());
+                throw new ResourceNotFoundException("The specified bucket ID does not exist in this registry.");
+            }
+
+            final FlowEntity existingFlow = metadataService.getFlowByIdWithSnapshotCounts(versionedFlow.getIdentifier());
+            if (existingFlow == null) {
+                LOGGER.warn("The specified flow id [{}] does not exist.", versionedFlow.getIdentifier());
+                throw new ResourceNotFoundException("The specified flow ID does not exist in this bucket.");
+            }
+
+            if (!existingBucket.getId().equals(existingFlow.getBucketId())) {
+                throw new IllegalStateException("The requested flow is not located in the given bucket");
+            }
+
+            // ensure a different flow with the same name does not exist
+            // since we're allowing partial updates here, only check this if a non-null name is provided
+            if (StringUtils.isNotBlank(versionedFlow.getName())) {
+                final List<FlowEntity> flowsWithSameName = metadataService.getFlowsByName(existingBucket.getId(), versionedFlow.getName());
+                if (flowsWithSameName != null) {
+                    for (final FlowEntity flowWithSameName : flowsWithSameName) {
+                         if(!flowWithSameName.getId().equals(existingFlow.getId())) {
+                            throw new IllegalStateException("A versioned flow with the same name already exists in the selected bucket");
+                        }
+                    }
+                }
+            }
+
+            // transfer over the new values to the existing flow
+            if (StringUtils.isNotBlank(versionedFlow.getName())) {
+                existingFlow.setName(versionedFlow.getName());
+            }
+
+            if (versionedFlow.getDescription() != null) {
+                existingFlow.setDescription(versionedFlow.getDescription());
+            }
+
+            // perform the actual update
+            final FlowEntity updatedFlow = metadataService.updateFlow(existingFlow);
+            return DataModelMapper.map(existingBucket, updatedFlow);
+        } finally {
+            writeLock.unlock();
+        }
+    }
+
+    public VersionedFlow deleteFlow(final String bucketIdentifier, final String flowIdentifier) {
+        if (StringUtils.isBlank(bucketIdentifier)) {
+            throw new IllegalArgumentException("Bucket identifier cannot be null or blank");
+        }
+        if (StringUtils.isBlank(flowIdentifier)) {
+            throw new IllegalArgumentException("Flow identifier cannot be null or blank");
+        }
+
+        writeLock.lock();
+        try {
+            // ensure the bucket exists
+            final BucketEntity existingBucket = metadataService.getBucketById(bucketIdentifier);
+            if (existingBucket == null) {
+                LOGGER.warn("The specified bucket id [{}] does not exist.", bucketIdentifier);
+                throw new ResourceNotFoundException("The specified bucket ID does not exist in this registry.");
+            }
+
+            // ensure the flow exists
+            final FlowEntity existingFlow = metadataService.getFlowById(flowIdentifier);
+            if (existingFlow == null) {
+                LOGGER.warn("The specified flow id [{}] does not exist.", flowIdentifier);
+                throw new ResourceNotFoundException("The specified flow ID does not exist in this bucket.");
+            }
+
+            if (!existingBucket.getId().equals(existingFlow.getBucketId())) {
+                throw new IllegalStateException("The requested flow is not located in the given bucket");
+            }
+
+            // delete all snapshots from the flow persistence provider
+            flowPersistenceProvider.deleteAllFlowContent(existingFlow.getBucketId(), existingFlow.getId());
+
+            // now delete the flow from the metadata provider
+            metadataService.deleteFlow(existingFlow);
+
+            return DataModelMapper.map(existingBucket, existingFlow);
+        } finally {
+            writeLock.unlock();
+        }
+    }
+
+    // ---------------------- VersionedFlowSnapshot methods ---------------------------------------------
+
+    public VersionedFlowSnapshot createFlowSnapshot(final VersionedFlowSnapshot flowSnapshot) {
+        if (flowSnapshot == null) {
+            throw new IllegalArgumentException("Versioned flow snapshot cannot be null");
+        }
+
+        // validation will ensure that the metadata and contents are not null
+        if (flowSnapshot.getSnapshotMetadata() != null) {
+            flowSnapshot.getSnapshotMetadata().setTimestamp(System.currentTimeMillis());
+        }
+
+        // these fields aren't used for creation
+        flowSnapshot.setFlow(null);
+        flowSnapshot.setBucket(null);
+
+        validate(flowSnapshot, "Cannot create versioned flow snapshot");
+
+        writeLock.lock();
+        try {
+            final VersionedFlowSnapshotMetadata snapshotMetadata = flowSnapshot.getSnapshotMetadata();
+
+            // ensure the bucket exists
+            final BucketEntity existingBucket = metadataService.getBucketById(snapshotMetadata.getBucketIdentifier());
+            if (existingBucket == null) {
+                LOGGER.warn("The specified bucket id [{}] does not exist.", snapshotMetadata.getBucketIdentifier());
+                throw new ResourceNotFoundException("The specified bucket ID does not exist in this registry.");
+            }
+
+            // ensure the flow exists
+            final FlowEntity existingFlow = metadataService.getFlowById(snapshotMetadata.getFlowIdentifier());
+            if (existingFlow == null) {
+                LOGGER.warn("The specified flow id [{}] does not exist.", snapshotMetadata.getFlowIdentifier());
+                throw new ResourceNotFoundException("The specified flow ID does not exist in this bucket.");
+            }
+
+            if (!existingBucket.getId().equals(existingFlow.getBucketId())) {
+                throw new IllegalStateException("The requested flow is not located in the given bucket");
+            }
+
+            // convert the set of FlowSnapshotEntity to set of VersionedFlowSnapshotMetadata
+            final SortedSet<VersionedFlowSnapshotMetadata> sortedSnapshots = new TreeSet<>();
+            final List<FlowSnapshotEntity> existingFlowSnapshots = metadataService.getSnapshots(existingFlow.getId());
+            if (existingFlowSnapshots != null) {
+                existingFlowSnapshots.stream().forEach(s -> sortedSnapshots.add(DataModelMapper.map(existingBucket, s)));
+            }
+
+            // if we already have snapshots we need to verify the new one has the correct version
+            if (sortedSnapshots != null && sortedSnapshots.size() > 0) {
+                final VersionedFlowSnapshotMetadata lastSnapshot = sortedSnapshots.last();
+
+                if (snapshotMetadata.getVersion() <= lastSnapshot.getVersion()) {
+                    throw new IllegalStateException("A Versioned flow snapshot with the same version already exists: " + snapshotMetadata.getVersion());
+                }
+
+                if (snapshotMetadata.getVersion() > (lastSnapshot.getVersion() + 1)) {
+                    throw new IllegalStateException("Version must be a one-up number, last version was "
+                            + lastSnapshot.getVersion() + " and version for this snapshot was "
+                            + snapshotMetadata.getVersion());
+                }
+            } else if (snapshotMetadata.getVersion() != 1) {
+                throw new IllegalStateException("Version of first snapshot must be 1");
+            }
+
+            // serialize the snapshot
+            final ByteArrayOutputStream out = new ByteArrayOutputStream();
+            processGroupSerializer.serialize(flowSnapshot.getFlowContents(), out);
+
+            // save the serialized snapshot to the persistence provider
+            final Bucket bucket = DataModelMapper.map(existingBucket);
+            final VersionedFlow versionedFlow = DataModelMapper.map(existingBucket, existingFlow);
+            final FlowSnapshotContext context = new StandardFlowSnapshotContext.Builder(bucket, versionedFlow, snapshotMetadata).build();
+            flowPersistenceProvider.saveFlowContent(context, out.toByteArray());
+
+            // create snapshot in the metadata provider
+            metadataService.createFlowSnapshot(DataModelMapper.map(snapshotMetadata));
+
+            // update the modified date on the flow
+            metadataService.updateFlow(existingFlow);
+
+            // get the updated flow, we need to use "with counts" here so we can return this is a part of the response
+            final FlowEntity updatedFlow = metadataService.getFlowByIdWithSnapshotCounts(snapshotMetadata.getFlowIdentifier());
+            if (updatedFlow == null) {
+                throw new ResourceNotFoundException("Versioned flow does not exist for identifier " + snapshotMetadata.getFlowIdentifier());
+            }
+            final VersionedFlow updatedVersionedFlow = DataModelMapper.map(existingBucket, updatedFlow);
+
+            flowSnapshot.setBucket(bucket);
+            flowSnapshot.setFlow(updatedVersionedFlow);
+            return flowSnapshot;
+        } finally {
+            writeLock.unlock();
+        }
+    }
+
+    public VersionedFlowSnapshot getFlowSnapshot(final String bucketIdentifier, final String flowIdentifier, final Integer version) {
+        if (StringUtils.isBlank(bucketIdentifier)) {
+            throw new IllegalArgumentException("Bucket identifier cannot be null or blank");
+        }
+
+        if (StringUtils.isBlank(flowIdentifier)) {
+            throw new IllegalArgumentException("Flow identifier cannot be null or blank");
+        }
+
+        if (version == null) {
+            throw new IllegalArgumentException("Version cannot be null or blank");
+        }
+
+        readLock.lock();
+        try {
+            final BucketEntity existingBucket = metadataService.getBucketById(bucketIdentifier);
+            if (existingBucket == null) {
+                LOGGER.warn("The specified bucket id [{}] does not exist.", bucketIdentifier);
+                throw new ResourceNotFoundException("The specified bucket ID does not exist in this registry.");
+            }
+
+            // we need to populate the version count here so we have to do this retrieval instead of snapshotEntity.getFlow()
+            final FlowEntity flowEntityWithCount = metadataService.getFlowByIdWithSnapshotCounts(flowIdentifier);
+            if (flowEntityWithCount == null) {
+                LOGGER.warn("The specified flow id [{}] does not exist.", flowIdentifier);
+                throw new ResourceNotFoundException("The specified flow ID does not exist in this bucket.");
+            }
+
+            if (!existingBucket.getId().equals(flowEntityWithCount.getBucketId())) {
+                throw new IllegalStateException("The requested flow is not located in the given bucket");
+            }
+
+            return getVersionedFlowSnapshot(existingBucket, flowEntityWithCount, version);
+        } finally {
+            readLock.unlock();
+        }
+    }
+
+    private VersionedFlowSnapshot getVersionedFlowSnapshot(final BucketEntity bucketEntity, final FlowEntity flowEntity, final Integer version) {
+        // ensure the snapshot exists
+        final FlowSnapshotEntity snapshotEntity = metadataService.getFlowSnapshot(flowEntity.getId(), version);
+        if (snapshotEntity == null) {
+            LOGGER.warn("The specified flow snapshot id [{}] does not exist for version [{}].", flowEntity.getId(), version);
+            throw new ResourceNotFoundException("The specified versioned flow snapshot does not exist for this flow.");
+        }
+
+        // get the serialized bytes of the snapshot
+        final byte[] serializedSnapshot = flowPersistenceProvider.getFlowContent(bucketEntity.getId(), flowEntity.getId(), version);
+
+        if (serializedSnapshot == null || serializedSnapshot.length == 0) {
+            throw new IllegalStateException("No serialized content found for snapshot with flow identifier "
+                    + flowEntity.getId() + " and version " + version);
+        }
+
+        // deserialize the contents
+        final InputStream input = new ByteArrayInputStream(serializedSnapshot);
+        final VersionedProcessGroup flowContents = processGroupSerializer.deserialize(input);
+
+        // map entities to data model
+        final Bucket bucket = DataModelMapper.map(bucketEntity);
+        final VersionedFlow versionedFlow = DataModelMapper.map(bucketEntity, flowEntity);
+        final VersionedFlowSnapshotMetadata snapshotMetadata = DataModelMapper.map(bucketEntity, snapshotEntity);
+
+        // create the snapshot to return
+        final VersionedFlowSnapshot snapshot = new VersionedFlowSnapshot();
+        snapshot.setFlowContents(flowContents);
+        snapshot.setSnapshotMetadata(snapshotMetadata);
+        snapshot.setFlow(versionedFlow);
+        snapshot.setBucket(bucket);
+        return snapshot;
+    }
+
+    /**
+     * Returns all versions of a flow, sorted newest to oldest.
+     *
+     * @param bucketIdentifier the id of the bucket to search for the flowIdentifier
+     * @param flowIdentifier the id of the flow to retrieve from the specified bucket
+     * @return all versions of the specified flow, sorted newest to oldest
+     */
+    public SortedSet<VersionedFlowSnapshotMetadata> getFlowSnapshots(final String bucketIdentifier, final String flowIdentifier) {
+        if (StringUtils.isBlank(bucketIdentifier)) {
+            throw new IllegalArgumentException("Bucket identifier cannot be null or blank");
+        }
+
+        if (StringUtils.isBlank(flowIdentifier)) {
+            throw new IllegalArgumentException("Flow identifier cannot be null or blank");
+        }
+
+        readLock.lock();
+        try {
+            // ensure the bucket exists
+            final BucketEntity existingBucket = metadataService.getBucketById(bucketIdentifier);
+            if (existingBucket == null) {
+                LOGGER.warn("The specified bucket id [{}] does not exist.", bucketIdentifier);
+                throw new ResourceNotFoundException("The specified bucket ID does not exist in this registry.");
+            }
+
+            // ensure the flow exists
+            final FlowEntity existingFlow = metadataService.getFlowById(flowIdentifier);
+            if (existingFlow == null) {
+                LOGGER.warn("The specified flow id [{}] does not exist.", flowIdentifier);
+                throw new ResourceNotFoundException("The specified flow ID does not exist in this bucket.");
+            }
+
+            if (!existingBucket.getId().equals(existingFlow.getBucketId())) {
+                throw new IllegalStateException("The requested flow is not located in the given bucket");
+            }
+
+            // convert the set of FlowSnapshotEntity to set of VersionedFlowSnapshotMetadata, ordered by version descending
+            final SortedSet<VersionedFlowSnapshotMetadata> sortedSnapshots = new TreeSet<>(Collections.reverseOrder());
+            final List<FlowSnapshotEntity> existingFlowSnapshots = metadataService.getSnapshots(existingFlow.getId());
+            if (existingFlowSnapshots != null) {
+                existingFlowSnapshots.stream().forEach(s -> sortedSnapshots.add(DataModelMapper.map(existingBucket, s)));
+            }
+
+            return sortedSnapshots;
+
+        } finally {
+            readLock.unlock();
+        }
+    }
+
+    public VersionedFlowSnapshotMetadata getLatestFlowSnapshotMetadata(final String bucketIdentifier, final String flowIdentifier) {
+        if (StringUtils.isBlank(bucketIdentifier)) {
+            throw new IllegalArgumentException("Bucket identifier cannot be null or blank");
+        }
+
+        if (StringUtils.isBlank(flowIdentifier)) {
+            throw new IllegalArgumentException("Flow identifier cannot be null or blank");
+        }
+
+        readLock.lock();
+        try {
+            // ensure the bucket exists
+            final BucketEntity existingBucket = metadataService.getBucketById(bucketIdentifier);
+            if (existingBucket == null) {
+                LOGGER.warn("The specified bucket id [{}] does not exist.", bucketIdentifier);
+                throw new ResourceNotFoundException("The specified bucket ID does not exist in this registry.");
+            }
+
+            // ensure the flow exists
+            final FlowEntity existingFlow = metadataService.getFlowById(flowIdentifier);
+            if (existingFlow == null) {
+                LOGGER.warn("The specified flow id [{}] does not exist.", flowIdentifier);
+                throw new ResourceNotFoundException("The specified flow ID does not exist in this bucket.");
+            }
+
+            if (!existingBucket.getId().equals(existingFlow.getBucketId())) {
+                throw new IllegalStateException("The requested flow is not located in the given bucket");
+            }
+
+            // get latest snapshot for the flow
+            final FlowSnapshotEntity latestSnapshot = metadataService.getLatestSnapshot(existingFlow.getId());
+            if (latestSnapshot == null) {
+                throw new ResourceNotFoundException("The specified flow ID has no versions");
+            }
+
+            return DataModelMapper.map(existingBucket, latestSnapshot);
+        } finally {
+            readLock.unlock();
+        }
+    }
+
+    public VersionedFlowSnapshotMetadata getLatestFlowSnapshotMetadata(final String flowIdentifier) {
+        if (StringUtils.isBlank(flowIdentifier)) {
+            throw new IllegalArgumentException("Flow identifier cannot be null or blank");
+        }
+
+        readLock.lock();
+        try {
+            // ensure the flow exists
+            final FlowEntity existingFlow = metadataService.getFlowById(flowIdentifier);
+            if (existingFlow == null) {
+                LOGGER.warn("The specified flow id [{}] does not exist.", flowIdentifier);
+                throw new ResourceNotFoundException("The specified flow ID does not exist in this bucket.");
+            }
+
+            // ensure the bucket exists
+            final BucketEntity existingBucket = metadataService.getBucketById(existingFlow.getBucketId());
+            if (existingBucket == null) {
+                LOGGER.warn("The specified bucket id [{}] does not exist.", existingFlow.getBucketId());
+                throw new ResourceNotFoundException("The specified bucket ID does not exist in this registry.");
+            }
+
+            // get latest snapshot for the flow
+            final FlowSnapshotEntity latestSnapshot = metadataService.getLatestSnapshot(existingFlow.getId());
+            if (latestSnapshot == null) {
+                throw new ResourceNotFoundException("The specified flow ID has no versions");
+            }
+
+            return DataModelMapper.map(existingBucket, latestSnapshot);
+        } finally {
+            readLock.unlock();
+        }
+    }
+
+    public VersionedFlowSnapshotMetadata deleteFlowSnapshot(final String bucketIdentifier, final String flowIdentifier, final Integer version) {
+        if (StringUtils.isBlank(bucketIdentifier)) {
+            throw new IllegalArgumentException("Bucket identifier cannot be null or blank");
+        }
+
+        if (StringUtils.isBlank(flowIdentifier)) {
+            throw new IllegalArgumentException("Flow identifier cannot be null or blank");
+        }
+
+        if (version == null) {
+            throw new IllegalArgumentException("Version cannot be null or blank");
+        }
+
+        writeLock.lock();
+        try {
+            // ensure the bucket exists
+            final BucketEntity existingBucket = metadataService.getBucketById(bucketIdentifier);
+            if (existingBucket == null) {
+                LOGGER.warn("The specified bucket id [{}] does not exist.", bucketIdentifier);
+                throw new ResourceNotFoundException("The specified bucket ID does not exist in this registry.");
+            }
+
+            // ensure the flow exists
+            final FlowEntity existingFlow = metadataService.getFlowById(flowIdentifier);
+            if (existingFlow == null) {
+                LOGGER.warn("The specified flow id [{}] does not exist.", flowIdentifier);
+                throw new ResourceNotFoundException("The specified flow ID does not exist in this bucket.");
+            }
+
+            if (!existingBucket.getId().equals(existingFlow.getBucketId())) {
+                throw new IllegalStateException("The requested flow is not located in the given bucket");
+            }
+
+            // ensure the snapshot exists
+            final FlowSnapshotEntity snapshotEntity = metadataService.getFlowSnapshot(flowIdentifier, version);
+            if (snapshotEntity == null) {
+                throw new ResourceNotFoundException("Versioned flow snapshot does not exist for flow "
+                        + flowIdentifier + " and version " + version);
+            }
+
+            // delete the content of the snapshot
+            flowPersistenceProvider.deleteFlowContent(bucketIdentifier, flowIdentifier, version);
+
+            // delete the snapshot itself
+            metadataService.deleteFlowSnapshot(snapshotEntity);
+            return DataModelMapper.map(existingBucket, snapshotEntity);
+        } finally {
+            writeLock.unlock();
+        }
+    }
+
+    /**
+     * Returns the differences between two specified versions of a flow.
+     *
+     * @param bucketIdentifier the id of the bucket the flow exists in
+     * @param flowIdentifier the flow to be examined
+     * @param versionA the first version of the comparison
+     * @param versionB the second version of the comparison
+     * @return The differences between two specified versions, grouped by component.
+     */
+    public VersionedFlowDifference getFlowDiff(final String bucketIdentifier, final String flowIdentifier,
+                                               final Integer versionA, final Integer versionB) {
+        if (StringUtils.isBlank(bucketIdentifier)) {
+            throw new IllegalArgumentException("Bucket identifier cannot be null or blank");
+        }
+
+        if (StringUtils.isBlank(flowIdentifier)) {
+            throw new IllegalArgumentException("Flow identifier cannot be null or blank");
+        }
+
+        if (versionA == null || versionB == null) {
+            throw new IllegalArgumentException("Version cannot be null or blank");
+        }
+        // older version is always the lower, regardless of the order supplied
+        final Integer older = Math.min(versionA, versionB);
+        final Integer newer = Math.max(versionA, versionB);
+
+        readLock.lock();
+        try {
+            // Get the content for both versions of the flow
+            final byte[] serializedSnapshotA = flowPersistenceProvider.getFlowContent(bucketIdentifier, flowIdentifier, older);
+            if (serializedSnapshotA == null || serializedSnapshotA.length == 0) {
+                throw new IllegalStateException("No serialized content found for snapshot with flow identifier "
+                        + flowIdentifier + " and version " + older);
+            }
+
+            final byte[] serializedSnapshotB = flowPersistenceProvider.getFlowContent(bucketIdentifier, flowIdentifier, newer);
+            if (serializedSnapshotB == null || serializedSnapshotB.length == 0) {
+                throw new IllegalStateException("No serialized content found for snapshot with flow identifier "
+                        + flowIdentifier + " and version " + newer);
+            }
+
+            // deserialize the contents
+            final InputStream inputA = new ByteArrayInputStream(serializedSnapshotA);
+            final VersionedProcessGroup flowContentsA = processGroupSerializer.deserialize(inputA);
+            final InputStream inputB = new ByteArrayInputStream(serializedSnapshotB);
+            final VersionedProcessGroup flowContentsB = processGroupSerializer.deserialize(inputB);
+
+            final ComparableDataFlow comparableFlowA = new StandardComparableDataFlow(String.format("Version %d", older), flowContentsA);
+            final ComparableDataFlow comparableFlowB = new StandardComparableDataFlow(String.format("Version %d", newer), flowContentsB);
+
+            // Compare the two versions of the flow
+            final FlowComparator flowComparator = new StandardFlowComparator(comparableFlowA, comparableFlowB,
+                    null, new ConciseEvolvingDifferenceDescriptor());
+            final FlowComparison flowComparison = flowComparator.compare();
+
+            VersionedFlowDifference result = new VersionedFlowDifference();
+            result.setBucketId(bucketIdentifier);
+            result.setFlowId(flowIdentifier);
+            result.setVersionA(older);
+            result.setVersionB(newer);
+
+            Set<ComponentDifferenceGroup> differenceGroups = getStringComponentDifferenceGroupMap(flowComparison.getDifferences());
+            result.setComponentDifferenceGroups(differenceGroups);
+
+            return result;
+        } finally {
+            readLock.unlock();
+        }
+    }
+
+    /**
+     * Group the differences in the comparison by component
+     * @param flowDifferences The differences to group together by component
+     * @return A set of componentDifferenceGroups where each entry contains a set of differences specific to that group
+     */
+    private Set<ComponentDifferenceGroup> getStringComponentDifferenceGroupMap(Set<FlowDifference> flowDifferences) {
+        Map<String, ComponentDifferenceGroup> differenceGroups = new HashMap<>();
+        for (FlowDifference diff : flowDifferences) {
+            ComponentDifferenceGroup group;
+            // A component may only exist on only one version for new/removed components
+            VersionedComponent component = ObjectUtils.firstNonNull(diff.getComponentA(), diff.getComponentB());
+            if(differenceGroups.containsKey(component.getIdentifier())){
+                group = differenceGroups.get(component.getIdentifier());
+            }else{
+                group = DataModelMapper.map(component);
+                differenceGroups.put(component.getIdentifier(), group);
+            }
+            group.getDifferences().add(DataModelMapper.map(diff));
+        }
+        return differenceGroups.values().stream().collect(Collectors.toSet());
+    }
+
+    // ---------------------- Field methods ---------------------------------------------
+
+    public Set<String> getBucketFields() {
+        return metadataService.getBucketFields();
+    }
+
+    public Set<String> getBucketItemFields() {
+        return metadataService.getBucketItemFields();
+    }
+
+    public Set<String> getFlowFields() {
+        return metadataService.getFlowFields();
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/6f26290d/nifi-registry-core/nifi-registry-framework/src/main/resources/META-INF/services/org.apache.nifi.registry.flow.FlowPersistenceProvider
----------------------------------------------------------------------
diff --git a/nifi-registry-core/nifi-registry-framework/src/main/resources/META-INF/services/org.apache.nifi.registry.flow.FlowPersistenceProvider b/nifi-registry-core/nifi-registry-framework/src/main/resources/META-INF/services/org.apache.nifi.registry.flow.FlowPersistenceProvider
new file mode 100644
index 0000000..e456fa2
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-framework/src/main/resources/META-INF/services/org.apache.nifi.registry.flow.FlowPersistenceProvider
@@ -0,0 +1,16 @@
+# 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.
+org.apache.nifi.registry.provider.flow.FileSystemFlowPersistenceProvider
+org.apache.nifi.registry.provider.flow.git.GitFlowPersistenceProvider
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/6f26290d/nifi-registry-core/nifi-registry-framework/src/main/resources/META-INF/services/org.apache.nifi.registry.hook.EventHookProvider
----------------------------------------------------------------------
diff --git a/nifi-registry-core/nifi-registry-framework/src/main/resources/META-INF/services/org.apache.nifi.registry.hook.EventHookProvider b/nifi-registry-core/nifi-registry-framework/src/main/resources/META-INF/services/org.apache.nifi.registry.hook.EventHookProvider
new file mode 100644
index 0000000..2676a35
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-framework/src/main/resources/META-INF/services/org.apache.nifi.registry.hook.EventHookProvider
@@ -0,0 +1,16 @@
+# 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.
+org.apache.nifi.registry.provider.hook.ScriptEventHookProvider
+org.apache.nifi.registry.provider.hook.LoggingEventHookProvider
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/6f26290d/nifi-registry-core/nifi-registry-framework/src/main/resources/META-INF/services/org.apache.nifi.registry.security.authentication.IdentityProvider
----------------------------------------------------------------------
diff --git a/nifi-registry-core/nifi-registry-framework/src/main/resources/META-INF/services/org.apache.nifi.registry.security.authentication.IdentityProvider b/nifi-registry-core/nifi-registry-framework/src/main/resources/META-INF/services/org.apache.nifi.registry.security.authentication.IdentityProvider
new file mode 100644
index 0000000..530528f
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-framework/src/main/resources/META-INF/services/org.apache.nifi.registry.security.authentication.IdentityProvider
@@ -0,0 +1,15 @@
+# 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.
+org.apache.nifi.registry.security.ldap.LdapIdentityProvider
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/6f26290d/nifi-registry-core/nifi-registry-framework/src/main/resources/META-INF/services/org.apache.nifi.registry.security.authorization.AccessPolicyProvider
----------------------------------------------------------------------
diff --git a/nifi-registry-core/nifi-registry-framework/src/main/resources/META-INF/services/org.apache.nifi.registry.security.authorization.AccessPolicyProvider b/nifi-registry-core/nifi-registry-framework/src/main/resources/META-INF/services/org.apache.nifi.registry.security.authorization.AccessPolicyProvider
new file mode 100644
index 0000000..f57163f
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-framework/src/main/resources/META-INF/services/org.apache.nifi.registry.security.authorization.AccessPolicyProvider
@@ -0,0 +1,15 @@
+# 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.
+org.apache.nifi.registry.security.authorization.file.FileAccessPolicyProvider
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/6f26290d/nifi-registry-core/nifi-registry-framework/src/main/resources/META-INF/services/org.apache.nifi.registry.security.authorization.Authorizer
----------------------------------------------------------------------
diff --git a/nifi-registry-core/nifi-registry-framework/src/main/resources/META-INF/services/org.apache.nifi.registry.security.authorization.Authorizer b/nifi-registry-core/nifi-registry-framework/src/main/resources/META-INF/services/org.apache.nifi.registry.security.authorization.Authorizer
new file mode 100644
index 0000000..b564fbb
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-framework/src/main/resources/META-INF/services/org.apache.nifi.registry.security.authorization.Authorizer
@@ -0,0 +1,16 @@
+# 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.
+org.apache.nifi.registry.security.authorization.StandardManagedAuthorizer
+org.apache.nifi.registry.security.authorization.file.FileAuthorizer
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/6f26290d/nifi-registry-core/nifi-registry-framework/src/main/resources/META-INF/services/org.apache.nifi.registry.security.authorization.UserGroupProvider
----------------------------------------------------------------------
diff --git a/nifi-registry-core/nifi-registry-framework/src/main/resources/META-INF/services/org.apache.nifi.registry.security.authorization.UserGroupProvider b/nifi-registry-core/nifi-registry-framework/src/main/resources/META-INF/services/org.apache.nifi.registry.security.authorization.UserGroupProvider
new file mode 100644
index 0000000..ee28c07
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-framework/src/main/resources/META-INF/services/org.apache.nifi.registry.security.authorization.UserGroupProvider
@@ -0,0 +1,18 @@
+# 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.
+org.apache.nifi.registry.security.authorization.CompositeUserGroupProvider
+org.apache.nifi.registry.security.authorization.CompositeConfigurableUserGroupProvider
+org.apache.nifi.registry.security.authorization.file.FileUserGroupProvider
+org.apache.nifi.registry.security.ldap.tenants.LdapUserGroupProvider
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/6f26290d/nifi-registry-core/nifi-registry-framework/src/main/resources/db/migration/V2__Initial.sql
----------------------------------------------------------------------
diff --git a/nifi-registry-core/nifi-registry-framework/src/main/resources/db/migration/V2__Initial.sql b/nifi-registry-core/nifi-registry-framework/src/main/resources/db/migration/V2__Initial.sql
new file mode 100644
index 0000000..b992d23
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-framework/src/main/resources/db/migration/V2__Initial.sql
@@ -0,0 +1,60 @@
+-- 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.
+
+-- The NAME column has a max size of 768 because this is the largest size that MySQL allows when using a unique constraint.
+CREATE TABLE BUCKET (
+    ID VARCHAR(50) NOT NULL,
+    NAME VARCHAR(1000) NOT NULL,
+    DESCRIPTION TEXT,
+    CREATED TIMESTAMP NOT NULL,
+    CONSTRAINT PK__BUCKET_ID PRIMARY KEY (ID),
+    CONSTRAINT UNIQUE__BUCKET_NAME UNIQUE (NAME)
+);
+
+CREATE TABLE BUCKET_ITEM (
+    ID VARCHAR(50) NOT NULL,
+    NAME VARCHAR(1000) NOT NULL,
+    DESCRIPTION TEXT,
+    CREATED TIMESTAMP NOT NULL,
+    MODIFIED TIMESTAMP NOT NULL,
+    ITEM_TYPE VARCHAR(50) NOT NULL,
+    BUCKET_ID VARCHAR(50) NOT NULL,
+    CONSTRAINT PK__BUCKET_ITEM_ID PRIMARY KEY (ID),
+    CONSTRAINT FK__BUCKET_ITEM_BUCKET_ID FOREIGN KEY (BUCKET_ID) REFERENCES BUCKET(ID)
+);
+
+CREATE TABLE FLOW (
+    ID VARCHAR(50) NOT NULL,
+    CONSTRAINT PK__FLOW_ID PRIMARY KEY (ID),
+    CONSTRAINT FK__FLOW_BUCKET_ITEM_ID FOREIGN KEY (ID) REFERENCES BUCKET_ITEM(ID)
+);
+
+CREATE TABLE FLOW_SNAPSHOT (
+    FLOW_ID VARCHAR(50) NOT NULL,
+    VERSION INT NOT NULL,
+    CREATED TIMESTAMP NOT NULL,
+    CREATED_BY VARCHAR(4096) NOT NULL,
+    COMMENTS TEXT,
+    CONSTRAINT PK__FLOW_SNAPSHOT_FLOW_ID_AND_VERSION PRIMARY KEY (FLOW_ID, VERSION),
+    CONSTRAINT FK__FLOW_SNAPSHOT_FLOW_ID FOREIGN KEY (FLOW_ID) REFERENCES FLOW(ID)
+);
+
+CREATE TABLE SIGNING_KEY (
+    ID VARCHAR(50) NOT NULL,
+    TENANT_IDENTITY VARCHAR(4096) NOT NULL,
+    KEY_VALUE VARCHAR(50) NOT NULL,
+    CONSTRAINT PK__SIGNING_KEY_ID PRIMARY KEY (ID),
+    CONSTRAINT UNIQUE__SIGNING_KEY_TENANT_IDENTITY UNIQUE (TENANT_IDENTITY)
+);
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/6f26290d/nifi-registry-core/nifi-registry-framework/src/main/resources/db/original/V1.2__IncreaseColumnSizes.sql
----------------------------------------------------------------------
diff --git a/nifi-registry-core/nifi-registry-framework/src/main/resources/db/original/V1.2__IncreaseColumnSizes.sql b/nifi-registry-core/nifi-registry-framework/src/main/resources/db/original/V1.2__IncreaseColumnSizes.sql
new file mode 100644
index 0000000..b2e92d5
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-framework/src/main/resources/db/original/V1.2__IncreaseColumnSizes.sql
@@ -0,0 +1,25 @@
+-- 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.
+
+ALTER TABLE BUCKET ALTER COLUMN NAME VARCHAR2(1000);
+ALTER TABLE BUCKET ALTER COLUMN DESCRIPTION VARCHAR2(65535);
+
+ALTER TABLE BUCKET_ITEM ALTER COLUMN NAME VARCHAR2(1000);
+ALTER TABLE BUCKET_ITEM ALTER COLUMN DESCRIPTION VARCHAR2(65535);
+
+ALTER TABLE FLOW_SNAPSHOT ALTER COLUMN CREATED_BY VARCHAR2(4096);
+ALTER TABLE FLOW_SNAPSHOT ALTER COLUMN COMMENTS VARCHAR2(65535);
+
+ALTER TABLE SIGNING_KEY ALTER COLUMN TENANT_IDENTITY VARCHAR2(4096);
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/6f26290d/nifi-registry-core/nifi-registry-framework/src/main/resources/db/original/V1.3__DropBucketItemNameUniqueness.sql
----------------------------------------------------------------------
diff --git a/nifi-registry-core/nifi-registry-framework/src/main/resources/db/original/V1.3__DropBucketItemNameUniqueness.sql b/nifi-registry-core/nifi-registry-framework/src/main/resources/db/original/V1.3__DropBucketItemNameUniqueness.sql
new file mode 100644
index 0000000..f29b4d0
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-framework/src/main/resources/db/original/V1.3__DropBucketItemNameUniqueness.sql
@@ -0,0 +1,27 @@
+-- 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.
+
+CREATE ALIAS IF NOT EXISTS EXECUTE AS $$ void executeSql(Connection conn, String sql)
+throws SQLException { conn.createStatement().executeUpdate(sql); } $$;
+
+call execute('ALTER TABLE BUCKET_ITEM DROP CONSTRAINT ' ||
+    (
+     SELECT DISTINCT CONSTRAINT_NAME
+     FROM INFORMATION_SCHEMA.CONSTRAINTS
+     WHERE TABLE_NAME = 'BUCKET_ITEM'
+     AND COLUMN_LIST = 'NAME'
+     AND CONSTRAINT_TYPE = 'UNIQUE'
+     )
+);

http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/6f26290d/nifi-registry-core/nifi-registry-framework/src/main/resources/db/original/V1__Initial.sql
----------------------------------------------------------------------
diff --git a/nifi-registry-core/nifi-registry-framework/src/main/resources/db/original/V1__Initial.sql b/nifi-registry-core/nifi-registry-framework/src/main/resources/db/original/V1__Initial.sql
new file mode 100644
index 0000000..a6b4960
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-framework/src/main/resources/db/original/V1__Initial.sql
@@ -0,0 +1,54 @@
+-- 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.
+
+CREATE TABLE BUCKET (
+    ID VARCHAR2(50) NOT NULL PRIMARY KEY,
+    NAME VARCHAR2(200) NOT NULL UNIQUE,
+    DESCRIPTION VARCHAR(4096),
+    CREATED TIMESTAMP NOT NULL
+);
+
+CREATE TABLE BUCKET_ITEM (
+    ID VARCHAR2(50) NOT NULL PRIMARY KEY,
+    NAME VARCHAR2(200) NOT NULL UNIQUE,
+    DESCRIPTION VARCHAR(4096),
+    CREATED TIMESTAMP NOT NULL,
+    MODIFIED TIMESTAMP NOT NULL,
+    ITEM_TYPE VARCHAR(50) NOT NULL,
+    BUCKET_ID VARCHAR2(50) NOT NULL,
+    FOREIGN KEY (BUCKET_ID) REFERENCES BUCKET(ID)
+);
+
+CREATE TABLE FLOW (
+    ID VARCHAR2(50) NOT NULL PRIMARY KEY,
+    FOREIGN KEY (ID) REFERENCES BUCKET_ITEM(ID)
+);
+
+CREATE TABLE FLOW_SNAPSHOT (
+    FLOW_ID VARCHAR2(50) NOT NULL,
+    VERSION INT NOT NULL,
+    CREATED TIMESTAMP NOT NULL,
+    CREATED_BY VARCHAR2(200) NOT NULL,
+    COMMENTS VARCHAR(4096),
+    PRIMARY KEY (FLOW_ID, VERSION),
+    FOREIGN KEY (FLOW_ID) REFERENCES FLOW(ID)
+);
+
+CREATE TABLE SIGNING_KEY (
+    ID VARCHAR2(50) NOT NULL,
+    TENANT_IDENTITY VARCHAR2(50) NOT NULL UNIQUE,
+    KEY_VALUE VARCHAR2(50) NOT NULL,
+    PRIMARY KEY (ID)
+);
\ No newline at end of file


[29/51] [partial] nifi-registry git commit: NIFIREG-201 Refactoring project structure to better isolate extensions

Posted by kd...@apache.org.
http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/6f26290d/nifi-registry-core/nifi-registry-framework/src/test/java/org/apache/nifi/registry/event/TestEventFactory.java
----------------------------------------------------------------------
diff --git a/nifi-registry-core/nifi-registry-framework/src/test/java/org/apache/nifi/registry/event/TestEventFactory.java b/nifi-registry-core/nifi-registry-framework/src/test/java/org/apache/nifi/registry/event/TestEventFactory.java
new file mode 100644
index 0000000..a9ad911
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-framework/src/test/java/org/apache/nifi/registry/event/TestEventFactory.java
@@ -0,0 +1,169 @@
+/*
+ * 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.event;
+
+import org.apache.nifi.registry.bucket.Bucket;
+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.flow.VersionedProcessGroup;
+import org.apache.nifi.registry.hook.Event;
+import org.apache.nifi.registry.hook.EventFieldName;
+import org.apache.nifi.registry.hook.EventType;
+import org.junit.Before;
+import org.junit.Test;
+
+import java.util.UUID;
+
+import static org.junit.Assert.assertEquals;
+
+public class TestEventFactory {
+
+    private Bucket bucket;
+    private VersionedFlow versionedFlow;
+    private VersionedFlowSnapshot versionedFlowSnapshot;
+
+    @Before
+    public void setup() {
+        bucket = new Bucket();
+        bucket.setName("Bucket1");
+        bucket.setIdentifier(UUID.randomUUID().toString());
+        bucket.setCreatedTimestamp(System.currentTimeMillis());
+
+        versionedFlow = new VersionedFlow();
+        versionedFlow.setIdentifier(UUID.randomUUID().toString());
+        versionedFlow.setName("Flow 1");
+        versionedFlow.setBucketIdentifier(bucket.getIdentifier());
+        versionedFlow.setBucketName(bucket.getName());
+
+        VersionedFlowSnapshotMetadata metadata = new VersionedFlowSnapshotMetadata();
+        metadata.setAuthor("user1");
+        metadata.setComments("This is flow 1");
+        metadata.setVersion(1);
+        metadata.setBucketIdentifier(bucket.getIdentifier());
+        metadata.setFlowIdentifier(versionedFlow.getIdentifier());
+
+        versionedFlowSnapshot = new VersionedFlowSnapshot();
+        versionedFlowSnapshot.setSnapshotMetadata(metadata);
+        versionedFlowSnapshot.setFlowContents(new VersionedProcessGroup());
+    }
+
+    @Test
+    public void testBucketCreatedEvent() {
+        final Event event = EventFactory.bucketCreated(bucket);
+        event.validate();
+
+        assertEquals(EventType.CREATE_BUCKET, event.getEventType());
+        assertEquals(2, event.getFields().size());
+
+        assertEquals(bucket.getIdentifier(), event.getField(EventFieldName.BUCKET_ID).getValue());
+        assertEquals("unknown", event.getField(EventFieldName.USER).getValue());
+    }
+
+    @Test
+    public void testBucketUpdatedEvent() {
+        final Event event = EventFactory.bucketUpdated(bucket);
+        event.validate();
+
+        assertEquals(EventType.UPDATE_BUCKET, event.getEventType());
+        assertEquals(2, event.getFields().size());
+
+        assertEquals(bucket.getIdentifier(), event.getField(EventFieldName.BUCKET_ID).getValue());
+        assertEquals("unknown", event.getField(EventFieldName.USER).getValue());
+    }
+
+    @Test
+    public void testBucketDeletedEvent() {
+        final Event event = EventFactory.bucketDeleted(bucket);
+        event.validate();
+
+        assertEquals(EventType.DELETE_BUCKET, event.getEventType());
+        assertEquals(2, event.getFields().size());
+
+        assertEquals(bucket.getIdentifier(), event.getField(EventFieldName.BUCKET_ID).getValue());
+        assertEquals("unknown", event.getField(EventFieldName.USER).getValue());
+    }
+
+    @Test
+    public void testFlowCreated() {
+        final Event event = EventFactory.flowCreated(versionedFlow);
+        event.validate();
+
+        assertEquals(EventType.CREATE_FLOW, event.getEventType());
+        assertEquals(3, event.getFields().size());
+
+        assertEquals(bucket.getIdentifier(), event.getField(EventFieldName.BUCKET_ID).getValue());
+        assertEquals(versionedFlow.getIdentifier(), event.getField(EventFieldName.FLOW_ID).getValue());
+        assertEquals("unknown", event.getField(EventFieldName.USER).getValue());
+    }
+
+    @Test
+    public void testFlowUpdated() {
+        final Event event = EventFactory.flowUpdated(versionedFlow);
+        event.validate();
+
+        assertEquals(EventType.UPDATE_FLOW, event.getEventType());
+        assertEquals(3, event.getFields().size());
+
+        assertEquals(bucket.getIdentifier(), event.getField(EventFieldName.BUCKET_ID).getValue());
+        assertEquals(versionedFlow.getIdentifier(), event.getField(EventFieldName.FLOW_ID).getValue());
+        assertEquals("unknown", event.getField(EventFieldName.USER).getValue());
+    }
+
+    @Test
+    public void testFlowDeleted() {
+        final Event event = EventFactory.flowDeleted(versionedFlow);
+        event.validate();
+
+        assertEquals(EventType.DELETE_FLOW, event.getEventType());
+        assertEquals(3, event.getFields().size());
+
+        assertEquals(bucket.getIdentifier(), event.getField(EventFieldName.BUCKET_ID).getValue());
+        assertEquals(versionedFlow.getIdentifier(), event.getField(EventFieldName.FLOW_ID).getValue());
+        assertEquals("unknown", event.getField(EventFieldName.USER).getValue());
+    }
+
+    @Test
+    public void testFlowVersionedCreated() {
+        final Event event = EventFactory.flowVersionCreated(versionedFlowSnapshot);
+        event.validate();
+
+        assertEquals(EventType.CREATE_FLOW_VERSION, event.getEventType());
+        assertEquals(5, event.getFields().size());
+
+        assertEquals(bucket.getIdentifier(), event.getField(EventFieldName.BUCKET_ID).getValue());
+        assertEquals(versionedFlow.getIdentifier(), event.getField(EventFieldName.FLOW_ID).getValue());
+
+        assertEquals(String.valueOf(versionedFlowSnapshot.getSnapshotMetadata().getVersion()),
+                event.getField(EventFieldName.VERSION).getValue());
+
+        assertEquals(versionedFlowSnapshot.getSnapshotMetadata().getAuthor(),
+                event.getField(EventFieldName.USER).getValue());
+
+        assertEquals(versionedFlowSnapshot.getSnapshotMetadata().getComments(),
+                event.getField(EventFieldName.COMMENT).getValue());
+    }
+
+    @Test
+    public void testFlowVersionedCreatedWhenCommentsMissing() {
+        versionedFlowSnapshot.getSnapshotMetadata().setComments(null);
+        final Event event = EventFactory.flowVersionCreated(versionedFlowSnapshot);
+        event.validate();
+        assertEquals("", event.getField(EventFieldName.COMMENT).getValue());
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/6f26290d/nifi-registry-core/nifi-registry-framework/src/test/java/org/apache/nifi/registry/event/TestEventService.java
----------------------------------------------------------------------
diff --git a/nifi-registry-core/nifi-registry-framework/src/test/java/org/apache/nifi/registry/event/TestEventService.java b/nifi-registry-core/nifi-registry-framework/src/test/java/org/apache/nifi/registry/event/TestEventService.java
new file mode 100644
index 0000000..0270dd8
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-framework/src/test/java/org/apache/nifi/registry/event/TestEventService.java
@@ -0,0 +1,97 @@
+/*
+ * 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.event;
+
+import org.apache.nifi.registry.bucket.Bucket;
+import org.apache.nifi.registry.hook.Event;
+import org.apache.nifi.registry.hook.EventHookException;
+import org.apache.nifi.registry.hook.EventHookProvider;
+import org.apache.nifi.registry.provider.ProviderConfigurationContext;
+import org.apache.nifi.registry.provider.ProviderCreationException;
+import org.junit.After;
+import org.junit.Assert;
+import org.junit.Before;
+import org.junit.Test;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.UUID;
+
+public class TestEventService {
+
+    private CapturingEventHook eventHook;
+    private EventService eventService;
+
+    @Before
+    public void setup() {
+        eventHook = new CapturingEventHook();
+        eventService = new EventService(Collections.singletonList(eventHook));
+        eventService.postConstruct();
+    }
+
+    @After
+    public void teardown() throws Exception {
+        eventService.destroy();
+    }
+
+    @Test
+    public void testPublishConsume() throws InterruptedException {
+        final Bucket bucket = new Bucket();
+        bucket.setIdentifier(UUID.randomUUID().toString());
+
+        final Event bucketCreatedEvent = EventFactory.bucketCreated(bucket);
+        eventService.publish(bucketCreatedEvent);
+
+        final Event bucketDeletedEvent = EventFactory.bucketDeleted(bucket);
+        eventService.publish(bucketDeletedEvent);
+
+        Thread.sleep(1000);
+
+        final List<Event> events = eventHook.getEvents();
+        Assert.assertEquals(2, events.size());
+
+        final Event firstEvent = events.get(0);
+        Assert.assertEquals(bucketCreatedEvent.getEventType(), firstEvent.getEventType());
+
+        final Event secondEvent = events.get(1);
+        Assert.assertEquals(bucketDeletedEvent.getEventType(), secondEvent.getEventType());
+    }
+
+    /**
+     * Simple implementation of EventHookProvider that captures event for later verification.
+     */
+    private class CapturingEventHook implements EventHookProvider {
+
+        private List<Event> events = new ArrayList<>();
+
+        @Override
+        public void onConfigured(ProviderConfigurationContext configurationContext) throws ProviderCreationException {
+
+        }
+
+        @Override
+        public void handle(Event event) throws EventHookException {
+            events.add(event);
+        }
+
+        public List<Event> getEvents() {
+            return events;
+        }
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/6f26290d/nifi-registry-core/nifi-registry-framework/src/test/java/org/apache/nifi/registry/event/TestStandardEvent.java
----------------------------------------------------------------------
diff --git a/nifi-registry-core/nifi-registry-framework/src/test/java/org/apache/nifi/registry/event/TestStandardEvent.java b/nifi-registry-core/nifi-registry-framework/src/test/java/org/apache/nifi/registry/event/TestStandardEvent.java
new file mode 100644
index 0000000..8e9f73f
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-framework/src/test/java/org/apache/nifi/registry/event/TestStandardEvent.java
@@ -0,0 +1,47 @@
+/*
+ * 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.event;
+
+import org.apache.nifi.registry.hook.Event;
+import org.apache.nifi.registry.hook.EventField;
+import org.apache.nifi.registry.hook.EventFieldName;
+import org.apache.nifi.registry.hook.EventType;
+import org.junit.Assert;
+import org.junit.Test;
+
+public class TestStandardEvent {
+
+    @Test(expected = IllegalStateException.class)
+    public void testInvalidEvent() {
+        final Event event = new StandardEvent.Builder()
+                .eventType(EventType.CREATE_BUCKET)
+                .build();
+
+        event.validate();
+    }
+
+    @Test
+    public void testGetFieldWhenDoesNotExist() {
+        final Event event = new StandardEvent.Builder()
+                .eventType(EventType.CREATE_BUCKET)
+                .build();
+
+        final EventField field = event.getField(EventFieldName.BUCKET_ID);
+        Assert.assertNull(field);
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/6f26290d/nifi-registry-core/nifi-registry-framework/src/test/java/org/apache/nifi/registry/provider/MockFlowPersistenceProvider.java
----------------------------------------------------------------------
diff --git a/nifi-registry-core/nifi-registry-framework/src/test/java/org/apache/nifi/registry/provider/MockFlowPersistenceProvider.java b/nifi-registry-core/nifi-registry-framework/src/test/java/org/apache/nifi/registry/provider/MockFlowPersistenceProvider.java
new file mode 100644
index 0000000..430f3a3
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-framework/src/test/java/org/apache/nifi/registry/provider/MockFlowPersistenceProvider.java
@@ -0,0 +1,57 @@
+/*
+ * 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.provider;
+
+import org.apache.nifi.registry.flow.FlowPersistenceProvider;
+import org.apache.nifi.registry.flow.FlowSnapshotContext;
+import org.apache.nifi.registry.flow.FlowPersistenceException;
+
+import java.util.Map;
+
+public class MockFlowPersistenceProvider implements FlowPersistenceProvider {
+
+    private Map<String,String> properties;
+
+    @Override
+    public void onConfigured(ProviderConfigurationContext configurationContext) throws ProviderCreationException {
+        properties = configurationContext.getProperties();
+    }
+
+    @Override
+    public void saveFlowContent(FlowSnapshotContext context, byte[] content) throws FlowPersistenceException {
+
+    }
+
+    @Override
+    public byte[] getFlowContent(String bucketId, String flowId, int version) throws FlowPersistenceException {
+        return new byte[0];
+    }
+
+    @Override
+    public void deleteAllFlowContent(String bucketId, String flowId) throws FlowPersistenceException {
+
+    }
+
+    @Override
+    public void deleteFlowContent(String bucketId, String flowId, int version) throws FlowPersistenceException {
+
+    }
+
+    public Map<String,String> getProperties() {
+        return properties;
+    }
+}

http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/6f26290d/nifi-registry-core/nifi-registry-framework/src/test/java/org/apache/nifi/registry/provider/TestStandardProviderFactory.java
----------------------------------------------------------------------
diff --git a/nifi-registry-core/nifi-registry-framework/src/test/java/org/apache/nifi/registry/provider/TestStandardProviderFactory.java b/nifi-registry-core/nifi-registry-framework/src/test/java/org/apache/nifi/registry/provider/TestStandardProviderFactory.java
new file mode 100644
index 0000000..30f66ef
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-framework/src/test/java/org/apache/nifi/registry/provider/TestStandardProviderFactory.java
@@ -0,0 +1,90 @@
+/*
+ * 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.provider;
+
+import org.apache.nifi.registry.extension.ExtensionManager;
+import org.apache.nifi.registry.flow.FlowPersistenceProvider;
+import org.apache.nifi.registry.properties.NiFiRegistryProperties;
+import org.junit.Test;
+import org.mockito.Mockito;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.mockito.Mockito.any;
+import static org.mockito.Mockito.when;
+
+public class TestStandardProviderFactory {
+
+    @Test
+    public void testGetProvidersSuccess() {
+        final NiFiRegistryProperties props = new NiFiRegistryProperties();
+        props.setProperty(NiFiRegistryProperties.PROVIDERS_CONFIGURATION_FILE, "src/test/resources/provider/providers-good.xml");
+
+        final ExtensionManager extensionManager = Mockito.mock(ExtensionManager.class);
+        when(extensionManager.getExtensionClassLoader(any(String.class))).thenReturn(this.getClass().getClassLoader());
+
+        final ProviderFactory providerFactory = new StandardProviderFactory(props, extensionManager);
+        providerFactory.initialize();
+
+        final FlowPersistenceProvider flowPersistenceProvider = providerFactory.getFlowPersistenceProvider();
+        assertNotNull(flowPersistenceProvider);
+
+        final MockFlowPersistenceProvider mockFlowProvider = (MockFlowPersistenceProvider) flowPersistenceProvider;
+        assertNotNull(mockFlowProvider.getProperties());
+        assertEquals("flow foo", mockFlowProvider.getProperties().get("Flow Property 1"));
+        assertEquals("flow bar", mockFlowProvider.getProperties().get("Flow Property 2"));
+    }
+
+    @Test(expected = ProviderFactoryException.class)
+    public void testGetFlowProviderBeforeInitializingShouldThrowException() {
+        final NiFiRegistryProperties props = new NiFiRegistryProperties();
+        props.setProperty(NiFiRegistryProperties.PROVIDERS_CONFIGURATION_FILE, "src/test/resources/provider/providers-good.xml");
+
+        final ExtensionManager extensionManager = Mockito.mock(ExtensionManager.class);
+        when(extensionManager.getExtensionClassLoader(any(String.class))).thenReturn(this.getClass().getClassLoader());
+
+        final ProviderFactory providerFactory = new StandardProviderFactory(props, extensionManager);
+        providerFactory.getFlowPersistenceProvider();
+    }
+
+    @Test(expected = ProviderFactoryException.class)
+    public void testProvidersConfigDoesNotExist() {
+        final NiFiRegistryProperties props = new NiFiRegistryProperties();
+        props.setProperty(NiFiRegistryProperties.PROVIDERS_CONFIGURATION_FILE, "src/test/resources/provider/providers-does-not-exist.xml");
+
+        final ExtensionManager extensionManager = Mockito.mock(ExtensionManager.class);
+        when(extensionManager.getExtensionClassLoader(any(String.class))).thenReturn(this.getClass().getClassLoader());
+
+        final ProviderFactory providerFactory = new StandardProviderFactory(props, extensionManager);
+        providerFactory.initialize();
+    }
+
+    @Test(expected = ProviderFactoryException.class)
+    public void testFlowProviderClassNotFound() {
+        final NiFiRegistryProperties props = new NiFiRegistryProperties();
+        props.setProperty(NiFiRegistryProperties.PROVIDERS_CONFIGURATION_FILE, "src/test/resources/provider/providers-class-not-found.xml");
+
+        final ExtensionManager extensionManager = Mockito.mock(ExtensionManager.class);
+        when(extensionManager.getExtensionClassLoader(any(String.class))).thenReturn(this.getClass().getClassLoader());
+
+        final ProviderFactory providerFactory = new StandardProviderFactory(props, extensionManager);
+        providerFactory.initialize();
+
+        providerFactory.getFlowPersistenceProvider();
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/6f26290d/nifi-registry-core/nifi-registry-framework/src/test/java/org/apache/nifi/registry/provider/flow/TestFileSystemFlowPersistenceProvider.java
----------------------------------------------------------------------
diff --git a/nifi-registry-core/nifi-registry-framework/src/test/java/org/apache/nifi/registry/provider/flow/TestFileSystemFlowPersistenceProvider.java b/nifi-registry-core/nifi-registry-framework/src/test/java/org/apache/nifi/registry/provider/flow/TestFileSystemFlowPersistenceProvider.java
new file mode 100644
index 0000000..14a4861
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-framework/src/test/java/org/apache/nifi/registry/provider/flow/TestFileSystemFlowPersistenceProvider.java
@@ -0,0 +1,204 @@
+/*
+ * 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.provider.flow;
+
+import org.apache.commons.io.IOUtils;
+import org.apache.nifi.registry.flow.FlowPersistenceProvider;
+import org.apache.nifi.registry.flow.FlowSnapshotContext;
+import org.apache.nifi.registry.provider.ProviderConfigurationContext;
+import org.junit.Assert;
+import org.junit.Before;
+import org.junit.Test;
+import org.mockito.Mockito;
+
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.nio.charset.StandardCharsets;
+import java.util.HashMap;
+import java.util.Map;
+
+import static org.mockito.Mockito.when;
+
+public class TestFileSystemFlowPersistenceProvider {
+
+    static final String FLOW_STORAGE_DIR = "target/flow_storage";
+
+    static final ProviderConfigurationContext CONFIGURATION_CONTEXT = new ProviderConfigurationContext() {
+        @Override
+        public Map<String, String> getProperties() {
+            final Map<String,String> props = new HashMap<>();
+            props.put(FileSystemFlowPersistenceProvider.FLOW_STORAGE_DIR_PROP, FLOW_STORAGE_DIR);
+            return props;
+        }
+    };
+
+    private File flowStorageDir;
+    private FileSystemFlowPersistenceProvider fileSystemFlowProvider;
+
+    @Before
+    public void setup() throws IOException {
+        flowStorageDir = new File(FLOW_STORAGE_DIR);
+        if (flowStorageDir.exists()) {
+            org.apache.commons.io.FileUtils.cleanDirectory(flowStorageDir);
+            flowStorageDir.delete();
+        }
+
+        Assert.assertFalse(flowStorageDir.exists());
+
+        fileSystemFlowProvider = new FileSystemFlowPersistenceProvider();
+        fileSystemFlowProvider.onConfigured(CONFIGURATION_CONTEXT);
+        Assert.assertTrue(flowStorageDir.exists());
+    }
+
+    @Test
+    public void testSaveSuccessfully() throws IOException {
+        createAndSaveSnapshot(fileSystemFlowProvider,"bucket1", "flow1", 1, "flow1v1");
+        verifySnapshot(flowStorageDir, "bucket1", "flow1", 1, "flow1v1");
+
+        createAndSaveSnapshot(fileSystemFlowProvider,"bucket1", "flow1", 2, "flow1v2");
+        verifySnapshot(flowStorageDir, "bucket1", "flow1", 2, "flow1v2");
+
+        createAndSaveSnapshot(fileSystemFlowProvider,"bucket1", "flow2", 1, "flow2v1");
+        verifySnapshot(flowStorageDir, "bucket1", "flow2", 1, "flow2v1");
+
+        createAndSaveSnapshot(fileSystemFlowProvider,"bucket2", "flow3", 1, "flow3v1");
+        verifySnapshot(flowStorageDir, "bucket2", "flow3", 1, "flow3v1");
+    }
+
+    @Test
+    public void testSaveWithExistingVersion() throws IOException {
+        final FlowSnapshotContext context = Mockito.mock(FlowSnapshotContext.class);
+        when(context.getBucketId()).thenReturn("bucket1");
+        when(context.getFlowId()).thenReturn("flow1");
+        when(context.getVersion()).thenReturn(1);
+
+        final byte[] content = "flow1v1".getBytes(StandardCharsets.UTF_8);
+        fileSystemFlowProvider.saveFlowContent(context, content);
+
+        // save new content for an existing version
+        final byte[] content2 = "XXX".getBytes(StandardCharsets.UTF_8);
+        try {
+            fileSystemFlowProvider.saveFlowContent(context, content2);
+            Assert.fail("Should have thrown exception");
+        } catch (Exception e) {
+
+        }
+
+        // verify the new content wasn't written
+        final File flowSnapshotFile = new File(flowStorageDir, "bucket1/flow1/1/1" + FileSystemFlowPersistenceProvider.SNAPSHOT_EXTENSION);
+        try (InputStream in = new FileInputStream(flowSnapshotFile)) {
+            Assert.assertEquals("flow1v1", IOUtils.toString(in, StandardCharsets.UTF_8));
+        }
+    }
+
+    @Test
+    public void testSaveAndGet() throws IOException {
+        createAndSaveSnapshot(fileSystemFlowProvider,"bucket1", "flow1", 1, "flow1v1");
+        createAndSaveSnapshot(fileSystemFlowProvider,"bucket1", "flow1", 2, "flow1v2");
+
+        final byte[] flow1v1 = fileSystemFlowProvider.getFlowContent("bucket1", "flow1", 1);
+        Assert.assertEquals("flow1v1", new String(flow1v1, StandardCharsets.UTF_8));
+
+        final byte[] flow1v2 = fileSystemFlowProvider.getFlowContent("bucket1", "flow1", 2);
+        Assert.assertEquals("flow1v2", new String(flow1v2, StandardCharsets.UTF_8));
+    }
+
+    @Test
+    public void testGetWhenDoesNotExist() {
+        final byte[] flow1v1 = fileSystemFlowProvider.getFlowContent("bucket1", "flow1", 1);
+        Assert.assertNull(flow1v1);
+    }
+
+    @Test
+    public void testDeleteSnapshots() throws IOException {
+        final String bucketId = "bucket1";
+        final String flowId = "flow1";
+
+        createAndSaveSnapshot(fileSystemFlowProvider, bucketId, flowId, 1, "flow1v1");
+        createAndSaveSnapshot(fileSystemFlowProvider, bucketId, flowId, 2, "flow1v2");
+
+        Assert.assertNotNull(fileSystemFlowProvider.getFlowContent(bucketId, flowId, 1));
+        Assert.assertNotNull(fileSystemFlowProvider.getFlowContent(bucketId, flowId, 2));
+
+        fileSystemFlowProvider.deleteAllFlowContent(bucketId, flowId);
+
+        Assert.assertNull(fileSystemFlowProvider.getFlowContent(bucketId, flowId, 1));
+        Assert.assertNull(fileSystemFlowProvider.getFlowContent(bucketId, flowId, 2));
+
+        // delete a flow that doesn't exist
+        fileSystemFlowProvider.deleteAllFlowContent(bucketId, "some-other-flow");
+
+        // delete a bucket that doesn't exist
+        fileSystemFlowProvider.deleteAllFlowContent("some-other-bucket", flowId);
+    }
+
+    @Test
+    public void testDeleteSnapshot() throws IOException {
+        final String bucketId = "bucket1";
+        final String flowId = "flow1";
+
+        createAndSaveSnapshot(fileSystemFlowProvider, bucketId, flowId, 1, "flow1v1");
+        createAndSaveSnapshot(fileSystemFlowProvider, bucketId, flowId, 2, "flow1v2");
+
+        Assert.assertNotNull(fileSystemFlowProvider.getFlowContent(bucketId, flowId, 1));
+        Assert.assertNotNull(fileSystemFlowProvider.getFlowContent(bucketId, flowId, 2));
+
+        fileSystemFlowProvider.deleteFlowContent(bucketId, flowId, 1);
+
+        Assert.assertNull(fileSystemFlowProvider.getFlowContent(bucketId, flowId, 1));
+        Assert.assertNotNull(fileSystemFlowProvider.getFlowContent(bucketId, flowId, 2));
+
+        fileSystemFlowProvider.deleteFlowContent(bucketId, flowId, 2);
+
+        Assert.assertNull(fileSystemFlowProvider.getFlowContent(bucketId, flowId, 1));
+        Assert.assertNull(fileSystemFlowProvider.getFlowContent(bucketId, flowId, 2));
+
+        // delete a version that doesn't exist
+        fileSystemFlowProvider.deleteFlowContent(bucketId, flowId, 3);
+
+        // delete a flow that doesn't exist
+        fileSystemFlowProvider.deleteFlowContent(bucketId, "some-other-flow", 1);
+
+        // delete a bucket that doesn't exist
+        fileSystemFlowProvider.deleteFlowContent("some-other-bucket", flowId, 1);
+    }
+
+    private void createAndSaveSnapshot(final FlowPersistenceProvider flowPersistenceProvider, final String bucketId, final String flowId, final int version,
+                                       final String contentString) throws IOException {
+        final FlowSnapshotContext context = Mockito.mock(FlowSnapshotContext.class);
+        when(context.getBucketId()).thenReturn(bucketId);
+        when(context.getFlowId()).thenReturn(flowId);
+        when(context.getVersion()).thenReturn(version);
+
+        final byte[] content = contentString.getBytes(StandardCharsets.UTF_8);
+        flowPersistenceProvider.saveFlowContent(context, content);
+    }
+
+    private void verifySnapshot(final File flowStorageDir, final String bucketId, final String flowId, final int version,
+                                final String contentString) throws IOException {
+        // verify the correct snapshot file was created
+        final File flowSnapshotFile = new File(flowStorageDir,
+                bucketId + "/" + flowId + "/" + version + "/" + version + FileSystemFlowPersistenceProvider.SNAPSHOT_EXTENSION);
+        Assert.assertTrue(flowSnapshotFile.exists());
+
+        try (InputStream in = new FileInputStream(flowSnapshotFile)) {
+            Assert.assertEquals(contentString, IOUtils.toString(in, StandardCharsets.UTF_8));
+        }
+    }
+}

http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/6f26290d/nifi-registry-core/nifi-registry-framework/src/test/java/org/apache/nifi/registry/provider/flow/TestStandardFlowSnapshotContext.java
----------------------------------------------------------------------
diff --git a/nifi-registry-core/nifi-registry-framework/src/test/java/org/apache/nifi/registry/provider/flow/TestStandardFlowSnapshotContext.java b/nifi-registry-core/nifi-registry-framework/src/test/java/org/apache/nifi/registry/provider/flow/TestStandardFlowSnapshotContext.java
new file mode 100644
index 0000000..bff2ef9
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-framework/src/test/java/org/apache/nifi/registry/provider/flow/TestStandardFlowSnapshotContext.java
@@ -0,0 +1,57 @@
+/*
+ * 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.provider.flow;
+
+import org.apache.nifi.registry.flow.FlowSnapshotContext;
+import org.junit.Assert;
+import org.junit.Test;
+
+public class TestStandardFlowSnapshotContext {
+
+    @Test
+    public void testBuilder() {
+        final String bucketId = "1234-1234-1234-1234";
+        final String bucketName = "Some Bucket";
+        final String flowId = "2345-2345-2345-2345";
+        final String flowName = "Some Flow";
+        final int version = 2;
+        final String comments = "Some Comments";
+        final String author = "anonymous";
+        final long timestamp = System.currentTimeMillis();
+
+        final FlowSnapshotContext context = new StandardFlowSnapshotContext.Builder()
+                .bucketId(bucketId)
+                .bucketName(bucketName)
+                .flowId(flowId)
+                .flowName(flowName)
+                .version(version)
+                .comments(comments)
+                .author(author)
+                .snapshotTimestamp(timestamp)
+                .build();
+
+        Assert.assertEquals(bucketId, context.getBucketId());
+        Assert.assertEquals(bucketName, context.getBucketName());
+        Assert.assertEquals(flowId, context.getFlowId());
+        Assert.assertEquals(flowName, context.getFlowName());
+        Assert.assertEquals(version, context.getVersion());
+        Assert.assertEquals(comments, context.getComments());
+        Assert.assertEquals(author, context.getAuthor());
+        Assert.assertEquals(timestamp, context.getSnapshotTimestamp());
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/6f26290d/nifi-registry-core/nifi-registry-framework/src/test/java/org/apache/nifi/registry/provider/flow/git/TestGitFlowPersistenceProvider.java
----------------------------------------------------------------------
diff --git a/nifi-registry-core/nifi-registry-framework/src/test/java/org/apache/nifi/registry/provider/flow/git/TestGitFlowPersistenceProvider.java b/nifi-registry-core/nifi-registry-framework/src/test/java/org/apache/nifi/registry/provider/flow/git/TestGitFlowPersistenceProvider.java
new file mode 100644
index 0000000..45351ab
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-framework/src/test/java/org/apache/nifi/registry/provider/flow/git/TestGitFlowPersistenceProvider.java
@@ -0,0 +1,290 @@
+/*
+ * 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.provider.flow.git;
+
+import org.apache.nifi.registry.flow.FlowPersistenceException;
+import org.apache.nifi.registry.provider.ProviderConfigurationContext;
+import org.apache.nifi.registry.provider.ProviderCreationException;
+import org.apache.nifi.registry.provider.StandardProviderConfigurationContext;
+import org.apache.nifi.registry.provider.flow.StandardFlowSnapshotContext;
+import org.apache.nifi.registry.util.FileUtils;
+import org.eclipse.jgit.api.Git;
+import org.eclipse.jgit.api.errors.GitAPIException;
+import org.eclipse.jgit.lib.StoredConfig;
+import org.eclipse.jgit.revwalk.RevCommit;
+import org.junit.Test;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.File;
+import java.io.IOException;
+import java.nio.charset.StandardCharsets;
+import java.util.Date;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.concurrent.atomic.AtomicInteger;
+import java.util.function.Consumer;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.fail;
+
+public class TestGitFlowPersistenceProvider {
+
+    private static final Logger logger = LoggerFactory.getLogger(TestGitFlowPersistenceProvider.class);
+
+    private void assertCreationFailure(final Map<String, String> properties, final Consumer<ProviderCreationException> assertion) {
+        final GitFlowPersistenceProvider persistenceProvider = new GitFlowPersistenceProvider();
+
+        try {
+            final ProviderConfigurationContext configurationContext = new StandardProviderConfigurationContext(properties);
+            persistenceProvider.onConfigured(configurationContext);
+            fail("Should fail");
+        } catch (ProviderCreationException e) {
+            assertion.accept(e);
+        }
+    }
+
+    @Test
+    public void testNoFlowStorageDirSpecified() {
+        final Map<String, String> properties = new HashMap<>();
+        assertCreationFailure(properties,
+                e -> assertEquals("The property Flow Storage Directory must be provided", e.getMessage()));
+    }
+
+    @Test
+    public void testLoadNonExistingDir() {
+        final Map<String, String> properties = new HashMap<>();
+        properties.put(GitFlowPersistenceProvider.FLOW_STORAGE_DIR_PROP, "target/non-existing");
+        assertCreationFailure(properties,
+                e -> assertEquals("'target/non-existing' is not a directory or does not exist.", e.getCause().getMessage()));
+    }
+
+    @Test
+    public void testLoadNonGitDir() {
+        final Map<String, String> properties = new HashMap<>();
+        properties.put(GitFlowPersistenceProvider.FLOW_STORAGE_DIR_PROP, "target");
+        assertCreationFailure(properties,
+                e -> assertEquals("Directory 'target' does not contain a .git directory." +
+                                " Please init and configure the directory with 'git init' command before using it from NiFi Registry.",
+                        e.getCause().getMessage()));
+    }
+
+    @FunctionalInterface
+    private interface GitConsumer {
+        void accept(Git git) throws GitAPIException;
+    }
+
+    private void assertProvider(final Map<String, String> properties, final GitConsumer gitConsumer, final Consumer<GitFlowPersistenceProvider> assertion, boolean deleteDir)
+            throws IOException, GitAPIException {
+
+        final File gitDir = new File(properties.get(GitFlowPersistenceProvider.FLOW_STORAGE_DIR_PROP));
+        try {
+            FileUtils.ensureDirectoryExistAndCanReadAndWrite(gitDir);
+
+            try (final Git git = Git.init().setDirectory(gitDir).call()) {
+                logger.debug("Initiated a git repository {}", git);
+                final StoredConfig config = git.getRepository().getConfig();
+                config.setString("user", null, "name", "git-user");
+                config.setString("user", null, "email", "git-user@example.com");
+                config.save();
+                gitConsumer.accept(git);
+            }
+
+            final GitFlowPersistenceProvider persistenceProvider = new GitFlowPersistenceProvider();
+
+            final ProviderConfigurationContext configurationContext = new StandardProviderConfigurationContext(properties);
+            persistenceProvider.onConfigured(configurationContext);
+            assertion.accept(persistenceProvider);
+
+        } finally {
+            if (deleteDir) {
+                FileUtils.deleteFile(gitDir, true);
+            }
+        }
+    }
+
+    @Test
+    public void testLoadEmptyGitDir() throws GitAPIException, IOException {
+        final Map<String, String> properties = new HashMap<>();
+        properties.put(GitFlowPersistenceProvider.FLOW_STORAGE_DIR_PROP, "target/empty-git");
+
+        assertProvider(properties, g -> {}, p -> {
+            try {
+                p.getFlowContent("bucket-id-A", "flow-id-1", 1);
+            } catch (FlowPersistenceException e) {
+                assertEquals("Bucket ID bucket-id-A was not found.", e.getMessage());
+            }
+        }, true);
+    }
+
+    @Test
+    public void testLoadCommitHistories() throws GitAPIException, IOException {
+        final Map<String, String> properties = new HashMap<>();
+        properties.put(GitFlowPersistenceProvider.FLOW_STORAGE_DIR_PROP, "target/repo-with-histories");
+
+        assertProvider(properties, g -> {}, p -> {
+            // Create some Flows and keep the directory.
+            final StandardFlowSnapshotContext.Builder contextBuilder = new StandardFlowSnapshotContext.Builder()
+                    .bucketId("bucket-id-A")
+                    .bucketName("C'est/Bucket A/です。")
+                    .flowId("flow-id-1")
+                    .flowName("テスト_用/フロー#1\\[contains invalid chars]")
+                    .author("unit-test-user")
+                    .comments("Initial commit.")
+                    .snapshotTimestamp(new Date().getTime())
+                    .version(1);
+
+            final byte[] flow1Ver1 = "Flow1 ver.1".getBytes(StandardCharsets.UTF_8);
+            p.saveFlowContent(contextBuilder.build(), flow1Ver1);
+
+            contextBuilder.comments("2nd commit.").version(2);
+            final byte[] flow1Ver2 = "Flow1 ver.2".getBytes(StandardCharsets.UTF_8);
+            p.saveFlowContent(contextBuilder.build(), flow1Ver2);
+
+            // Rename flow.
+            contextBuilder.flowName("FlowOne").comments("3rd commit.").version(3);
+            final byte[] flow1Ver3 = "FlowOne ver.3".getBytes(StandardCharsets.UTF_8);
+            p.saveFlowContent(contextBuilder.build(), flow1Ver3);
+
+            // Adding another flow.
+            contextBuilder.flowId("flow-id-2").flowName("FlowTwo").comments("4th commit.").version(1);
+            final byte[] flow2Ver1 = "FlowTwo ver.1".getBytes(StandardCharsets.UTF_8);
+            p.saveFlowContent(contextBuilder.build(), flow2Ver1);
+
+            // Rename bucket.
+            contextBuilder.bucketName("New name for Bucket A").comments("5th commit.").version(2);
+            final byte[] flow2Ver2 = "FlowTwo ver.2".getBytes(StandardCharsets.UTF_8);
+            p.saveFlowContent(contextBuilder.build(), flow2Ver2);
+
+
+        }, false);
+
+        assertProvider(properties, g -> {
+            // Assert commit.
+            final AtomicInteger commitCount = new AtomicInteger(0);
+            final String[] commitMessages = {
+                    "5th commit.\n\nBy NiFi Registry user: unit-test-user",
+                    "4th commit.\n\nBy NiFi Registry user: unit-test-user",
+                    "3rd commit.\n\nBy NiFi Registry user: unit-test-user",
+                    "2nd commit.\n\nBy NiFi Registry user: unit-test-user",
+                    "Initial commit.\n\nBy NiFi Registry user: unit-test-user"
+            };
+            for (RevCommit commit : g.log().call()) {
+                assertEquals("git-user", commit.getAuthorIdent().getName());
+                final int commitIndex = commitCount.getAndIncrement();
+                assertEquals(commitMessages[commitIndex], commit.getFullMessage());
+            }
+            assertEquals(commitMessages.length, commitCount.get());
+        }, p -> {
+            // Should be able to load flow from commit histories.
+            final byte[] flow1Ver1 = p.getFlowContent("bucket-id-A", "flow-id-1", 1);
+            assertEquals("Flow1 ver.1", new String(flow1Ver1, StandardCharsets.UTF_8));
+
+            final byte[] flow1Ver2 = p.getFlowContent("bucket-id-A", "flow-id-1", 2);
+            assertEquals("Flow1 ver.2", new String(flow1Ver2, StandardCharsets.UTF_8));
+
+            // Even if the name of flow has been changed, it can be retrieved by the same flow id.
+            final byte[] flow1Ver3 = p.getFlowContent("bucket-id-A", "flow-id-1", 3);
+            assertEquals("FlowOne ver.3", new String(flow1Ver3, StandardCharsets.UTF_8));
+
+            final byte[] flow2Ver1 = p.getFlowContent("bucket-id-A", "flow-id-2", 1);
+            assertEquals("FlowTwo ver.1", new String(flow2Ver1, StandardCharsets.UTF_8));
+
+            // Even if the name of bucket has been changed, it can be retrieved by the same flow id.
+            final byte[] flow2Ver2 = p.getFlowContent("bucket-id-A", "flow-id-2", 2);
+            assertEquals("FlowTwo ver.2", new String(flow2Ver2, StandardCharsets.UTF_8));
+
+            // Delete the 2nd flow.
+            p.deleteAllFlowContent("bucket-id-A", "flow-id-2");
+
+        }, false);
+
+        assertProvider(properties, g -> {
+            // Assert commit.
+            final AtomicInteger commitCount = new AtomicInteger(0);
+            final String[] commitMessages = {
+                    "Deleted flow FlowTwo.snapshot:flow-id-2 in bucket New_name_for_Bucket_A:bucket-id-A.",
+                    "5th commit.",
+                    "4th commit.",
+                    "3rd commit.",
+                    "2nd commit.",
+                    "Initial commit."
+            };
+            for (RevCommit commit : g.log().call()) {
+                assertEquals("git-user", commit.getAuthorIdent().getName());
+                final int commitIndex = commitCount.getAndIncrement();
+                assertEquals(commitMessages[commitIndex], commit.getShortMessage());
+            }
+            assertEquals(commitMessages.length, commitCount.get());
+        }, p -> {
+            // Should be able to load flow from commit histories.
+            final byte[] flow1Ver1 = p.getFlowContent("bucket-id-A", "flow-id-1", 1);
+            assertEquals("Flow1 ver.1", new String(flow1Ver1, StandardCharsets.UTF_8));
+
+            final byte[] flow1Ver2 = p.getFlowContent("bucket-id-A", "flow-id-1", 2);
+            assertEquals("Flow1 ver.2", new String(flow1Ver2, StandardCharsets.UTF_8));
+
+            // Even if the name of flow has been changed, it can be retrieved by the same flow id.
+            final byte[] flow1Ver3 = p.getFlowContent("bucket-id-A", "flow-id-1", 3);
+            assertEquals("FlowOne ver.3", new String(flow1Ver3, StandardCharsets.UTF_8));
+
+            // The 2nd flow has been deleted, and should not exist.
+            try {
+                p.getFlowContent("bucket-id-A", "flow-id-2", 1);
+            } catch (FlowPersistenceException e) {
+                assertEquals("Flow ID flow-id-2 was not found in bucket New_name_for_Bucket_A:bucket-id-A.", e.getMessage());
+            }
+
+            try {
+                p.getFlowContent("bucket-id-A", "flow-id-2", 2);
+            } catch (FlowPersistenceException e) {
+                assertEquals("Flow ID flow-id-2 was not found in bucket New_name_for_Bucket_A:bucket-id-A.", e.getMessage());
+            }
+
+            // Delete the 1st flow, too.
+            p.deleteAllFlowContent("bucket-id-A", "flow-id-1");
+
+        }, false);
+
+        assertProvider(properties, g -> {
+            // Assert commit.
+            final AtomicInteger commitCount = new AtomicInteger(0);
+            final String[] commitMessages = {
+                    "Deleted flow FlowOne.snapshot:flow-id-1 in bucket New_name_for_Bucket_A:bucket-id-A.",
+                    "Deleted flow FlowTwo.snapshot:flow-id-2 in bucket New_name_for_Bucket_A:bucket-id-A.",
+                    "5th commit.",
+                    "4th commit.",
+                    "3rd commit.",
+                    "2nd commit.",
+                    "Initial commit."
+            };
+            for (RevCommit commit : g.log().call()) {
+                assertEquals("git-user", commit.getAuthorIdent().getName());
+                final int commitIndex = commitCount.getAndIncrement();
+                assertEquals(commitMessages[commitIndex], commit.getShortMessage());
+            }
+            assertEquals(commitMessages.length, commitCount.get());
+        }, p -> {
+            // The 1st flow has been deleted, and should not exist. Moreover, the bucket A has been deleted since there's no flow.
+            try {
+                p.getFlowContent("bucket-id-A", "flow-id-1", 1);
+            } catch (FlowPersistenceException e) {
+                assertEquals("Bucket ID bucket-id-A was not found.", e.getMessage());
+            }
+        }, true);
+    }
+}

http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/6f26290d/nifi-registry-core/nifi-registry-framework/src/test/java/org/apache/nifi/registry/provider/hook/TestScriptEventHookProvider.java
----------------------------------------------------------------------
diff --git a/nifi-registry-core/nifi-registry-framework/src/test/java/org/apache/nifi/registry/provider/hook/TestScriptEventHookProvider.java b/nifi-registry-core/nifi-registry-framework/src/test/java/org/apache/nifi/registry/provider/hook/TestScriptEventHookProvider.java
new file mode 100644
index 0000000..ab24998
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-framework/src/test/java/org/apache/nifi/registry/provider/hook/TestScriptEventHookProvider.java
@@ -0,0 +1,45 @@
+/*
+ * 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.provider.hook;
+
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.when;
+
+import org.apache.nifi.registry.extension.ExtensionManager;
+import org.apache.nifi.registry.properties.NiFiRegistryProperties;
+import org.apache.nifi.registry.provider.ProviderCreationException;
+import org.apache.nifi.registry.provider.ProviderFactory;
+import org.apache.nifi.registry.provider.StandardProviderFactory;
+import org.junit.Test;
+import org.mockito.Mockito;
+
+public class TestScriptEventHookProvider {
+
+    @Test(expected = ProviderCreationException.class)
+    public void testBadScriptProvider() {
+        final NiFiRegistryProperties props = new NiFiRegistryProperties();
+        props.setProperty(NiFiRegistryProperties.PROVIDERS_CONFIGURATION_FILE, "src/test/resources/provider/hook/bad-script-provider.xml");
+
+        final ExtensionManager extensionManager = Mockito.mock(ExtensionManager.class);
+        when(extensionManager.getExtensionClassLoader(any(String.class))).thenReturn(this.getClass().getClassLoader());
+
+        final ProviderFactory providerFactory = new StandardProviderFactory(props, extensionManager);
+        providerFactory.initialize();
+        providerFactory.getEventHookProviders();
+    }
+
+}


[26/51] [partial] nifi-registry git commit: NIFIREG-201 Refactoring project structure to better isolate extensions

Posted by kd...@apache.org.
http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/6f26290d/nifi-registry-core/nifi-registry-framework/src/test/resources/security/authorizers-good-file-providers.xml
----------------------------------------------------------------------
diff --git a/nifi-registry-core/nifi-registry-framework/src/test/resources/security/authorizers-good-file-providers.xml b/nifi-registry-core/nifi-registry-framework/src/test/resources/security/authorizers-good-file-providers.xml
new file mode 100644
index 0000000..98ad3ce
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-framework/src/test/resources/security/authorizers-good-file-providers.xml
@@ -0,0 +1,39 @@
+<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
+<!--
+  ~ 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.
+  -->
+<authorizers>
+
+    <userGroupProvider>
+        <identifier>file-user-group-provider</identifier>
+        <class>org.apache.nifi.registry.security.authorization.file.FileUserGroupProvider</class>
+        <property name="Users File">./target/test-classes/security/users.xml</property>
+    </userGroupProvider>
+
+    <accessPolicyProvider>
+        <identifier>file-access-policy-provider</identifier>
+        <class>org.apache.nifi.registry.security.authorization.file.FileAccessPolicyProvider</class>
+        <property name="User Group Provider">file-user-group-provider</property>
+        <property name="Authorizations File">./target/test-classes/security/authorizations.xml</property>
+    </accessPolicyProvider>
+    
+    <authorizer>
+        <identifier>managed-authorizer</identifier>
+        <class>org.apache.nifi.registry.security.authorization.StandardManagedAuthorizer</class>
+        <property name="Access Policy Provider">file-access-policy-provider</property>
+    </authorizer>
+
+</authorizers>
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/6f26290d/nifi-registry-core/nifi-registry-framework/src/test/resources/serialization/json/no-version.snapshot
----------------------------------------------------------------------
diff --git a/nifi-registry-core/nifi-registry-framework/src/test/resources/serialization/json/no-version.snapshot b/nifi-registry-core/nifi-registry-framework/src/test/resources/serialization/json/no-version.snapshot
new file mode 100644
index 0000000..ce1901f
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-framework/src/test/resources/serialization/json/no-version.snapshot
@@ -0,0 +1,5 @@
+{
+  "header": {
+  },
+  "content": {}
+}
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/6f26290d/nifi-registry-core/nifi-registry-framework/src/test/resources/serialization/json/non-integer-version.snapshot
----------------------------------------------------------------------
diff --git a/nifi-registry-core/nifi-registry-framework/src/test/resources/serialization/json/non-integer-version.snapshot b/nifi-registry-core/nifi-registry-framework/src/test/resources/serialization/json/non-integer-version.snapshot
new file mode 100644
index 0000000..33d4da3
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-framework/src/test/resources/serialization/json/non-integer-version.snapshot
@@ -0,0 +1,6 @@
+{
+  "header": {
+    "dataModelVersion": "One"
+  },
+  "content": {}
+}
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/6f26290d/nifi-registry-core/nifi-registry-framework/src/test/resources/serialization/ver1.snapshot
----------------------------------------------------------------------
diff --git a/nifi-registry-core/nifi-registry-framework/src/test/resources/serialization/ver1.snapshot b/nifi-registry-core/nifi-registry-framework/src/test/resources/serialization/ver1.snapshot
new file mode 100644
index 0000000..7c1ab49
Binary files /dev/null and b/nifi-registry-core/nifi-registry-framework/src/test/resources/serialization/ver1.snapshot differ

http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/6f26290d/nifi-registry-core/nifi-registry-framework/src/test/resources/serialization/ver2.snapshot
----------------------------------------------------------------------
diff --git a/nifi-registry-core/nifi-registry-framework/src/test/resources/serialization/ver2.snapshot b/nifi-registry-core/nifi-registry-framework/src/test/resources/serialization/ver2.snapshot
new file mode 100644
index 0000000..7f4dfc5
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-framework/src/test/resources/serialization/ver2.snapshot
@@ -0,0 +1,97 @@
+{
+  "header": {
+    "dataModelVersion": 2
+  },
+  "content": {
+    "identifier": "a2c80883-171c-316d-ba25-24df2c352693",
+    "name": "Flow1",
+    "comments": "",
+    "position": {
+      "x": 1549.249149182042,
+      "y": 764.2426186568309
+    },
+    "processGroups": [],
+    "remoteProcessGroups": [],
+    "processors": [
+      {
+        "identifier": "92fe4513-21c0-34f6-a916-2874f46ae864",
+        "name": "GenerateFlowFile",
+        "comments": "",
+        "position": {
+          "x": 488.99999411591034,
+          "y": 114.00000359389122
+        },
+        "bundle": {
+          "group": "org.apache.nifi",
+          "artifact": "nifi-standard-nar",
+          "version": "1.6.0-SNAPSHOT"
+        },
+        "style": {},
+        "type": "org.apache.nifi.processors.standard.GenerateFlowFile",
+        "properties": {
+          "character-set": "UTF-8",
+          "File Size": "0B",
+          "Batch Size": "1",
+          "Unique FlowFiles": "false",
+          "Data Format": "Text"
+        },
+        "propertyDescriptors": {
+          "character-set": {
+            "name": "character-set",
+            "displayName": "Character Set",
+            "identifiesControllerService": false,
+            "sensitive": false
+          },
+          "File Size": {
+            "name": "File Size",
+            "displayName": "File Size",
+            "identifiesControllerService": false,
+            "sensitive": false
+          },
+          "generate-ff-custom-text": {
+            "name": "generate-ff-custom-text",
+            "displayName": "Custom Text",
+            "identifiesControllerService": false,
+            "sensitive": false
+          },
+          "Batch Size": {
+            "name": "Batch Size",
+            "displayName": "Batch Size",
+            "identifiesControllerService": false,
+            "sensitive": false
+          },
+          "Unique FlowFiles": {
+            "name": "Unique FlowFiles",
+            "displayName": "Unique FlowFiles",
+            "identifiesControllerService": false,
+            "sensitive": false
+          },
+          "Data Format": {
+            "name": "Data Format",
+            "displayName": "Data Format",
+            "identifiesControllerService": false,
+            "sensitive": false
+          }
+        },
+        "schedulingPeriod": "0 sec",
+        "schedulingStrategy": "TIMER_DRIVEN",
+        "executionNode": "ALL",
+        "penaltyDuration": "30 sec",
+        "yieldDuration": "1 sec",
+        "bulletinLevel": "WARN",
+        "runDurationMillis": 0,
+        "concurrentlySchedulableTaskCount": 1,
+        "componentType": "PROCESSOR",
+        "groupIdentifier": "a2c80883-171c-316d-ba25-24df2c352693"
+      }
+    ],
+    "inputPorts": [],
+    "outputPorts": [],
+    "connections": [],
+    "labels": [],
+    "funnels": [],
+    "controllerServices": [],
+    "variables": {},
+    "componentType": "PROCESS_GROUP"
+  }
+}
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/6f26290d/nifi-registry-core/nifi-registry-framework/src/test/resources/serialization/ver3.snapshot
----------------------------------------------------------------------
diff --git a/nifi-registry-core/nifi-registry-framework/src/test/resources/serialization/ver3.snapshot b/nifi-registry-core/nifi-registry-framework/src/test/resources/serialization/ver3.snapshot
new file mode 100644
index 0000000..574fe56
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-framework/src/test/resources/serialization/ver3.snapshot
@@ -0,0 +1,6 @@
+{
+  "header": {
+    "dataModelVersion": 3
+  },
+  "content": {}
+}
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/6f26290d/nifi-registry-core/nifi-registry-jetty/pom.xml
----------------------------------------------------------------------
diff --git a/nifi-registry-core/nifi-registry-jetty/pom.xml b/nifi-registry-core/nifi-registry-jetty/pom.xml
new file mode 100644
index 0000000..9c17c11
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-jetty/pom.xml
@@ -0,0 +1,66 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+  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.
+-->
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+    <modelVersion>4.0.0</modelVersion>
+    <parent>
+        <groupId>org.apache.nifi.registry</groupId>
+        <artifactId>nifi-registry-core</artifactId>
+        <version>0.3.0-SNAPSHOT</version>
+    </parent>
+    <artifactId>nifi-registry-jetty</artifactId>
+    <packaging>jar</packaging>
+    <dependencies>
+        <dependency>
+            <groupId>org.apache.nifi.registry</groupId>
+            <artifactId>nifi-registry-properties</artifactId>
+            <version>0.3.0-SNAPSHOT</version>
+        </dependency>
+        <dependency>
+            <groupId>org.eclipse.jetty</groupId>
+            <artifactId>jetty-server</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>org.eclipse.jetty</groupId>
+            <artifactId>jetty-servlet</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>org.eclipse.jetty</groupId>
+            <artifactId>jetty-webapp</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>org.eclipse.jetty</groupId>
+            <artifactId>jetty-servlets</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>org.eclipse.jetty</groupId>
+            <artifactId>jetty-annotations</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>org.apache.commons</groupId>
+            <artifactId>commons-lang3</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>org.eclipse.jetty</groupId>
+            <artifactId>apache-jsp</artifactId>
+            <scope>compile</scope>
+        </dependency>
+        <dependency>
+            <groupId>org.eclipse.jetty</groupId>
+            <artifactId>apache-jstl</artifactId>
+            <scope>compile</scope>
+        </dependency>
+    </dependencies>
+</project>

http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/6f26290d/nifi-registry-core/nifi-registry-jetty/src/main/java/org/apache/nifi/registry/jetty/JettyServer.java
----------------------------------------------------------------------
diff --git a/nifi-registry-core/nifi-registry-jetty/src/main/java/org/apache/nifi/registry/jetty/JettyServer.java b/nifi-registry-core/nifi-registry-jetty/src/main/java/org/apache/nifi/registry/jetty/JettyServer.java
new file mode 100644
index 0000000..c202a5b
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-jetty/src/main/java/org/apache/nifi/registry/jetty/JettyServer.java
@@ -0,0 +1,489 @@
+/*
+ * 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.jetty;
+
+import org.apache.commons.lang3.StringUtils;
+import org.apache.nifi.registry.properties.NiFiRegistryProperties;
+import org.apache.nifi.registry.security.crypto.CryptoKeyProvider;
+import org.eclipse.jetty.annotations.AnnotationConfiguration;
+import org.eclipse.jetty.server.Connector;
+import org.eclipse.jetty.server.Handler;
+import org.eclipse.jetty.server.HttpConfiguration;
+import org.eclipse.jetty.server.HttpConnectionFactory;
+import org.eclipse.jetty.server.SecureRequestCustomizer;
+import org.eclipse.jetty.server.Server;
+import org.eclipse.jetty.server.ServerConnector;
+import org.eclipse.jetty.server.SslConnectionFactory;
+import org.eclipse.jetty.server.handler.ContextHandler;
+import org.eclipse.jetty.server.handler.HandlerCollection;
+import org.eclipse.jetty.server.handler.ResourceHandler;
+import org.eclipse.jetty.util.resource.Resource;
+import org.eclipse.jetty.util.resource.ResourceCollection;
+import org.eclipse.jetty.util.ssl.SslContextFactory;
+import org.eclipse.jetty.util.thread.QueuedThreadPool;
+import org.eclipse.jetty.webapp.Configuration;
+import org.eclipse.jetty.webapp.JettyWebXmlConfiguration;
+import org.eclipse.jetty.webapp.WebAppClassLoader;
+import org.eclipse.jetty.webapp.WebAppContext;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.File;
+import java.io.FileFilter;
+import java.io.IOException;
+import java.net.InetAddress;
+import java.net.MalformedURLException;
+import java.net.NetworkInterface;
+import java.net.SocketException;
+import java.net.URL;
+import java.net.URLClassLoader;
+import java.nio.file.Paths;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.Enumeration;
+import java.util.HashSet;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Set;
+
+
+public class JettyServer {
+
+    private static final Logger logger = LoggerFactory.getLogger(JettyServer.class);
+    private static final String WEB_DEFAULTS_XML = "org/apache/nifi-registry/web/webdefault.xml";
+    private static final int HEADER_BUFFER_SIZE = 16 * 1024; // 16kb
+
+    private static final FileFilter WAR_FILTER = new FileFilter() {
+        @Override
+        public boolean accept(File pathname) {
+            final String nameToTest = pathname.getName().toLowerCase();
+            return nameToTest.endsWith(".war") && pathname.isFile();
+        }
+    };
+
+    private final NiFiRegistryProperties properties;
+    private final CryptoKeyProvider masterKeyProvider;
+    private final Server server;
+
+    private WebAppContext webUiContext;
+    private WebAppContext webApiContext;
+    private WebAppContext webDocsContext;
+
+    public JettyServer(final NiFiRegistryProperties properties, final CryptoKeyProvider cryptoKeyProvider) {
+        final QueuedThreadPool threadPool = new QueuedThreadPool(properties.getWebThreads());
+        threadPool.setName("NiFi Registry Web Server");
+
+        this.properties = properties;
+        this.masterKeyProvider = cryptoKeyProvider;
+        this.server = new Server(threadPool);
+
+        // enable the annotation based configuration to ensure the jsp container is initialized properly
+        final Configuration.ClassList classlist = Configuration.ClassList.setServerDefault(server);
+        classlist.addBefore(JettyWebXmlConfiguration.class.getName(), AnnotationConfiguration.class.getName());
+
+        try {
+            configureConnectors();
+            loadWars();
+        } catch (final Throwable t) {
+            startUpFailure(t);
+        }
+    }
+
+    private void configureConnectors() {
+        // create the http configuration
+        final HttpConfiguration httpConfiguration = new HttpConfiguration();
+        httpConfiguration.setRequestHeaderSize(HEADER_BUFFER_SIZE);
+        httpConfiguration.setResponseHeaderSize(HEADER_BUFFER_SIZE);
+
+        if (properties.getPort() != null) {
+            final Integer port = properties.getPort();
+            if (port < 0 || (int) Math.pow(2, 16) <= port) {
+                throw new IllegalStateException("Invalid HTTP port: " + port);
+            }
+
+            logger.info("Configuring Jetty for HTTP on port: " + port);
+
+            // create the connector
+            final ServerConnector http = new ServerConnector(server, new HttpConnectionFactory(httpConfiguration));
+
+            // set host and port
+            if (StringUtils.isNotBlank(properties.getHttpHost())) {
+                http.setHost(properties.getHttpHost());
+            }
+            http.setPort(port);
+
+            // add this connector
+            server.addConnector(http);
+        } else if (properties.getSslPort() != null) {
+            final Integer port = properties.getSslPort();
+            if (port < 0 || (int) Math.pow(2, 16) <= port) {
+                throw new IllegalStateException("Invalid HTTPs port: " + port);
+            }
+
+            if (StringUtils.isBlank(properties.getKeyStorePath())) {
+                throw new IllegalStateException(NiFiRegistryProperties.SECURITY_KEYSTORE
+                        + " must be provided to configure Jetty for HTTPs");
+            }
+
+            logger.info("Configuring Jetty for HTTPs on port: " + port);
+
+            // add some secure config
+            final HttpConfiguration httpsConfiguration = new HttpConfiguration(httpConfiguration);
+            httpsConfiguration.setSecureScheme("https");
+            httpsConfiguration.setSecurePort(properties.getSslPort());
+            httpsConfiguration.addCustomizer(new SecureRequestCustomizer());
+
+            // build the connector
+            final ServerConnector https = new ServerConnector(server,
+                    new SslConnectionFactory(createSslContextFactory(), "http/1.1"),
+                    new HttpConnectionFactory(httpsConfiguration));
+
+            // set host and port
+            if (StringUtils.isNotBlank(properties.getHttpsHost())) {
+                https.setHost(properties.getHttpsHost());
+            }
+            https.setPort(port);
+
+            // add this connector
+            server.addConnector(https);
+        }
+    }
+
+    private SslContextFactory createSslContextFactory() {
+        final SslContextFactory contextFactory = new SslContextFactory();
+
+        // if needClientAuth is false then set want to true so we can optionally use certs
+        if (properties.getNeedClientAuth()) {
+            logger.info("Setting Jetty's SSLContextFactory needClientAuth to true");
+            contextFactory.setNeedClientAuth(true);
+        } else {
+            logger.info("Setting Jetty's SSLContextFactory wantClientAuth to true");
+            contextFactory.setWantClientAuth(true);
+        }
+
+        /* below code sets JSSE system properties when values are provided */
+        // keystore properties
+        if (StringUtils.isNotBlank(properties.getKeyStorePath())) {
+            contextFactory.setKeyStorePath(properties.getKeyStorePath());
+        }
+        if (StringUtils.isNotBlank(properties.getKeyStoreType())) {
+            contextFactory.setKeyStoreType(properties.getKeyStoreType());
+        }
+        final String keystorePassword = properties.getKeyStorePassword();
+        final String keyPassword = properties.getKeyPassword();
+        if (StringUtils.isNotBlank(keystorePassword)) {
+            // if no key password was provided, then assume the keystore password is the same as the key password.
+            final String defaultKeyPassword = (StringUtils.isBlank(keyPassword)) ? keystorePassword : keyPassword;
+            contextFactory.setKeyManagerPassword(keystorePassword);
+            contextFactory.setKeyStorePassword(defaultKeyPassword);
+        } else if (StringUtils.isNotBlank(keyPassword)) {
+            // since no keystore password was provided, there will be no keystore integrity check
+            contextFactory.setKeyStorePassword(keyPassword);
+        }
+
+        // truststore properties
+        if (StringUtils.isNotBlank(properties.getTrustStorePath())) {
+            contextFactory.setTrustStorePath(properties.getTrustStorePath());
+        }
+        if (StringUtils.isNotBlank(properties.getTrustStoreType())) {
+            contextFactory.setTrustStoreType(properties.getTrustStoreType());
+        }
+        if (StringUtils.isNotBlank(properties.getTrustStorePassword())) {
+            contextFactory.setTrustStorePassword(properties.getTrustStorePassword());
+        }
+
+        return contextFactory;
+    }
+
+    private void loadWars() throws IOException {
+        final File warDirectory = properties.getWarLibDirectory();
+        final File[] wars = warDirectory.listFiles(WAR_FILTER);
+
+        if (wars == null) {
+            throw new RuntimeException("Unable to access war lib directory: " + warDirectory);
+        }
+
+        File webUiWar = null;
+        File webApiWar = null;
+        File webDocsWar = null;
+        for (final File war : wars) {
+            if (war.getName().startsWith("nifi-registry-web-ui")) {
+                webUiWar = war;
+            } else if (war.getName().startsWith("nifi-registry-web-api")) {
+                webApiWar = war;
+            } else if (war.getName().startsWith("nifi-registry-web-docs")) {
+                webDocsWar = war;
+            }
+        }
+
+        if (webUiWar == null) {
+            throw new IllegalStateException("Unable to locate NiFi Registry Web UI");
+        } else if (webApiWar == null) {
+            throw new IllegalStateException("Unable to locate NiFi Registry Web API");
+        } else if (webDocsWar == null) {
+            throw new IllegalStateException("Unable to locate NiFi Registry Web Docs");
+        }
+
+        webUiContext = loadWar(webUiWar, "/nifi-registry");
+
+        webApiContext = loadWar(webApiWar, "/nifi-registry-api", getWebApiAdditionalClasspath());
+        logger.info("Adding {} object to ServletContext with key 'nifi-registry.properties'", properties.getClass().getSimpleName());
+        webApiContext.setAttribute("nifi-registry.properties", properties);
+        logger.info("Adding {} object to ServletContext with key 'nifi-registry.key'", masterKeyProvider.getClass().getSimpleName());
+        webApiContext.setAttribute("nifi-registry.key", masterKeyProvider);
+
+        // there is an issue scanning the asm repackaged jar so narrow down what we are scanning
+        webApiContext.setAttribute("org.eclipse.jetty.server.webapp.WebInfIncludeJarPattern", ".*/spring-[^/]*\\.jar$");
+
+        final String docsContextPath = "/nifi-registry-docs";
+        webDocsContext = loadWar(webDocsWar, docsContextPath);
+
+        final HandlerCollection handlers = new HandlerCollection();
+        handlers.addHandler(webUiContext);
+        handlers.addHandler(webApiContext);
+        handlers.addHandler(createDocsWebApp(docsContextPath));
+        handlers.addHandler(webDocsContext);
+        server.setHandler(handlers);
+    }
+
+    private WebAppContext loadWar(final File warFile, final String contextPath)
+            throws IOException {
+        return loadWar(warFile, contextPath, new URL[0]);
+    }
+
+    private WebAppContext loadWar(final File warFile, final String contextPath, final URL[] additionalResources)
+            throws IOException {
+        final WebAppContext webappContext = new WebAppContext(warFile.getPath(), contextPath);
+        webappContext.setContextPath(contextPath);
+        webappContext.setDisplayName(contextPath);
+
+        // remove slf4j server class to allow WAR files to have slf4j dependencies in WEB-INF/lib
+        List<String> serverClasses = new ArrayList<>(Arrays.asList(webappContext.getServerClasses()));
+        serverClasses.remove("org.slf4j.");
+        webappContext.setServerClasses(serverClasses.toArray(new String[0]));
+        webappContext.setDefaultsDescriptor(WEB_DEFAULTS_XML);
+
+        // get the temp directory for this webapp
+        final File webWorkingDirectory = properties.getWebWorkingDirectory();
+        final File tempDir = new File(webWorkingDirectory, warFile.getName());
+        if (tempDir.exists() && !tempDir.isDirectory()) {
+            throw new RuntimeException(tempDir.getAbsolutePath() + " is not a directory");
+        } else if (!tempDir.exists()) {
+            final boolean made = tempDir.mkdirs();
+            if (!made) {
+                throw new RuntimeException(tempDir.getAbsolutePath() + " could not be created");
+            }
+        }
+        if (!(tempDir.canRead() && tempDir.canWrite())) {
+            throw new RuntimeException(tempDir.getAbsolutePath() + " directory does not have read/write privilege");
+        }
+
+        // configure the temp dir
+        webappContext.setTempDirectory(tempDir);
+
+        // configure the max form size (3x the default)
+        webappContext.setMaxFormContentSize(600000);
+
+        // start out assuming the system ClassLoader will be the parent, but if additional resources were specified then
+        // inject a new ClassLoader in between the system and webapp ClassLoaders that contains the additional resources
+        ClassLoader parentClassLoader = ClassLoader.getSystemClassLoader();
+        if (additionalResources != null && additionalResources.length > 0) {
+            URLClassLoader additionalClassLoader = new URLClassLoader(additionalResources, ClassLoader.getSystemClassLoader());
+            parentClassLoader = additionalClassLoader;
+        }
+
+        webappContext.setClassLoader(new WebAppClassLoader(parentClassLoader, webappContext));
+
+        logger.info("Loading WAR: " + warFile.getAbsolutePath() + " with context path set to " + contextPath);
+        return webappContext;
+    }
+
+    private URL[] getWebApiAdditionalClasspath() {
+        final String dbDriverDir = properties.getDatabaseDriverDirectory();
+
+        if (StringUtils.isBlank(dbDriverDir)) {
+            logger.info("No database driver directory was specified");
+            return new URL[0];
+        }
+
+        final File dirFile = new File(dbDriverDir);
+
+        if (!dirFile.exists()) {
+            logger.warn("Skipping database driver directory that does not exist: " + dbDriverDir);
+            return new URL[0];
+        }
+
+        if (!dirFile.canRead()) {
+            logger.warn("Skipping database driver directory that can not be read: " + dbDriverDir);
+            return new URL[0];
+        }
+
+        final List<URL> resources = new LinkedList<>();
+        try {
+            resources.add(dirFile.toURI().toURL());
+        } catch (final MalformedURLException mfe) {
+            logger.warn("Unable to add {} to classpath due to {}", new Object[]{ dirFile.getAbsolutePath(), mfe.getMessage()}, mfe);
+        }
+
+        if (dirFile.isDirectory()) {
+            final File[] files = dirFile.listFiles();
+            if (files != null) {
+                for (final File resource : files) {
+                    if (resource.isDirectory()) {
+                        logger.warn("Recursive directories are not supported, skipping " + resource.getAbsolutePath());
+                    } else {
+                        try {
+                            resources.add(resource.toURI().toURL());
+                        } catch (final MalformedURLException mfe) {
+                            logger.warn("Unable to add {} to classpath due to {}", new Object[]{ resource.getAbsolutePath(), mfe.getMessage()}, mfe);
+                        }
+                    }
+                }
+            }
+        }
+
+        if (!resources.isEmpty()) {
+            logger.info("Added additional resources to nifi-registry-api classpath: [");
+            for (URL resource : resources) {
+                logger.info(" " + resource.toString());
+            }
+            logger.info("]");
+        }
+
+        return resources.toArray(new URL[resources.size()]);
+    }
+
+    private ContextHandler createDocsWebApp(final String contextPath) throws IOException {
+        final ResourceHandler resourceHandler = new ResourceHandler();
+        resourceHandler.setDirectoriesListed(false);
+
+        // load the docs directory
+        final File docsDir = Paths.get("docs").toRealPath().toFile();
+        final Resource docsResource = Resource.newResource(docsDir);
+
+        // load the rest documentation
+        final File webApiDocsDir = new File(webApiContext.getTempDirectory(), "webapp/docs");
+        if (!webApiDocsDir.exists()) {
+            final boolean made = webApiDocsDir.mkdirs();
+            if (!made) {
+                throw new RuntimeException(webApiDocsDir.getAbsolutePath() + " could not be created");
+            }
+        }
+        final Resource webApiDocsResource = Resource.newResource(webApiDocsDir);
+
+        // create resources for both docs locations
+        final ResourceCollection resources = new ResourceCollection(docsResource, webApiDocsResource);
+        resourceHandler.setBaseResource(resources);
+
+        // create the context handler
+        final ContextHandler handler = new ContextHandler(contextPath);
+        handler.setHandler(resourceHandler);
+
+        logger.info("Loading documents web app with context path set to " + contextPath);
+        return handler;
+    }
+
+    public void start() {
+        try {
+            // start the server
+            server.start();
+
+            // ensure everything started successfully
+            for (Handler handler : server.getChildHandlers()) {
+                // see if the handler is a web app
+                if (handler instanceof WebAppContext) {
+                    WebAppContext context = (WebAppContext) handler;
+
+                    // see if this webapp had any exceptions that would
+                    // cause it to be unavailable
+                    if (context.getUnavailableException() != null) {
+                        startUpFailure(context.getUnavailableException());
+                    }
+                }
+            }
+
+            dumpUrls();
+        } catch (final Throwable t) {
+            startUpFailure(t);
+        }
+    }
+
+    private void startUpFailure(Throwable t) {
+        System.err.println("Failed to start web server: " + t.getMessage());
+        System.err.println("Shutting down...");
+        logger.warn("Failed to start web server... shutting down.", t);
+        System.exit(1);
+    }
+
+    private void dumpUrls() throws SocketException {
+        final List<String> urls = new ArrayList<>();
+
+        for (Connector connector : server.getConnectors()) {
+            if (connector instanceof ServerConnector) {
+                final ServerConnector serverConnector = (ServerConnector) connector;
+
+                Set<String> hosts = new HashSet<>();
+
+                // determine the hosts
+                if (StringUtils.isNotBlank(serverConnector.getHost())) {
+                    hosts.add(serverConnector.getHost());
+                } else {
+                    Enumeration<NetworkInterface> networkInterfaces = NetworkInterface.getNetworkInterfaces();
+                    if (networkInterfaces != null) {
+                        for (NetworkInterface networkInterface : Collections.list(networkInterfaces)) {
+                            for (InetAddress inetAddress : Collections.list(networkInterface.getInetAddresses())) {
+                                hosts.add(inetAddress.getHostAddress());
+                            }
+                        }
+                    }
+                }
+
+                // ensure some hosts were found
+                if (!hosts.isEmpty()) {
+                    String scheme = "http";
+                    if (properties.getSslPort() != null && serverConnector.getPort() == properties.getSslPort()) {
+                        scheme = "https";
+                    }
+
+                    // dump each url
+                    for (String host : hosts) {
+                        urls.add(String.format("%s://%s:%s", scheme, host, serverConnector.getPort()));
+                    }
+                }
+            }
+        }
+
+        if (urls.isEmpty()) {
+            logger.warn("NiFi Registry has started, but the UI is not available on any hosts. Please verify the host properties.");
+        } else {
+            // log the ui location
+            logger.info("NiFi Registry has started. The UI is available at the following URLs:");
+            for (final String url : urls) {
+                logger.info(String.format("%s/nifi-registry", url));
+            }
+        }
+    }
+
+    public void stop() {
+        try {
+            server.stop();
+        } catch (Exception ex) {
+            logger.warn("Failed to stop web server", ex);
+        }
+    }
+}

http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/6f26290d/nifi-registry-core/nifi-registry-jetty/src/main/resources/org/apache/nifi-registry/web/webdefault.xml
----------------------------------------------------------------------
diff --git a/nifi-registry-core/nifi-registry-jetty/src/main/resources/org/apache/nifi-registry/web/webdefault.xml b/nifi-registry-core/nifi-registry-jetty/src/main/resources/org/apache/nifi-registry/web/webdefault.xml
new file mode 100644
index 0000000..814dbd8
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-jetty/src/main/resources/org/apache/nifi-registry/web/webdefault.xml
@@ -0,0 +1,556 @@
+<?xml version="1.0" encoding="ISO-8859-1"?>
+<!--
+  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.
+-->
+<web-app 
+    xmlns="http://xmlns.jcp.org/xml/ns/javaee" 
+    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+    xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_3_1.xsd"
+    metadata-complete="false"
+    version="3.1"> 
+
+    <!-- ===================================================================== -->
+    <!-- This file contains the default descriptor for web applications.       -->
+    <!-- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -->
+    <!-- The intent of this descriptor is to include jetty specific or common  -->
+    <!-- configuration for all webapps.   If a context has a webdefault.xml    -->
+    <!-- descriptor, it is applied before the contexts own web.xml file        -->
+    <!--                                                                       -->
+    <!-- A context may be assigned a default descriptor by:                    -->
+    <!--  + Calling WebApplicationContext.setDefaultsDescriptor                -->
+    <!--  + Passed an arg to addWebApplications                                -->
+    <!--                                                                       -->
+    <!-- This file is used both as the resource within the jetty.jar (which is -->
+    <!-- used as the default if no explicit defaults descriptor is set) and it -->
+    <!-- is copied to the etc directory of the Jetty distro and explicitly     -->
+    <!-- by the jetty.xml file.                                                -->
+    <!--                                                                       -->
+    <!-- ===================================================================== -->
+
+    <description>
+        Default web.xml file.  
+        This file is applied to a Web application before it's own WEB_INF/web.xml file
+    </description>
+
+    <!-- ==================================================================== -->
+    <!-- Removes static references to beans from javax.el.BeanELResolver to   -->
+    <!-- ensure webapp classloader can be released on undeploy                -->
+    <!-- ==================================================================== -->
+    <listener>
+        <listener-class>org.eclipse.jetty.servlet.listener.ELContextCleaner</listener-class>
+    </listener>
+  
+    <!-- ==================================================================== -->
+    <!-- Removes static cache of Methods from java.beans.Introspector to      -->
+    <!-- ensure webapp classloader can be released on undeploy                -->
+    <!-- ==================================================================== -->  
+    <listener>
+        <listener-class>org.eclipse.jetty.servlet.listener.IntrospectorCleaner</listener-class>
+    </listener>
+  
+
+    <!-- ==================================================================== -->
+    <!-- Context params to control Session Cookies                            -->
+    <!-- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -  -->
+    <!--
+      UNCOMMENT TO ACTIVATE 
+      <context-param> 
+        <param-name>org.eclipse.jetty.servlet.SessionDomain</param-name> 
+        <param-value>127.0.0.1</param-value> 
+      </context-param> 
+      <context-param>
+        <param-name>org.eclipse.jetty.servlet.SessionPath</param-name>
+        <param-value>/</param-value>
+      </context-param>
+      <context-param>
+        <param-name>org.eclipse.jetty.servlet.MaxAge</param-name>
+        <param-value>-1</param-value>
+      </context-param>
+    -->
+
+    <!-- ==================================================================== -->
+    <!-- The default servlet.                                                 -->
+    <!-- This servlet, normally mapped to /, provides the handling for static -->
+    <!-- content, OPTIONS and TRACE methods for the context.                  -->
+    <!-- The following initParameters are supported:                          -->
+    <!--  
+    *  acceptRanges      If true, range requests and responses are
+    *                    supported
+    *
+    *  dirAllowed        If true, directory listings are returned if no
+    *                    welcome file is found. Else 403 Forbidden.
+    *
+    *  welcomeServlets   If true, attempt to dispatch to welcome files
+    *                    that are servlets, but only after no matching static
+    *                    resources could be found. If false, then a welcome
+    *                    file must exist on disk. If "exact", then exact
+    *                    servlet matches are supported without an existing file.
+    *                    Default is true.
+    *
+    *                    This must be false if you want directory listings,
+    *                    but have index.jsp in your welcome file list.
+    *
+    *  redirectWelcome   If true, welcome files are redirected rather than
+    *                    forwarded to.
+    *
+    *  gzip              If set to true, then static content will be served as
+    *                    gzip content encoded if a matching resource is
+    *                    found ending with ".gz"
+    *
+    *  resourceBase      Set to replace the context resource base
+    *
+    *  resourceCache     If set, this is a context attribute name, which the servlet
+    *                    will use to look for a shared ResourceCache instance.
+    *
+    *  relativeResourceBase
+    *                    Set with a pathname relative to the base of the
+    *                    servlet context root. Useful for only serving static content out
+    *                    of only specific subdirectories.
+    *
+    *  pathInfoOnly      If true, only the path info will be applied to the resourceBase
+    *
+    *  stylesheet        Set with the location of an optional stylesheet that will be used
+    *                    to decorate the directory listing html.
+    *
+    *  aliases           If True, aliases of resources are allowed (eg. symbolic
+    *                    links and caps variations). May bypass security constraints.
+    *                    
+    *  etags             If True, weak etags will be generated and handled.
+    *
+    *  maxCacheSize      The maximum total size of the cache or 0 for no cache.
+    *  maxCachedFileSize The maximum size of a file to cache
+    *  maxCachedFiles    The maximum number of files to cache
+    *
+    *  useFileMappedBuffer
+    *                    If set to true, it will use mapped file buffer to serve static content
+    *                    when using NIO connector. Setting this value to false means that
+    *                    a direct buffer will be used instead of a mapped file buffer.
+    *                    By default, this is set to true.
+    *
+    *  cacheControl      If set, all static content will have this value set as the cache-control
+    *                    header.
+    *
+    -->
+ 
+ 
+    <!-- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -  -->
+    <servlet>
+        <servlet-name>default</servlet-name>
+        <servlet-class>org.eclipse.jetty.servlet.DefaultServlet</servlet-class>
+        <init-param>
+            <param-name>aliases</param-name>
+            <param-value>false</param-value>
+        </init-param>
+        <init-param>
+            <param-name>acceptRanges</param-name>
+            <param-value>true</param-value>
+        </init-param>
+        <init-param>
+            <param-name>dirAllowed</param-name>
+            <param-value>false</param-value>
+        </init-param>
+        <init-param>
+            <param-name>welcomeServlets</param-name>
+            <param-value>true</param-value>
+        </init-param>
+        <init-param>
+            <param-name>redirectWelcome</param-name>
+            <param-value>false</param-value>
+        </init-param>
+        <init-param>
+            <param-name>maxCacheSize</param-name>
+            <param-value>256000000</param-value>
+        </init-param>
+        <init-param>
+            <param-name>maxCachedFileSize</param-name>
+            <param-value>200000000</param-value>
+        </init-param>
+        <init-param>
+            <param-name>maxCachedFiles</param-name>
+            <param-value>2048</param-value>
+        </init-param>
+        <init-param>
+            <param-name>gzip</param-name>
+            <param-value>true</param-value>
+        </init-param>
+        <init-param>
+            <param-name>etags</param-name>
+            <param-value>false</param-value>
+        </init-param>
+        <init-param>
+            <param-name>useFileMappedBuffer</param-name>
+            <param-value>true</param-value>
+        </init-param>
+        <!--
+        <init-param>
+          <param-name>resourceCache</param-name>
+          <param-value>resourceCache</param-value>
+        </init-param>
+        -->
+        <!--
+        <init-param>
+          <param-name>cacheControl</param-name>
+          <param-value>max-age=3600,public</param-value>
+        </init-param>
+        -->
+        <load-on-startup>0</load-on-startup>
+    </servlet>
+
+    <servlet-mapping>
+        <servlet-name>default</servlet-name>
+        <url-pattern>/</url-pattern>
+    </servlet-mapping>
+
+
+    <!-- ==================================================================== -->
+    <!-- JSP Servlet                                                          -->
+    <!-- This is the jasper JSP servlet from the jakarta project              -->
+    <!-- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -  -->
+    <!-- The JSP page compiler and execution servlet, which is the mechanism  -->
+    <!-- used by Glassfish to support JSP pages.  Traditionally, this servlet -->
+    <!-- is mapped to URL patterh "*.jsp".  This servlet supports the         -->
+    <!-- following initialization parameters (default values are in square    -->
+    <!-- brackets):                                                           -->
+    <!--                                                                      -->
+    <!--   checkInterval       If development is false and reloading is true, -->
+    <!--                       background compiles are enabled. checkInterval -->
+    <!--                       is the time in seconds between checks to see   -->
+    <!--                       if a JSP page needs to be recompiled. [300]    -->
+    <!--                                                                      -->
+    <!--   compiler            Which compiler Ant should use to compile JSP   -->
+    <!--                       pages.  See the Ant documenation for more      -->
+    <!--                       information. [javac]                           -->
+    <!--                                                                      -->
+    <!--   classdebuginfo      Should the class file be compiled with         -->
+    <!--                       debugging information?  [true]                 -->
+    <!--                                                                      -->
+    <!--   classpath           What class path should I use while compiling   -->
+    <!--                       generated servlets?  [Created dynamically      -->
+    <!--                       based on the current web application]          -->
+    <!--                       Set to ? to make the container explicitly set  -->
+    <!--                       this parameter.                                -->
+    <!--                                                                      -->
+    <!--   development         Is Jasper used in development mode (will check -->
+    <!--                       for JSP modification on every access)?  [true] -->
+    <!--                                                                      -->
+    <!--   enablePooling       Determines whether tag handler pooling is      -->
+    <!--                       enabled  [true]                                -->
+    <!--                                                                      -->
+    <!--   fork                Tell Ant to fork compiles of JSP pages so that -->
+    <!--                       a separate JVM is used for JSP page compiles   -->
+    <!--                       from the one Tomcat is running in. [true]      -->
+    <!--                                                                      -->
+    <!--   ieClassId           The class-id value to be sent to Internet      -->
+    <!--                       Explorer when using <jsp:plugin> tags.         -->
+    <!--                       [clsid:8AD9C840-044E-11D1-B3E9-00805F499D93]   -->
+    <!--                                                                      -->
+    <!--   javaEncoding        Java file encoding to use for generating java  -->
+    <!--                       source files. [UTF-8]                          -->
+    <!--                                                                      -->
+    <!--   keepgenerated       Should we keep the generated Java source code  -->
+    <!--                       for each page instead of deleting it? [true]   -->
+    <!--                                                                      -->
+    <!--   logVerbosityLevel   The level of detailed messages to be produced  -->
+    <!--                       by this servlet.  Increasing levels cause the  -->
+    <!--                       generation of more messages.  Valid values are -->
+    <!--                       FATAL, ERROR, WARNING, INFORMATION, and DEBUG. -->
+    <!--                       [WARNING]                                      -->
+    <!--                                                                      -->
+    <!--   mappedfile          Should we generate static content with one     -->
+    <!--                       print statement per input line, to ease        -->
+    <!--                       debugging?  [false]                            -->
+    <!--                                                                      -->
+    <!--                                                                      -->
+    <!--   reloading           Should Jasper check for modified JSPs?  [true] -->
+    <!--                                                                      -->
+    <!--   suppressSmap        Should the generation of SMAP info for JSR45   -->
+    <!--                       debugging be suppressed?  [false]              -->
+    <!--                                                                      -->
+    <!--   dumpSmap            Should the SMAP info for JSR45 debugging be    -->
+    <!--                       dumped to a file? [false]                      -->
+    <!--                       False if suppressSmap is true                  -->
+    <!--                                                                      -->
+    <!--   scratchdir          What scratch directory should we use when      -->
+    <!--                       compiling JSP pages?  [default work directory  -->
+    <!--                       for the current web application]               -->
+    <!--                                                                      -->
+    <!--   tagpoolMaxSize      The maximum tag handler pool size  [5]         -->
+    <!--                                                                      -->
+    <!--   xpoweredBy          Determines whether X-Powered-By response       -->
+    <!--                       header is added by generated servlet  [false]  -->
+    <!--                                                                      -->
+    <!-- If you wish to use Jikes to compile JSP pages:                       -->
+    <!--   Set the init parameter "compiler" to "jikes".  Define              -->
+    <!--   the property "-Dbuild.compiler.emacs=true" when starting Jetty     -->
+    <!--   to cause Jikes to emit error messages in a format compatible with  -->
+    <!--   Jasper.                                                            -->
+    <!--   If you get an error reporting that jikes can't use UTF-8 encoding, -->
+    <!--   try setting the init parameter "javaEncoding" to "ISO-8859-1".     -->
+    <!-- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -  -->
+    <servlet id="jsp">
+        <servlet-name>jsp</servlet-name>
+        <servlet-class>org.apache.jasper.servlet.JspServlet</servlet-class>
+        <init-param>
+            <param-name>logVerbosityLevel</param-name>
+            <param-value>DEBUG</param-value>
+        </init-param>
+        <init-param>
+            <param-name>fork</param-name>
+            <param-value>false</param-value>
+        </init-param>
+        <init-param>
+            <param-name>keepgenerated</param-name>
+            <param-value>true</param-value>
+        </init-param>
+        <init-param>
+            <param-name>development</param-name>
+            <param-value>false</param-value>
+        </init-param>
+        <init-param>
+            <param-name>xpoweredBy</param-name>
+            <param-value>false</param-value>
+        </init-param>
+        <init-param>
+            <param-name>compilerTargetVM</param-name>
+            <param-value>1.7</param-value>
+        </init-param>
+        <init-param>
+            <param-name>compilerSourceVM</param-name>
+            <param-value>1.7</param-value>
+        </init-param>
+        <!--  
+        <init-param>
+            <param-name>classpath</param-name>
+            <param-value>?</param-value>
+        </init-param>
+        -->
+        <load-on-startup>0</load-on-startup>
+    </servlet>
+
+    <servlet-mapping>
+        <servlet-name>jsp</servlet-name>
+        <url-pattern>*.jsp</url-pattern>
+        <url-pattern>*.jspf</url-pattern>
+        <url-pattern>*.jspx</url-pattern>
+        <url-pattern>*.xsp</url-pattern>
+        <url-pattern>*.JSP</url-pattern>
+        <url-pattern>*.JSPF</url-pattern>
+        <url-pattern>*.JSPX</url-pattern>
+        <url-pattern>*.XSP</url-pattern>
+    </servlet-mapping>
+
+
+    <!-- ==================================================================== -->
+    <session-config>
+        <session-timeout>30</session-timeout>
+    </session-config>
+
+    <!-- ==================================================================== -->
+    <!-- Default MIME mappings                                                -->
+    <!-- The default MIME mappings are provided by the mime.properties        -->
+    <!-- resource in the org.eclipse.jetty.server.jar file.  Additional or modified  -->
+    <!-- mappings may be specified here                                       -->
+    <!-- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -  -->
+    <!-- UNCOMMENT TO ACTIVATE
+    <mime-mapping>
+      <extension>mysuffix</extension>
+      <mime-type>mymime/type</mime-type>
+    </mime-mapping>
+    -->
+
+    <!-- ==================================================================== -->
+    <welcome-file-list>
+        <welcome-file>index.html</welcome-file>
+        <welcome-file>index.htm</welcome-file>
+        <welcome-file>index.jsp</welcome-file>
+    </welcome-file-list>
+
+    <!-- ==================================================================== -->
+    <locale-encoding-mapping-list>
+        <locale-encoding-mapping>
+            <locale>ar</locale>
+            <encoding>ISO-8859-6</encoding>
+        </locale-encoding-mapping>
+        <locale-encoding-mapping>
+            <locale>be</locale>
+            <encoding>ISO-8859-5</encoding>
+        </locale-encoding-mapping>
+        <locale-encoding-mapping>
+            <locale>bg</locale>
+            <encoding>ISO-8859-5</encoding>
+        </locale-encoding-mapping>
+        <locale-encoding-mapping>
+            <locale>ca</locale>
+            <encoding>ISO-8859-1</encoding>
+        </locale-encoding-mapping>
+        <locale-encoding-mapping>
+            <locale>cs</locale>
+            <encoding>ISO-8859-2</encoding>
+        </locale-encoding-mapping>
+        <locale-encoding-mapping>
+            <locale>da</locale>
+            <encoding>ISO-8859-1</encoding>
+        </locale-encoding-mapping>
+        <locale-encoding-mapping>
+            <locale>de</locale>
+            <encoding>ISO-8859-1</encoding>
+        </locale-encoding-mapping>
+        <locale-encoding-mapping>
+            <locale>el</locale>
+            <encoding>ISO-8859-7</encoding>
+        </locale-encoding-mapping>
+        <locale-encoding-mapping>
+            <locale>en</locale>
+            <encoding>ISO-8859-1</encoding>
+        </locale-encoding-mapping>
+        <locale-encoding-mapping>
+            <locale>es</locale>
+            <encoding>ISO-8859-1</encoding>
+        </locale-encoding-mapping>
+        <locale-encoding-mapping>
+            <locale>et</locale>
+            <encoding>ISO-8859-1</encoding>
+        </locale-encoding-mapping>
+        <locale-encoding-mapping>
+            <locale>fi</locale>
+            <encoding>ISO-8859-1</encoding>
+        </locale-encoding-mapping>
+        <locale-encoding-mapping>
+            <locale>fr</locale>
+            <encoding>ISO-8859-1</encoding>
+        </locale-encoding-mapping>
+        <locale-encoding-mapping>
+            <locale>hr</locale>
+            <encoding>ISO-8859-2</encoding>
+        </locale-encoding-mapping>
+        <locale-encoding-mapping>
+            <locale>hu</locale>
+            <encoding>ISO-8859-2</encoding>
+        </locale-encoding-mapping>
+        <locale-encoding-mapping>
+            <locale>is</locale>
+            <encoding>ISO-8859-1</encoding>
+        </locale-encoding-mapping>
+        <locale-encoding-mapping>
+            <locale>it</locale>
+            <encoding>ISO-8859-1</encoding>
+        </locale-encoding-mapping>
+        <locale-encoding-mapping>
+            <locale>iw</locale>
+            <encoding>ISO-8859-8</encoding>
+        </locale-encoding-mapping>
+        <locale-encoding-mapping>
+            <locale>ja</locale>
+            <encoding>Shift_JIS</encoding>
+        </locale-encoding-mapping>
+        <locale-encoding-mapping>
+            <locale>ko</locale>
+            <encoding>EUC-KR</encoding>
+        </locale-encoding-mapping>
+        <locale-encoding-mapping>
+            <locale>lt</locale>
+            <encoding>ISO-8859-2</encoding>
+        </locale-encoding-mapping>
+        <locale-encoding-mapping>
+            <locale>lv</locale>
+            <encoding>ISO-8859-2</encoding>
+        </locale-encoding-mapping>
+        <locale-encoding-mapping>
+            <locale>mk</locale>
+            <encoding>ISO-8859-5</encoding>
+        </locale-encoding-mapping>
+        <locale-encoding-mapping>
+            <locale>nl</locale>
+            <encoding>ISO-8859-1</encoding>
+        </locale-encoding-mapping>
+        <locale-encoding-mapping>
+            <locale>no</locale>
+            <encoding>ISO-8859-1</encoding>
+        </locale-encoding-mapping>
+        <locale-encoding-mapping>
+            <locale>pl</locale>
+            <encoding>ISO-8859-2</encoding>
+        </locale-encoding-mapping>
+        <locale-encoding-mapping>
+            <locale>pt</locale>
+            <encoding>ISO-8859-1</encoding>
+        </locale-encoding-mapping>
+        <locale-encoding-mapping>
+            <locale>ro</locale>
+            <encoding>ISO-8859-2</encoding>
+        </locale-encoding-mapping>
+        <locale-encoding-mapping>
+            <locale>ru</locale>
+            <encoding>ISO-8859-5</encoding>
+        </locale-encoding-mapping>
+        <locale-encoding-mapping>
+            <locale>sh</locale>
+            <encoding>ISO-8859-5</encoding>
+        </locale-encoding-mapping>
+        <locale-encoding-mapping>
+            <locale>sk</locale>
+            <encoding>ISO-8859-2</encoding>
+        </locale-encoding-mapping>
+        <locale-encoding-mapping>
+            <locale>sl</locale>
+            <encoding>ISO-8859-2</encoding>
+        </locale-encoding-mapping>
+        <locale-encoding-mapping>
+            <locale>sq</locale>
+            <encoding>ISO-8859-2</encoding>
+        </locale-encoding-mapping>
+        <locale-encoding-mapping>
+            <locale>sr</locale>
+            <encoding>ISO-8859-5</encoding>
+        </locale-encoding-mapping>
+        <locale-encoding-mapping>
+            <locale>sv</locale>
+            <encoding>ISO-8859-1</encoding>
+        </locale-encoding-mapping>
+        <locale-encoding-mapping>
+            <locale>tr</locale>
+            <encoding>ISO-8859-9</encoding>
+        </locale-encoding-mapping>
+        <locale-encoding-mapping>
+            <locale>uk</locale>
+            <encoding>ISO-8859-5</encoding>
+        </locale-encoding-mapping>
+        <locale-encoding-mapping>
+            <locale>zh</locale>
+            <encoding>GB2312</encoding>
+        </locale-encoding-mapping>
+        <locale-encoding-mapping>
+            <locale>zh_TW</locale>
+            <encoding>Big5</encoding>
+        </locale-encoding-mapping>
+    </locale-encoding-mapping-list>
+
+    <security-constraint>
+        <web-resource-collection>
+            <web-resource-name>Disable TRACE</web-resource-name>
+            <url-pattern>/</url-pattern>
+            <http-method>TRACE</http-method>
+        </web-resource-collection>
+        <auth-constraint/>
+    </security-constraint>
+    <security-constraint>
+        <web-resource-collection>
+            <web-resource-name>Enable everything but TRACE</web-resource-name>
+            <url-pattern>/</url-pattern>
+            <http-method-omission>TRACE</http-method-omission>
+        </web-resource-collection>
+    </security-constraint>
+
+</web-app>
+

http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/6f26290d/nifi-registry-core/nifi-registry-properties/pom.xml
----------------------------------------------------------------------
diff --git a/nifi-registry-core/nifi-registry-properties/pom.xml b/nifi-registry-core/nifi-registry-properties/pom.xml
new file mode 100644
index 0000000..a6d6422
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-properties/pom.xml
@@ -0,0 +1,73 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+  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.
+-->
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+    <modelVersion>4.0.0</modelVersion>
+    <parent>
+        <groupId>org.apache.nifi.registry</groupId>
+        <artifactId>nifi-registry-core</artifactId>
+        <version>0.3.0-SNAPSHOT</version>
+    </parent>
+    <artifactId>nifi-registry-properties</artifactId>
+    <packaging>jar</packaging>
+
+    <build>
+        <plugins>
+            <plugin>
+                <groupId>org.codehaus.gmavenplus</groupId>
+                <artifactId>gmavenplus-plugin</artifactId>
+                <version>1.5</version>
+                <executions>
+                    <execution>
+                        <goals>
+                            <goal>addTestSources</goal>
+                            <goal>testCompile</goal>
+                        </goals>
+                    </execution>
+                </executions>
+            </plugin>
+        </plugins>
+    </build>
+
+    <dependencies>
+        <dependency>
+            <groupId>org.apache.commons</groupId>
+            <artifactId>commons-lang3</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>org.bouncycastle</groupId>
+            <artifactId>bcprov-jdk15on</artifactId>
+            <version>1.55</version>
+        </dependency>
+        <dependency>
+            <groupId>org.codehaus.groovy</groupId>
+            <artifactId>groovy-all</artifactId>
+            <version>2.4.12</version>
+            <scope>test</scope>
+        </dependency>
+        <dependency>
+            <groupId>cglib</groupId>
+            <artifactId>cglib-nodep</artifactId>
+            <version>2.2.2</version>
+            <scope>test</scope>
+        </dependency>
+        <dependency>
+            <groupId>org.slf4j</groupId>
+            <artifactId>slf4j-simple</artifactId>
+            <version>1.7.12</version>
+            <scope>test</scope>
+        </dependency>
+    </dependencies>
+</project>

http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/6f26290d/nifi-registry-core/nifi-registry-properties/src/main/java/org/apache/nifi/registry/properties/AESSensitivePropertyProvider.java
----------------------------------------------------------------------
diff --git a/nifi-registry-core/nifi-registry-properties/src/main/java/org/apache/nifi/registry/properties/AESSensitivePropertyProvider.java b/nifi-registry-core/nifi-registry-properties/src/main/java/org/apache/nifi/registry/properties/AESSensitivePropertyProvider.java
new file mode 100644
index 0000000..b7d1d2e
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-properties/src/main/java/org/apache/nifi/registry/properties/AESSensitivePropertyProvider.java
@@ -0,0 +1,265 @@
+/*
+ * 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.properties;
+
+import org.apache.commons.lang3.StringUtils;
+import org.bouncycastle.jce.provider.BouncyCastleProvider;
+import org.bouncycastle.util.encoders.Base64;
+import org.bouncycastle.util.encoders.DecoderException;
+import org.bouncycastle.util.encoders.EncoderException;
+import org.bouncycastle.util.encoders.Hex;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import javax.crypto.BadPaddingException;
+import javax.crypto.Cipher;
+import javax.crypto.IllegalBlockSizeException;
+import javax.crypto.NoSuchPaddingException;
+import javax.crypto.SecretKey;
+import javax.crypto.spec.IvParameterSpec;
+import javax.crypto.spec.SecretKeySpec;
+import java.nio.charset.StandardCharsets;
+import java.security.InvalidAlgorithmParameterException;
+import java.security.InvalidKeyException;
+import java.security.NoSuchAlgorithmException;
+import java.security.NoSuchProviderException;
+import java.security.SecureRandom;
+import java.security.Security;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.stream.Collectors;
+
+public class AESSensitivePropertyProvider implements SensitivePropertyProvider {
+    private static final Logger logger = LoggerFactory.getLogger(AESSensitivePropertyProvider.class);
+
+    private static final String IMPLEMENTATION_NAME = "AES Sensitive Property Provider";
+    private static final String IMPLEMENTATION_KEY = "aes/gcm/";
+    private static final String ALGORITHM = "AES/GCM/NoPadding";
+    private static final String PROVIDER = "BC";
+    private static final String DELIMITER = "||"; // "|" is not a valid Base64 character, so ensured not to be present in cipher text
+    private static final int IV_LENGTH = 12;
+    private static final int MIN_CIPHER_TEXT_LENGTH = IV_LENGTH * 4 / 3 + DELIMITER.length() + 1;
+
+    private Cipher cipher;
+    private final SecretKey key;
+
+    public AESSensitivePropertyProvider(String keyHex) throws NoSuchPaddingException, NoSuchAlgorithmException, NoSuchProviderException {
+        byte[] key = validateKey(keyHex);
+
+        try {
+            Security.addProvider(new BouncyCastleProvider());
+            cipher = Cipher.getInstance(ALGORITHM, PROVIDER);
+            // Only store the key if the cipher was initialized successfully
+            this.key = new SecretKeySpec(key, "AES");
+        } catch (NoSuchAlgorithmException | NoSuchProviderException | NoSuchPaddingException e) {
+            logger.error("Encountered an error initializing the {}: {}", IMPLEMENTATION_NAME, e.getMessage());
+            throw new SensitivePropertyProtectionException("Error initializing the protection cipher", e);
+        }
+    }
+
+    private byte[] validateKey(String keyHex) {
+        if (keyHex == null || StringUtils.isBlank(keyHex)) {
+            throw new SensitivePropertyProtectionException("The key cannot be empty");
+        }
+        keyHex = formatHexKey(keyHex);
+        if (!isHexKeyValid(keyHex)) {
+            throw new SensitivePropertyProtectionException("The key must be a valid hexadecimal key");
+        }
+        byte[] key = Hex.decode(keyHex);
+        final List<Integer> validKeyLengths = getValidKeyLengths();
+        if (!validKeyLengths.contains(key.length * 8)) {
+            List<String> validKeyLengthsAsStrings = validKeyLengths.stream().map(i -> Integer.toString(i)).collect(Collectors.toList());
+            throw new SensitivePropertyProtectionException("The key (" + key.length * 8 + " bits) must be a valid length: " + StringUtils.join(validKeyLengthsAsStrings, ", "));
+        }
+        return key;
+    }
+
+    public AESSensitivePropertyProvider(byte[] key) throws NoSuchPaddingException, NoSuchAlgorithmException, NoSuchProviderException {
+        this(key == null ? "" : Hex.toHexString(key));
+    }
+
+    private static String formatHexKey(String input) {
+        if (input == null || StringUtils.isBlank(input)) {
+            return "";
+        }
+        return input.replaceAll("[^0-9a-fA-F]", "").toLowerCase();
+    }
+
+    private static boolean isHexKeyValid(String key) {
+        if (key == null || StringUtils.isBlank(key)) {
+            return false;
+        }
+        // Key length is in "nibbles" (i.e. one hex char = 4 bits)
+        return getValidKeyLengths().contains(key.length() * 4) && key.matches("^[0-9a-fA-F]*$");
+    }
+
+    private static List<Integer> getValidKeyLengths() {
+        List<Integer> validLengths = new ArrayList<>();
+        validLengths.add(128);
+
+        try {
+            if (Cipher.getMaxAllowedKeyLength("AES") > 128) {
+                validLengths.add(192);
+                validLengths.add(256);
+            } else {
+                logger.warn("JCE Unlimited Strength Cryptography Jurisdiction policies are not available, so the max key length is 128 bits");
+            }
+        } catch (NoSuchAlgorithmException e) {
+            logger.warn("Encountered an error determining the max key length", e);
+        }
+
+        return validLengths;
+    }
+
+    /**
+     * Returns the name of the underlying implementation.
+     *
+     * @return the name of this sensitive property provider
+     */
+    @Override
+    public String getName() {
+        return IMPLEMENTATION_NAME;
+    }
+
+    /**
+     * Returns the key used to identify the provider implementation in {@code nifi.properties}.
+     *
+     * @return the key to persist in the sibling property
+     */
+    @Override
+    public String getIdentifierKey() {
+        return IMPLEMENTATION_KEY + getKeySize(Hex.toHexString(key.getEncoded()));
+    }
+
+    private int getKeySize(String key) {
+        if (StringUtils.isBlank(key)) {
+            return 0;
+        } else {
+            // A key in hexadecimal format has one char per nibble (4 bits)
+            return formatHexKey(key).length() * 4;
+        }
+    }
+
+    /**
+     * Returns the encrypted cipher text.
+     *
+     * @param unprotectedValue the sensitive value
+     * @return the value to persist in the {@code nifi.properties} file
+     * @throws SensitivePropertyProtectionException if there is an exception encrypting the value
+     */
+    @Override
+    public String protect(String unprotectedValue) throws SensitivePropertyProtectionException {
+        if (unprotectedValue == null || unprotectedValue.trim().length() == 0) {
+            throw new IllegalArgumentException("Cannot encrypt an empty value");
+        }
+
+        // Generate IV
+        byte[] iv = generateIV();
+        if (iv.length < IV_LENGTH) {
+            throw new IllegalArgumentException("The IV (" + iv.length + " bytes) must be at least " + IV_LENGTH + " bytes");
+        }
+
+        try {
+            // Initialize cipher for encryption
+            cipher.init(Cipher.ENCRYPT_MODE, this.key, new IvParameterSpec(iv));
+
+            byte[] plainBytes = unprotectedValue.getBytes(StandardCharsets.UTF_8);
+            byte[] cipherBytes = cipher.doFinal(plainBytes);
+            logger.info(getName() + " encrypted a sensitive value successfully");
+            return base64Encode(iv) + DELIMITER + base64Encode(cipherBytes);
+            // return Base64.toBase64String(iv) + DELIMITER + Base64.toBase64String(cipherBytes);
+        } catch (BadPaddingException | IllegalBlockSizeException | EncoderException | InvalidAlgorithmParameterException | InvalidKeyException e) {
+            final String msg = "Error encrypting a protected value";
+            logger.error(msg, e);
+            throw new SensitivePropertyProtectionException(msg, e);
+        }
+    }
+
+    private String base64Encode(byte[] input) {
+        return Base64.toBase64String(input).replaceAll("=", "");
+    }
+
+    /**
+     * Generates a new random IV of 12 bytes using {@link SecureRandom}.
+     *
+     * @return the IV
+     */
+    private byte[] generateIV() {
+        byte[] iv = new byte[IV_LENGTH];
+        new SecureRandom().nextBytes(iv);
+        return iv;
+    }
+
+    /**
+     * Returns the decrypted plaintext.
+     *
+     * @param protectedValue the cipher text read from the {@code nifi.properties} file
+     * @return the raw value to be used by the application
+     * @throws SensitivePropertyProtectionException if there is an error decrypting the cipher text
+     */
+    @Override
+    public String unprotect(String protectedValue) throws SensitivePropertyProtectionException {
+        if (protectedValue == null || protectedValue.trim().length() < MIN_CIPHER_TEXT_LENGTH) {
+            throw new IllegalArgumentException("Cannot decrypt a cipher text shorter than " + MIN_CIPHER_TEXT_LENGTH + " chars");
+        }
+
+        if (!protectedValue.contains(DELIMITER)) {
+            throw new IllegalArgumentException("The cipher text does not contain the delimiter " + DELIMITER + " -- it should be of the form Base64(IV) || Base64(cipherText)");
+        }
+
+        protectedValue = protectedValue.trim();
+
+        final String IV_B64 = protectedValue.substring(0, protectedValue.indexOf(DELIMITER));
+        byte[] iv = Base64.decode(IV_B64);
+        if (iv.length < IV_LENGTH) {
+            throw new IllegalArgumentException("The IV (" + iv.length + " bytes) must be at least " + IV_LENGTH + " bytes");
+        }
+
+        String CIPHERTEXT_B64 = protectedValue.substring(protectedValue.indexOf(DELIMITER) + 2);
+
+        // Restore the = padding if necessary to reconstitute the GCM MAC check
+        if (CIPHERTEXT_B64.length() % 4 != 0) {
+            final int paddedLength = CIPHERTEXT_B64.length() + 4 - (CIPHERTEXT_B64.length() % 4);
+            CIPHERTEXT_B64 = StringUtils.rightPad(CIPHERTEXT_B64, paddedLength, '=');
+        }
+
+        try {
+            byte[] cipherBytes = Base64.decode(CIPHERTEXT_B64);
+
+            cipher.init(Cipher.DECRYPT_MODE, this.key, new IvParameterSpec(iv));
+            byte[] plainBytes = cipher.doFinal(cipherBytes);
+            logger.debug(getName() + " decrypted a sensitive value successfully");
+            return new String(plainBytes, StandardCharsets.UTF_8);
+        } catch (BadPaddingException | IllegalBlockSizeException | DecoderException | InvalidAlgorithmParameterException | InvalidKeyException e) {
+            final String msg = "Error decrypting a protected value";
+            logger.error(msg, e);
+            throw new SensitivePropertyProtectionException(msg, e);
+        }
+    }
+
+    public static int getIvLength() {
+        return IV_LENGTH;
+    }
+
+    public static int getMinCipherTextLength() {
+        return MIN_CIPHER_TEXT_LENGTH;
+    }
+
+    public static String getDelimiter() {
+        return DELIMITER;
+    }
+}

http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/6f26290d/nifi-registry-core/nifi-registry-properties/src/main/java/org/apache/nifi/registry/properties/AESSensitivePropertyProviderFactory.java
----------------------------------------------------------------------
diff --git a/nifi-registry-core/nifi-registry-properties/src/main/java/org/apache/nifi/registry/properties/AESSensitivePropertyProviderFactory.java b/nifi-registry-core/nifi-registry-properties/src/main/java/org/apache/nifi/registry/properties/AESSensitivePropertyProviderFactory.java
new file mode 100644
index 0000000..5c24a73
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-properties/src/main/java/org/apache/nifi/registry/properties/AESSensitivePropertyProviderFactory.java
@@ -0,0 +1,54 @@
+/*
+ * 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.properties;
+
+import org.apache.commons.lang3.StringUtils;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import javax.crypto.NoSuchPaddingException;
+import java.security.NoSuchAlgorithmException;
+import java.security.NoSuchProviderException;
+
+public class AESSensitivePropertyProviderFactory implements SensitivePropertyProviderFactory {
+    private static final Logger logger = LoggerFactory.getLogger(AESSensitivePropertyProviderFactory.class);
+
+    private String keyHex;
+
+    public AESSensitivePropertyProviderFactory(String keyHex) {
+        this.keyHex = keyHex;
+    }
+
+    public SensitivePropertyProvider getProvider() throws SensitivePropertyProtectionException {
+        try {
+            if (keyHex != null && !StringUtils.isBlank(keyHex)) {
+                return new AESSensitivePropertyProvider(keyHex);
+            } else {
+                throw new SensitivePropertyProtectionException("The provider factory cannot generate providers without a key");
+            }
+        } catch (NoSuchAlgorithmException | NoSuchProviderException | NoSuchPaddingException e) {
+            String msg = "Error creating AES Sensitive Property Provider";
+            logger.warn(msg, e);
+            throw new SensitivePropertyProtectionException(msg, e);
+        }
+    }
+
+    @Override
+    public String toString() {
+        return "SensitivePropertyProviderFactory for creating AESSensitivePropertyProviders";
+    }
+}

http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/6f26290d/nifi-registry-core/nifi-registry-properties/src/main/java/org/apache/nifi/registry/properties/MultipleSensitivePropertyProtectionException.java
----------------------------------------------------------------------
diff --git a/nifi-registry-core/nifi-registry-properties/src/main/java/org/apache/nifi/registry/properties/MultipleSensitivePropertyProtectionException.java b/nifi-registry-core/nifi-registry-properties/src/main/java/org/apache/nifi/registry/properties/MultipleSensitivePropertyProtectionException.java
new file mode 100644
index 0000000..df4047f
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-properties/src/main/java/org/apache/nifi/registry/properties/MultipleSensitivePropertyProtectionException.java
@@ -0,0 +1,129 @@
+/*
+ * 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.properties;
+
+import org.apache.commons.lang3.StringUtils;
+
+import java.util.Collection;
+import java.util.HashSet;
+import java.util.Set;
+
+public class MultipleSensitivePropertyProtectionException extends SensitivePropertyProtectionException {
+
+    private Set<String> failedKeys;
+
+    /**
+     * Constructs a new throwable with {@code null} as its detail message.
+     * The cause is not initialized, and may subsequently be initialized by a
+     * call to {@link #initCause}.
+     * <p>
+     * <p>The {@link #fillInStackTrace()} method is called to initialize
+     * the stack trace data in the newly created throwable.
+     */
+    public MultipleSensitivePropertyProtectionException() {
+    }
+
+    /**
+     * Constructs a new throwable with the specified detail message.  The
+     * cause is not initialized, and may subsequently be initialized by
+     * a call to {@link #initCause}.
+     * <p>
+     * <p>The {@link #fillInStackTrace()} method is called to initialize
+     * the stack trace data in the newly created throwable.
+     *
+     * @param message the detail message. The detail message is saved for
+     *                later retrieval by the {@link #getMessage()} method.
+     */
+    public MultipleSensitivePropertyProtectionException(String message) {
+        super(message);
+    }
+
+    /**
+     * Constructs a new throwable with the specified detail message and
+     * cause.  <p>Note that the detail message associated with
+     * {@code cause} is <i>not</i> automatically incorporated in
+     * this throwable's detail message.
+     * <p>
+     * <p>The {@link #fillInStackTrace()} method is called to initialize
+     * the stack trace data in the newly created throwable.
+     *
+     * @param message the detail message (which is saved for later retrieval
+     *                by the {@link #getMessage()} method).
+     * @param cause   the cause (which is saved for later retrieval by the
+     *                {@link #getCause()} method).  (A {@code null} value is
+     *                permitted, and indicates that the cause is nonexistent or
+     *                unknown.)
+     * @since 1.4
+     */
+    public MultipleSensitivePropertyProtectionException(String message, Throwable cause) {
+        super(message, cause);
+    }
+
+    /**
+     * Constructs a new throwable with the specified cause and a detail
+     * message of {@code (cause==null ? null : cause.toString())} (which
+     * typically contains the class and detail message of {@code cause}).
+     * This constructor is useful for throwables that are little more than
+     * wrappers for other throwables (for example, PrivilegedActionException).
+     * <p>
+     * <p>The {@link #fillInStackTrace()} method is called to initialize
+     * the stack trace data in the newly created throwable.
+     *
+     * @param cause the cause (which is saved for later retrieval by the
+     *              {@link #getCause()} method).  (A {@code null} value is
+     *              permitted, and indicates that the cause is nonexistent or
+     *              unknown.)
+     * @since 1.4
+     */
+    public MultipleSensitivePropertyProtectionException(Throwable cause) {
+        super(cause);
+    }
+
+    /**
+     * Constructs a new exception with the provided message and a unique set of the keys that caused the error.
+     *
+     * @param message    the message
+     * @param failedKeys any failed keys
+     */
+    public MultipleSensitivePropertyProtectionException(String message, Collection<String> failedKeys) {
+        this(message, failedKeys, null);
+    }
+
+    /**
+     * Constructs a new exception with the provided message and a unique set of the keys that caused the error.
+     *
+     * @param message    the message
+     * @param failedKeys any failed keys
+     * @param cause      the cause (which is saved for later retrieval by the
+     *                   {@link #getCause()} method).  (A {@code null} value is
+     *                   permitted, and indicates that the cause is nonexistent or
+     *                   unknown.)
+     */
+    public MultipleSensitivePropertyProtectionException(String message, Collection<String> failedKeys, Throwable cause) {
+        super(message, cause);
+        this.failedKeys = new HashSet<>(failedKeys);
+    }
+
+    public Set<String> getFailedKeys() {
+        return this.failedKeys;
+    }
+
+    @Override
+    public String toString() {
+        return "SensitivePropertyProtectionException for [" + StringUtils.join(this.failedKeys, ", ") + "]: " + getLocalizedMessage();
+    }
+}


[22/51] [partial] nifi-registry git commit: NIFIREG-201 Refactoring project structure to better isolate extensions

Posted by kd...@apache.org.
http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/6f26290d/nifi-registry-core/nifi-registry-resources/src/main/resources/bin/run-nifi-registry.bat
----------------------------------------------------------------------
diff --git a/nifi-registry-core/nifi-registry-resources/src/main/resources/bin/run-nifi-registry.bat b/nifi-registry-core/nifi-registry-resources/src/main/resources/bin/run-nifi-registry.bat
new file mode 100644
index 0000000..c8d6541
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-resources/src/main/resources/bin/run-nifi-registry.bat
@@ -0,0 +1,50 @@
+@echo off
+rem
+rem    Licensed to the Apache Software Foundation (ASF) under one or more
+rem    contributor license agreements.  See the NOTICE file distributed with
+rem    this work for additional information regarding copyright ownership.
+rem    The ASF licenses this file to You under the Apache License, Version 2.0
+rem    (the "License"); you may not use this file except in compliance with
+rem    the License.  You may obtain a copy of the License at
+rem
+rem       http://www.apache.org/licenses/LICENSE-2.0
+rem
+rem    Unless required by applicable law or agreed to in writing, software
+rem    distributed under the License is distributed on an "AS IS" BASIS,
+rem    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+rem    See the License for the specific language governing permissions and
+rem    limitations under the License.
+rem
+
+rem Use JAVA_HOME if it's set; otherwise, just use java
+
+if "%JAVA_HOME%" == "" goto noJavaHome
+if not exist "%JAVA_HOME%\bin\java.exe" goto noJavaHome
+set JAVA_EXE=%JAVA_HOME%\bin\java.exe
+goto startNiFiRegistry
+
+:noJavaHome
+echo The JAVA_HOME environment variable is not defined correctly.
+echo Instead the PATH will be used to find the java executable.
+echo.
+set JAVA_EXE=java
+goto startNiFiRegistry
+
+:startNiFiRegistry
+set NIFI_REGISTRY_ROOT=%~dp0..
+pushd "%NIFI_REGISTRY_ROOT%\"
+set LIB_DIR=%NIFI_REGISTRY_ROOT%\lib
+set SHARED_DIR=%NIFI_REGISTRY_ROOT%\lib\shared
+set BOOTSTRAP_DIR=%NIFI_REGISTRY_ROOT%\lib\bootstrap
+set CONF_DIR=%NIFI_REGISTRY_ROOT%\conf
+
+set BOOTSTRAP_CONF_FILE=%CONF_DIR%\bootstrap.conf
+set JAVA_ARGS=-Dorg.apache.nifi.registry.bootstrap.config.file=%BOOTSTRAP_CONF_FILE%
+
+SET JAVA_PARAMS=-cp %CONF_DIR%;%LIB_DIR%\*;%SHARED_DIR%\*;%BOOTSTRAP_DIR%\* -Xms512m -Xmx1024m %JAVA_ARGS% org.apache.nifi.registry.NiFiRegistry
+set BOOTSTRAP_ACTION=run
+
+echo cmd.exe /C "%JAVA_EXE%" %JAVA_PARAMS% %BOOTSTRAP_ACTION%
+cmd.exe /C "%JAVA_EXE%" %JAVA_PARAMS% %BOOTSTRAP_ACTION%
+
+popd

http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/6f26290d/nifi-registry-core/nifi-registry-resources/src/main/resources/bin/status-nifi-registry.bat
----------------------------------------------------------------------
diff --git a/nifi-registry-core/nifi-registry-resources/src/main/resources/bin/status-nifi-registry.bat b/nifi-registry-core/nifi-registry-resources/src/main/resources/bin/status-nifi-registry.bat
new file mode 100644
index 0000000..30a29a0
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-resources/src/main/resources/bin/status-nifi-registry.bat
@@ -0,0 +1,49 @@
+@echo off
+rem
+rem    Licensed to the Apache Software Foundation (ASF) under one or more
+rem    contributor license agreements.  See the NOTICE file distributed with
+rem    this work for additional information regarding copyright ownership.
+rem    The ASF licenses this file to You under the Apache License, Version 2.0
+rem    (the "License"); you may not use this file except in compliance with
+rem    the License.  You may obtain a copy of the License at
+rem
+rem       http://www.apache.org/licenses/LICENSE-2.0
+rem
+rem    Unless required by applicable law or agreed to in writing, software
+rem    distributed under the License is distributed on an "AS IS" BASIS,
+rem    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+rem    See the License for the specific language governing permissions and
+rem    limitations under the License.
+rem
+
+rem Use JAVA_HOME if it's set; otherwise, just use java
+
+if "%JAVA_HOME%" == "" goto noJavaHome
+if not exist "%JAVA_HOME%\bin\java.exe" goto noJavaHome
+set JAVA_EXE=%JAVA_HOME%\bin\java.exe
+goto startNiFiRegistry
+
+:noJavaHome
+echo The JAVA_HOME environment variable is not defined correctly.
+echo Instead the PATH will be used to find the java executable.
+echo.
+set JAVA_EXE=java
+goto startNiFiRegistry
+
+:startNiFiRegistry
+set NIFI_REGISTRY_ROOT=%~dp0..\
+pushd "%NIFI_REGISTRY_ROOT%"
+set LIB_DIR=%NIFI_REGISTRY_ROOT%\lib
+set SHARED_DIR=%NIFI_REGISTRY_ROOT%\lib\shared
+set BOOTSTRAP_DIR=%NIFI_REGISTRY_ROOT%\lib\bootstrap
+set CONF_DIR=conf
+
+set BOOTSTRAP_CONF_FILE=%CONF_DIR%\bootstrap.conf
+set JAVA_ARGS=-Dorg.apache.nifi.registry.bootstrap.config.file=%BOOTSTRAP_CONF_FILE%
+
+set JAVA_PARAMS=-cp %LIB_DIR%\*;%SHARED_DIR%\*;%BOOTSTRAP_DIR%\* -Xms12m -Xmx24m %JAVA_ARGS% org.apache.nifi.registry.NiFiRegistry
+set BOOTSTRAP_ACTION=status
+
+cmd.exe /C "%JAVA_EXE%" %JAVA_PARAMS% %BOOTSTRAP_ACTION%
+
+popd

http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/6f26290d/nifi-registry-core/nifi-registry-resources/src/main/resources/conf/authorizers.xml
----------------------------------------------------------------------
diff --git a/nifi-registry-core/nifi-registry-resources/src/main/resources/conf/authorizers.xml b/nifi-registry-core/nifi-registry-resources/src/main/resources/conf/authorizers.xml
new file mode 100644
index 0000000..772db61
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-resources/src/main/resources/conf/authorizers.xml
@@ -0,0 +1,256 @@
+<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
+<!--
+  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.
+-->
+<!--
+    This file lists the userGroupProviders, accessPolicyProviders, and authorizers to use when running securely. In order
+    to use a specific authorizer it must be configured here and its identifier must be specified in the nifi-registry.properties file.
+    If the authorizer is a managedAuthorizer, it may need to be configured with an accessPolicyProvider and an userGroupProvider.
+    This file allows for configuration of them, but they must be configured in order:
+
+    ...
+    all userGroupProviders
+    all accessPolicyProviders
+    all Authorizers
+    ...
+-->
+<authorizers>
+
+    <!--
+        The FileUserGroupProvider will provide support for managing users and groups which is backed by a file
+        on the local file system.
+
+        - Users File - The file where the FileUserGroupProvider will store users and groups.
+
+        - Initial User Identity [unique key] - The identity of a users and systems to seed the Users File. The name of
+            each property must be unique, for example: "Initial User Identity A", "Initial User Identity B",
+            "Initial User Identity C" or "Initial User Identity 1", "Initial User Identity 2", "Initial User Identity 3"
+
+            NOTE: Any identity mapping rules specified in nifi-registry.properties will also be applied to the user identities,
+            so the values should be the unmapped identities (i.e. full DN from a certificate).
+    -->
+    <userGroupProvider>
+        <identifier>file-user-group-provider</identifier>
+        <class>org.apache.nifi.registry.security.authorization.file.FileUserGroupProvider</class>
+        <property name="Users File">./conf/users.xml</property>
+        <property name="Initial User Identity 1"><!--CN=abc, OU=xyz--></property>
+    </userGroupProvider>
+
+    <!--
+        The LdapUserGroupProvider will retrieve users and groups from an LDAP server. The users and groups
+        are not configurable.
+
+        'Authentication Strategy' - How the connection to the LDAP server is authenticated. Possible
+            values are ANONYMOUS, SIMPLE, LDAPS, or START_TLS.
+
+        'Manager DN' - The DN of the manager that is used to bind to the LDAP server to search for users.
+        'Manager Password' - The password of the manager that is used to bind to the LDAP server to
+            search for users.
+
+        'TLS - Keystore' - Path to the Keystore that is used when connecting to LDAP using LDAPS or START_TLS.
+        'TLS - Keystore Password' - Password for the Keystore that is used when connecting to LDAP
+            using LDAPS or START_TLS.
+        'TLS - Keystore Type' - Type of the Keystore that is used when connecting to LDAP using
+            LDAPS or START_TLS (i.e. JKS or PKCS12).
+        'TLS - Truststore' - Path to the Truststore that is used when connecting to LDAP using LDAPS or START_TLS.
+        'TLS - Truststore Password' - Password for the Truststore that is used when connecting to
+            LDAP using LDAPS or START_TLS.
+        'TLS - Truststore Type' - Type of the Truststore that is used when connecting to LDAP using
+            LDAPS or START_TLS (i.e. JKS or PKCS12).
+        'TLS - Client Auth' - Client authentication policy when connecting to LDAP using LDAPS or START_TLS.
+            Possible values are REQUIRED, WANT, NONE.
+        'TLS - Protocol' - Protocol to use when connecting to LDAP using LDAPS or START_TLS. (i.e. TLS,
+            TLSv1.1, TLSv1.2, etc).
+        'TLS - Shutdown Gracefully' - Specifies whether the TLS should be shut down gracefully
+            before the target context is closed. Defaults to false.
+
+        'Referral Strategy' - Strategy for handling referrals. Possible values are FOLLOW, IGNORE, THROW.
+        'Connect Timeout' - Duration of connect timeout. (i.e. 10 secs).
+        'Read Timeout' - Duration of read timeout. (i.e. 10 secs).
+
+        'Url' - Space-separated list of URLs of the LDAP servers (i.e. ldap://<hostname>:<port>).
+        'Page Size' - Sets the page size when retrieving users and groups. If not specified, no paging is performed.
+        'Sync Interval' - Duration of time between syncing users and groups. (i.e. 30 mins).
+
+        'User Search Base' - Base DN for searching for users (i.e. ou=users,o=nifi). Required to search users.
+        'User Object Class' - Object class for identifying users (i.e. person). Required if searching users.
+        'User Search Scope' - Search scope for searching users (ONE_LEVEL, OBJECT, or SUBTREE). Required if searching users.
+        'User Search Filter' - Filter for searching for users against the 'User Search Base' (i.e. (memberof=cn=team1,ou=groups,o=nifi) ). Optional.
+        'User Identity Attribute' - Attribute to use to extract user identity (i.e. cn). Optional. If not set, the entire DN is used.
+        'User Group Name Attribute' - Attribute to use to define group membership (i.e. memberof). Optional. If not set
+            group membership will not be calculated through the users. Will rely on group membership being defined
+            through 'Group Member Attribute' if set. The value of this property is the name of the attribute in the user ldap entry that
+            associates them with a group. The value of that user attribute could be a dn or group name for instance. What value is expected
+            is configured in the 'User Group Name Attribute - Referenced Group Attribute'.
+        'User Group Name Attribute - Referenced Group Attribute' - If blank, the value of the attribute defined in 'User Group Name Attribute'
+            is expected to be the full dn of the group. If not blank, this property will define the attribute of the group ldap entry that
+            the value of the attribute defined in 'User Group Name Attribute' is referencing (i.e. name). Use of this property requires that
+            'Group Search Base' is also configured.
+
+        'Group Search Base' - Base DN for searching for groups (i.e. ou=groups,o=nifi). Required to search groups.
+        'Group Object Class' - Object class for identifying groups (i.e. groupOfNames). Required if searching groups.
+        'Group Search Scope' - Search scope for searching groups (ONE_LEVEL, OBJECT, or SUBTREE). Required if searching groups.
+        'Group Search Filter' - Filter for searching for groups against the 'Group Search Base'. Optional.
+        'Group Name Attribute' - Attribute to use to extract group name (i.e. cn). Optional. If not set, the entire DN is used.
+        'Group Member Attribute' - Attribute to use to define group membership (i.e. member). Optional. If not set
+            group membership will not be calculated through the groups. Will rely on group membership being defined
+            through 'User Group Name Attribute' if set. The value of this property is the name of the attribute in the group ldap entry that
+            associates them with a user. The value of that group attribute could be a dn or memberUid for instance. What value is expected
+            is configured in the 'Group Member Attribute - Referenced User Attribute'. (i.e. member: cn=User 1,ou=users,o=nifi-registry vs. memberUid: user1)
+        'Group Member Attribute - Referenced User Attribute' - If blank, the value of the attribute defined in 'Group Member Attribute'
+            is expected to be the full dn of the user. If not blank, this property will define the attribute of the user ldap entry that
+            the value of the attribute defined in 'Group Member Attribute' is referencing (i.e. uid). Use of this property requires that
+            'User Search Base' is also configured. (i.e. member: cn=User 1,ou=users,o=nifi-registry vs. memberUid: user1)
+
+        NOTE: Any identity mapping rules specified in nifi-registry.properties will also be applied to the user identities.
+            Group names are not mapped.
+    -->
+    <!-- To enable the ldap-user-group-provider remove 2 lines. This is 1 of 2.
+    <userGroupProvider>
+        <identifier>ldap-user-group-provider</identifier>
+        <class>org.apache.nifi.registry.security.ldap.tenants.LdapUserGroupProvider</class>
+        <property name="Authentication Strategy">START_TLS</property>
+
+        <property name="Manager DN"></property>
+        <property name="Manager Password"></property>
+
+        <property name="TLS - Keystore"></property>
+        <property name="TLS - Keystore Password"></property>
+        <property name="TLS - Keystore Type"></property>
+        <property name="TLS - Truststore"></property>
+        <property name="TLS - Truststore Password"></property>
+        <property name="TLS - Truststore Type"></property>
+        <property name="TLS - Client Auth"></property>
+        <property name="TLS - Protocol"></property>
+        <property name="TLS - Shutdown Gracefully"></property>
+
+        <property name="Referral Strategy">FOLLOW</property>
+        <property name="Connect Timeout">10 secs</property>
+        <property name="Read Timeout">10 secs</property>
+
+        <property name="Url"></property>
+        <property name="Page Size"></property>
+        <property name="Sync Interval">30 mins</property>
+
+        <property name="User Search Base"></property>
+        <property name="User Object Class">person</property>
+        <property name="User Search Scope">ONE_LEVEL</property>
+        <property name="User Search Filter"></property>
+        <property name="User Identity Attribute"></property>
+        <property name="User Group Name Attribute"></property>
+        <property name="User Group Name Attribute - Referenced Group Attribute"></property>
+
+        <property name="Group Search Base"></property>
+        <property name="Group Object Class">group</property>
+        <property name="Group Search Scope">ONE_LEVEL</property>
+        <property name="Group Search Filter"></property>
+        <property name="Group Name Attribute"></property>
+        <property name="Group Member Attribute"></property>
+        <property name="Group Member Attribute - Referenced User Attribute"></property>
+    </userGroupProvider>
+    To enable the ldap-user-group-provider remove 2 lines. This is 2 of 2. -->
+
+    <!--
+        The CompositeUserGroupProvider will provide support for retrieving users and groups from multiple sources.
+
+        - User Group Provider [unique key] - The identifier of user group providers to load from. The name of
+            each property must be unique, for example: "User Group Provider A", "User Group Provider B",
+            "User Group Provider C" or "User Group Provider 1", "User Group Provider 2", "User Group Provider 3"
+
+            NOTE: Any identity mapping rules specified in nifi-registry.properties are not applied in this implementation. This
+            behavior would need to be applied by the base implementation.
+    -->
+    <!-- To enable the composite-user-group-provider remove 2 lines. This is 1 of 2.
+    <userGroupProvider>
+        <identifier>composite-user-group-provider</identifier>
+        <class>org.apache.nifi.registry.security.authorization.CompositeUserGroupProvider</class>
+        <property name="User Group Provider 1"></property>
+    </userGroupProvider>
+    To enable the composite-user-group-provider remove 2 lines. This is 2 of 2. -->
+
+    <!--
+        The CompositeConfigurableUserGroupProvider will provide support for retrieving users and groups from multiple sources.
+        Additionally, a single configurable user group provider is required. Users from the configurable user group provider
+        are configurable, however users loaded from one of the User Group Provider [unique key] will not be.
+
+        - Configurable User Group Provider - A configurable user group provider.
+
+        - User Group Provider [unique key] - The identifier of user group providers to load from. The name of
+            each property must be unique, for example: "User Group Provider A", "User Group Provider B",
+            "User Group Provider C" or "User Group Provider 1", "User Group Provider 2", "User Group Provider 3"
+
+            NOTE: Any identity mapping rules specified in nifi-registry.properties are not applied in this implementation. This
+            behavior would need to be applied by the base implementation.
+    -->
+    <!-- To enable the composite-configurable-user-group-provider remove 2 lines. This is 1 of 2.
+    <userGroupProvider>
+        <identifier>composite-configurable-user-group-provider</identifier>
+        <class>org.apache.nifi.registry.security.authorization.CompositeConfigurableUserGroupProvider</class>
+        <property name="Configurable User Group Provider">file-user-group-provider</property>
+        <property name="User Group Provider 1"></property>
+    </userGroupProvider>
+    To enable the composite-configurable-user-group-provider remove 2 lines. This is 2 of 2. -->
+
+    <!--
+        The FileAccessPolicyProvider will provide support for managing access policies which is backed by a file
+        on the local file system.
+
+        - User Group Provider - The identifier for an User Group Provider defined above that will be used to access
+            users and groups for use in the managed access policies.
+
+        - Authorizations File - The file where the FileAccessPolicyProvider will store policies.
+
+        - Initial Admin Identity - The identity of an initial admin user that will be granted access to the UI and
+            given the ability to create additional users, groups, and policies. The value of this property could be
+            a DN when using certificates or LDAP. This property will only be used when there
+            are no other policies defined.
+
+            NOTE: Any identity mapping rules specified in nifi-registry.properties will also be applied to the initial admin identity,
+            so the value should be the unmapped identity. This identity must be found in the configured User Group Provider.
+
+        - NiFi Identity [unique key] - The identity of a NiFi node that will have access to this NiFi Registry and will be able
+            to act as a proxy on behalf of a NiFi Registry end user. A property should be created for the identity of every NiFi
+            node that needs to access this NiFi Registry. The name of each property must be unique, for example for three
+            NiFi clients:
+            "NiFi Identity A", "NiFi Identity B", "NiFi Identity C" or "NiFi Identity 1", "NiFi Identity 2", "NiFi Identity 3"
+
+            NOTE: Any identity mapping rules specified in nifi-registry.properties will also be applied to the nifi identities,
+            so the values should be the unmapped identities (i.e. full DN from a certificate). This identity must be found
+            in the configured User Group Provider.
+    -->
+    <accessPolicyProvider>
+        <identifier>file-access-policy-provider</identifier>
+        <class>org.apache.nifi.registry.security.authorization.file.FileAccessPolicyProvider</class>
+        <property name="User Group Provider">file-user-group-provider</property>
+        <property name="Authorizations File">./conf/authorizations.xml</property>
+        <property name="Initial Admin Identity"><!-- CN=abc, OU=xyz --></property>
+
+        <!--<property name="NiFi Identity 1"></property>-->
+    </accessPolicyProvider>
+
+    <!--
+        The StandardManagedAuthorizer. This authorizer implementation must be configured with the
+        Access Policy Provider which it will use to access and manage users, groups, and policies.
+        These users, groups, and policies will be used to make all access decisions during authorization
+        requests.
+
+        - Access Policy Provider - The identifier for an Access Policy Provider defined above.
+    -->
+    <authorizer>
+        <identifier>managed-authorizer</identifier>
+        <class>org.apache.nifi.registry.security.authorization.StandardManagedAuthorizer</class>
+        <property name="Access Policy Provider">file-access-policy-provider</property>
+    </authorizer>
+
+</authorizers>
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/6f26290d/nifi-registry-core/nifi-registry-resources/src/main/resources/conf/bootstrap.conf
----------------------------------------------------------------------
diff --git a/nifi-registry-core/nifi-registry-resources/src/main/resources/conf/bootstrap.conf b/nifi-registry-core/nifi-registry-resources/src/main/resources/conf/bootstrap.conf
new file mode 100644
index 0000000..637eb64
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-resources/src/main/resources/conf/bootstrap.conf
@@ -0,0 +1,48 @@
+#
+# 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.
+#
+
+# Java command to use when running nifi-registry
+java=java
+
+# Username to use when running nifi-registry. This value will be ignored on Windows.
+run.as=
+
+# Configure where nifi-registry's lib and conf directories live
+lib.dir=./lib
+conf.dir=./conf
+
+# How long to wait after telling nifi-registry to shutdown before explicitly killing the Process
+graceful.shutdown.seconds=20
+
+# Disable JSR 199 so that we can use JSP's without running a JDK
+java.arg.1=-Dorg.apache.jasper.compiler.disablejsr199=true
+
+# JVM memory settings
+java.arg.2=-Xms512m
+java.arg.3=-Xmx512m
+
+# Enable Remote Debugging
+#java.arg.debug=-agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=8000
+
+java.arg.4=-Djava.net.preferIPv4Stack=true
+
+# allowRestrictedHeaders is required for Cluster/Node communications to work properly
+java.arg.5=-Dsun.net.http.allowRestrictedHeaders=true
+java.arg.6=-Djava.protocol.handler.pkgs=sun.net.www.protocol
+
+# Master key in hexadecimal format for encrypted sensitive configuration values
+nifi.registry.bootstrap.sensitive.key=
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/6f26290d/nifi-registry-core/nifi-registry-resources/src/main/resources/conf/identity-providers.xml
----------------------------------------------------------------------
diff --git a/nifi-registry-core/nifi-registry-resources/src/main/resources/conf/identity-providers.xml b/nifi-registry-core/nifi-registry-resources/src/main/resources/conf/identity-providers.xml
new file mode 100644
index 0000000..1e8cf64
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-resources/src/main/resources/conf/identity-providers.xml
@@ -0,0 +1,106 @@
+<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
+<!--
+  ~ 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.
+  -->
+<!--
+    This file lists the identity providers to use when running securely. In order
+    to use a specific provider it must be configured here and its identifier
+    must be specified in the nifi-registry.properties file.
+-->
+<identityProviders>
+    <!--
+        Identity Provider for users logging in with username/password against an LDAP server.
+        
+        'Authentication Strategy' - How the connection to the LDAP server is authenticated. Possible
+            values are ANONYMOUS, SIMPLE, LDAPS, or START_TLS.
+        
+        'Manager DN' - The DN of the manager that is used to bind to the LDAP server to search for users.
+        'Manager Password' - The password of the manager that is used to bind to the LDAP server to
+            search for users.
+            
+        'TLS - Keystore' - Path to the Keystore that is used when connecting to LDAP using LDAPS or START_TLS.
+        'TLS - Keystore Password' - Password for the Keystore that is used when connecting to LDAP
+            using LDAPS or START_TLS.
+        'TLS - Keystore Type' - Type of the Keystore that is used when connecting to LDAP using
+            LDAPS or START_TLS (i.e. JKS or PKCS12).
+        'TLS - Truststore' - Path to the Truststore that is used when connecting to LDAP using LDAPS or START_TLS.
+        'TLS - Truststore Password' - Password for the Truststore that is used when connecting to
+            LDAP using LDAPS or START_TLS.
+        'TLS - Truststore Type' - Type of the Truststore that is used when connecting to LDAP using
+            LDAPS or START_TLS (i.e. JKS or PKCS12).
+        'TLS - Client Auth' - Client authentication policy when connecting to LDAP using LDAPS or START_TLS.
+            Possible values are REQUIRED, WANT, NONE.
+        'TLS - Protocol' - Protocol to use when connecting to LDAP using LDAPS or START_TLS. (i.e. TLS,
+            TLSv1.1, TLSv1.2, etc).
+        'TLS - Shutdown Gracefully' - Specifies whether the TLS should be shut down gracefully 
+            before the target context is closed. Defaults to false.
+            
+        'Referral Strategy' - Strategy for handling referrals. Possible values are FOLLOW, IGNORE, THROW.
+        'Connect Timeout' - Duration of connect timeout. (i.e. 10 secs).
+        'Read Timeout' - Duration of read timeout. (i.e. 10 secs).
+       
+        'Url' - Space-separated list of URLs of the LDAP servers (i.e. ldap://<hostname>:<port>).
+        'User Search Base' - Base DN for searching for users (i.e. CN=Users,DC=example,DC=com).
+        'User Search Filter' - Filter for searching for users against the 'User Search Base'.
+            (i.e. sAMAccountName={0}). The user specified name is inserted into '{0}'.
+
+        'Identity Strategy' - Strategy to identify users. Possible values are USE_DN and USE_USERNAME.
+            The default functionality if this property is missing is USE_DN in order to retain
+            backward compatibility. USE_DN will use the full DN of the user entry if possible.
+            USE_USERNAME will use the username the user logged in with.
+        'Authentication Expiration' - The duration of how long the user authentication is valid
+            for. If the user never logs out, they will be required to log back in following
+            this duration.
+    -->
+    <!-- To enable the ldap-identity-provider remove 2 lines. This is 1 of 2.
+    <provider>
+        <identifier>ldap-identity-provider</identifier>
+        <class>org.apache.nifi.registry.security.ldap.LdapIdentityProvider</class>
+        <property name="Authentication Strategy">SIMPLE</property>
+
+        <property name="Manager DN"></property>
+        <property name="Manager Password"></property>
+        
+        <property name="Referral Strategy">FOLLOW</property>
+        <property name="Connect Timeout">10 secs</property>
+        <property name="Read Timeout">10 secs</property>
+
+        <property name="Url"></property>
+        <property name="User Search Base"></property>
+        <property name="User Search Filter"></property>
+
+        <property name="Identity Strategy">USE_USERNAME</property>
+        <property name="Authentication Expiration">12 hours</property>
+    </provider>
+    To enable the ldap-identity-provider remove 2 lines. This is 2 of 2. -->
+
+    <!--
+        Identity Provider for users logging in with username/password against a Kerberos KDC server.
+
+        'Default Realm' - Default realm to provide when user enters incomplete user principal (i.e. NIFI.APACHE.ORG).
+        'Authentication Expiration' - The duration of how long the user authentication is valid for. If the user never logs out, they will be required to log back in following this duration.
+    -->
+    <!-- To enable the kerberos-identity-provider remove 2 lines. This is 1 of 2.
+    <provider>
+        <identifier>kerberos-identity-provider</identifier>
+        <class>org.apache.nifi.registry.web.security.authentication.kerberos.KerberosIdentityProvider</class>
+        <property name="Default Realm">NIFI.APACHE.ORG</property>
+        <property name="Authentication Expiration">12 hours</property>
+        <property name="Enable Debug">false</property>
+    </provider>
+    To enable the kerberos-provider remove 2 lines. This is 2 of 2. -->
+
+</identityProviders>
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/6f26290d/nifi-registry-core/nifi-registry-resources/src/main/resources/conf/logback.xml
----------------------------------------------------------------------
diff --git a/nifi-registry-core/nifi-registry-resources/src/main/resources/conf/logback.xml b/nifi-registry-core/nifi-registry-resources/src/main/resources/conf/logback.xml
new file mode 100644
index 0000000..7d65bda
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-resources/src/main/resources/conf/logback.xml
@@ -0,0 +1,124 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+  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.
+-->
+<configuration scan="true" scanPeriod="30 seconds">
+    <contextListener class="ch.qos.logback.classic.jul.LevelChangePropagator">
+        <resetJUL>true</resetJUL>
+    </contextListener>
+    
+    <appender name="APP_FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
+        <file>logs/nifi-registry-app.log</file>
+        <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
+            <!--
+              For daily rollover, use 'app_%d.log'.
+              For hourly rollover, use 'app_%d{yyyy-MM-dd_HH}.log'.
+              To GZIP rolled files, replace '.log' with '.log.gz'.
+              To ZIP rolled files, replace '.log' with '.log.zip'.
+            -->
+            <fileNamePattern>./logs/nifi-registry-app_%d{yyyy-MM-dd_HH}.%i.log</fileNamePattern>
+            <timeBasedFileNamingAndTriggeringPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedFNATP">
+                <maxFileSize>100MB</maxFileSize>
+            </timeBasedFileNamingAndTriggeringPolicy>
+            <!-- keep 30 log files worth of history -->
+            <maxHistory>30</maxHistory>
+        </rollingPolicy>
+        <encoder class="ch.qos.logback.classic.encoder.PatternLayoutEncoder">
+            <pattern>%date %level [%thread] %logger{40} %msg%n</pattern>
+            <immediateFlush>true</immediateFlush>
+        </encoder>
+    </appender>
+
+    <appender name="BOOTSTRAP_FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
+        <file>${org.apache.nifi.registry.bootstrap.config.log.dir}/nifi-registry-bootstrap.log</file>
+        <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
+            <!--
+              For daily rollover, use 'user_%d.log'.
+              For hourly rollover, use 'user_%d{yyyy-MM-dd_HH}.log'.
+              To GZIP rolled files, replace '.log' with '.log.gz'.
+              To ZIP rolled files, replace '.log' with '.log.zip'.
+            -->
+            <fileNamePattern>${org.apache.nifi.registry.bootstrap.config.log.dir}/nifi-registry-bootstrap_%d.log</fileNamePattern>
+            <!-- keep 5 log files worth of history -->
+            <maxHistory>5</maxHistory>
+        </rollingPolicy>
+        <encoder class="ch.qos.logback.classic.encoder.PatternLayoutEncoder">
+            <pattern>%date %level [%thread] %logger{40} %msg%n</pattern>
+        </encoder>
+    </appender>
+
+    <appender name="EVENTS_FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
+        <file>${org.apache.nifi.registry.bootstrap.config.log.dir}/nifi-registry-event.log</file>
+        <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
+            <!--
+              For daily rollover, use 'user_%d.log'.
+              For hourly rollover, use 'user_%d{yyyy-MM-dd_HH}.log'.
+              To GZIP rolled files, replace '.log' with '.log.gz'.
+              To ZIP rolled files, replace '.log' with '.log.zip'.
+            -->
+            <fileNamePattern>${org.apache.nifi.registry.bootstrap.config.log.dir}/nifi-registry-event_%d.log</fileNamePattern>
+            <!-- keep 5 log files worth of history -->
+            <maxHistory>5</maxHistory>
+        </rollingPolicy>
+        <encoder class="ch.qos.logback.classic.encoder.PatternLayoutEncoder">
+            <pattern>%date ## %msg%n</pattern>
+        </encoder>
+    </appender>
+
+    <appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
+        <encoder class="ch.qos.logback.classic.encoder.PatternLayoutEncoder">
+            <pattern>%date %level [%thread] %logger{40} %msg%n</pattern>
+        </encoder>
+    </appender>
+    
+    <!-- valid logging levels: TRACE, DEBUG, INFO, WARN, ERROR -->
+    
+    <logger name="org.apache.nifi.registry" level="INFO"/>
+
+    <!-- To see SQL statements set this to DEBUG -->
+    <logger name="org.hibernate.SQL" level="INFO" />
+    <!-- To see the values in SQL statements set this to TRACE -->
+    <logger name="org.hibernate.type" level="INFO" />
+
+    <!--
+        Logger for capturing Bootstrap logs and NiFi Registry's standard error and standard out.
+    -->
+    <logger name="org.apache.nifi.registry.bootstrap" level="INFO" additivity="false">
+        <appender-ref ref="BOOTSTRAP_FILE" />
+    </logger>
+    <logger name="org.apache.nifi.registry.bootstrap.Command" level="INFO" additivity="false">
+        <appender-ref ref="CONSOLE" />
+        <appender-ref ref="BOOTSTRAP_FILE" />
+    </logger>
+
+    <!-- Everything written to NiFi Registry's Standard Out will be logged with the logger org.apache.nifi.StdOut at INFO level -->
+    <logger name="org.apache.nifi.registry.StdOut" level="INFO" additivity="false">
+        <appender-ref ref="BOOTSTRAP_FILE" />
+    </logger>
+
+    <!-- Everything written to NiFi Registry's Standard Error will be logged with the logger org.apache.nifi.StdErr at ERROR level -->
+    <logger name="org.apache.nifi.registry.StdErr" level="ERROR" additivity="false">
+        <appender-ref ref="BOOTSTRAP_FILE" />
+    </logger>
+
+    <!-- This will log all events to a separate file when the LoggingEventHookProvider is enabled in providers.xml -->
+    <logger name="org.apache.nifi.registry.provider.hook.LoggingEventHookProvider" level="INFO" additivity="false">
+        <appender-ref ref="EVENTS_FILE" />
+    </logger>
+
+    <root level="INFO">
+        <appender-ref ref="APP_FILE"/>
+    </root>
+    
+</configuration>

http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/6f26290d/nifi-registry-core/nifi-registry-resources/src/main/resources/conf/nifi-registry.properties
----------------------------------------------------------------------
diff --git a/nifi-registry-core/nifi-registry-resources/src/main/resources/conf/nifi-registry.properties b/nifi-registry-core/nifi-registry-resources/src/main/resources/conf/nifi-registry.properties
new file mode 100644
index 0000000..fb77a07
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-resources/src/main/resources/conf/nifi-registry.properties
@@ -0,0 +1,80 @@
+# 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.
+
+# web properties #
+nifi.registry.web.war.directory=${nifi.registry.web.war.directory}
+nifi.registry.web.http.host=${nifi.registry.web.http.host}
+nifi.registry.web.http.port=${nifi.registry.web.http.port}
+nifi.registry.web.https.host=${nifi.registry.web.https.host}
+nifi.registry.web.https.port=${nifi.registry.web.https.port}
+nifi.registry.web.jetty.working.directory=${nifi.registry.jetty.work.dir}
+nifi.registry.web.jetty.threads=${nifi.registry.web.jetty.threads}
+
+# security properties #
+nifi.registry.security.keystore=${nifi.registry.security.keystore}
+nifi.registry.security.keystoreType=${nifi.registry.security.keystoreType}
+nifi.registry.security.keystorePasswd=${nifi.registry.security.keystorePasswd}
+nifi.registry.security.keyPasswd=${nifi.registry.security.keyPasswd}
+nifi.registry.security.truststore=${nifi.registry.security.truststore}
+nifi.registry.security.truststoreType=${nifi.registry.security.truststoreType}
+nifi.registry.security.truststorePasswd=${nifi.registry.security.truststorePasswd}
+nifi.registry.security.needClientAuth=${nifi.registry.security.needClientAuth}
+nifi.registry.security.authorizers.configuration.file=${nifi.registry.security.authorizers.configuration.file}
+nifi.registry.security.authorizer=${nifi.registry.security.authorizer}
+nifi.registry.security.identity.providers.configuration.file=${nifi.registry.security.identity.providers.configuration.file}
+nifi.registry.security.identity.provider=${nifi.registry.security.identity.provider}
+
+# sensitive property protection properties #
+# nifi.registry.sensitive.props.additional.keys=
+
+# providers properties #
+nifi.registry.providers.configuration.file=${nifi.registry.providers.configuration.file}
+
+# legacy database properties, used to migrate data from original DB to new DB below
+# NOTE: Users upgrading from 0.1.0 should leave these populated, but new installs after 0.1.0 should leave these empty
+nifi.registry.db.directory=${nifi.registry.db.directory}
+nifi.registry.db.url.append=${nifi.registry.db.url.append}
+
+# database properties
+nifi.registry.db.url=${nifi.registry.db.url}
+nifi.registry.db.driver.class=${nifi.registry.db.driver.class}
+nifi.registry.db.driver.directory=${nifi.registry.db.driver.directory}
+nifi.registry.db.username=${nifi.registry.db.username}
+nifi.registry.db.password=${nifi.registry.db.password}
+nifi.registry.db.maxConnections=${nifi.registry.db.maxConnections}
+nifi.registry.db.sql.debug=${nifi.registry.db.sql.debug}
+
+# extension directories #
+# Each property beginning with "nifi.registry.extension.dir." will be treated as location for an extension,
+# and a class loader will be created for each location, with the system class loader as the parent
+#
+#nifi.registry.extension.dir.1=/path/to/extension1
+#nifi.registry.extension.dir.2=/path/to/extension2
+
+# Identity Mapping Properties #
+# These properties allow normalizing user identities such that identities coming from different identity providers
+# (certificates, LDAP, Kerberos) can be treated the same internally in NiFi. The following example demonstrates normalizing
+# DNs from certificates and principals from Kerberos into a common identity string:
+#
+# nifi.registry.security.identity.mapping.pattern.dn=^CN=(.*?), OU=(.*?), O=(.*?), L=(.*?), ST=(.*?), C=(.*?)$
+# nifi.registry.security.identity.mapping.value.dn=$1@$2
+# nifi.registry.security.identity.mapping.pattern.kerb=^(.*?)/instance@(.*?)$
+# nifi.registry.security.identity.mapping.value.kerb=$1@$2
+
+# kerberos properties #
+nifi.registry.kerberos.krb5.file=${nifi.registry.kerberos.krb5.file}
+nifi.registry.kerberos.spnego.principal=${nifi.registry.kerberos.spnego.principal}
+nifi.registry.kerberos.spnego.keytab.location=${nifi.registry.kerberos.spnego.keytab.location}
+nifi.registry.kerberos.spnego.authentication.expiration=${nifi.registry.kerberos.spnego.authentication.expiration}

http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/6f26290d/nifi-registry-core/nifi-registry-resources/src/main/resources/conf/providers.xml
----------------------------------------------------------------------
diff --git a/nifi-registry-core/nifi-registry-resources/src/main/resources/conf/providers.xml b/nifi-registry-core/nifi-registry-resources/src/main/resources/conf/providers.xml
new file mode 100644
index 0000000..faf8d4f
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-resources/src/main/resources/conf/providers.xml
@@ -0,0 +1,54 @@
+<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
+<!--
+  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.
+-->
+<providers>
+
+    <flowPersistenceProvider>
+        <class>org.apache.nifi.registry.provider.flow.FileSystemFlowPersistenceProvider</class>
+        <property name="Flow Storage Directory">./flow_storage</property>
+    </flowPersistenceProvider>
+
+    <!--
+    <flowPersistenceProvider>
+        <class>org.apache.nifi.registry.provider.flow.git.GitFlowPersistenceProvider</class>
+        <property name="Flow Storage Directory">./flow_storage</property>
+        <property name="Remote To Push"></property>
+        <property name="Remote Access User"></property>
+        <property name="Remote Access Password"></property>
+    </flowPersistenceProvider>
+    -->
+
+    <!--
+    <eventHookProvider>
+    	<class>org.apache.nifi.registry.provider.hook.ScriptEventHookProvider</class>
+    	<property name="Script Path"></property>
+    	<property name="Working Directory"></property>
+    	-->
+    	<!-- Optional Whitelist Event types
+        <property name="Whitelisted Event Type 1">CREATE_FLOW</property>
+        <property name="Whitelisted Event Type 2">DELETE_FLOW</property>
+    	-->
+    <!--
+    </eventHookProvider>
+    -->
+
+    <!-- This will log all events to a separate file specified by the EVENT_APPENDER in logback.xml -->
+    <!--
+    <eventHookProvider>
+        <class>org.apache.nifi.registry.provider.hook.LoggingEventHookProvider</class>
+    </eventHookProvider>
+    -->
+
+</providers>
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/6f26290d/nifi-registry-core/nifi-registry-runtime/pom.xml
----------------------------------------------------------------------
diff --git a/nifi-registry-core/nifi-registry-runtime/pom.xml b/nifi-registry-core/nifi-registry-runtime/pom.xml
new file mode 100644
index 0000000..ed0fae4
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-runtime/pom.xml
@@ -0,0 +1,46 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+  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.
+-->
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+    <modelVersion>4.0.0</modelVersion>
+    <parent>
+        <groupId>org.apache.nifi.registry</groupId>
+        <artifactId>nifi-registry-core</artifactId>
+        <version>0.3.0-SNAPSHOT</version>
+    </parent>
+    <artifactId>nifi-registry-runtime</artifactId>
+    <packaging>jar</packaging>
+    <dependencies>
+        <dependency>
+            <groupId>org.apache.nifi.registry</groupId>
+            <artifactId>nifi-registry-utils</artifactId>
+            <version>0.3.0-SNAPSHOT</version>
+        </dependency>
+        <dependency>
+            <groupId>org.apache.nifi.registry</groupId>
+            <artifactId>nifi-registry-properties</artifactId>
+            <version>0.3.0-SNAPSHOT</version>
+        </dependency>
+        <dependency>
+            <groupId>org.apache.nifi.registry</groupId>
+            <artifactId>nifi-registry-jetty</artifactId>
+            <version>0.3.0-SNAPSHOT</version>
+        </dependency>
+        <dependency>
+            <groupId>org.slf4j</groupId>
+            <artifactId>jul-to-slf4j</artifactId>
+        </dependency>
+    </dependencies>
+</project>

http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/6f26290d/nifi-registry-core/nifi-registry-runtime/src/main/java/org/apache/nifi/registry/BootstrapListener.java
----------------------------------------------------------------------
diff --git a/nifi-registry-core/nifi-registry-runtime/src/main/java/org/apache/nifi/registry/BootstrapListener.java b/nifi-registry-core/nifi-registry-runtime/src/main/java/org/apache/nifi/registry/BootstrapListener.java
new file mode 100644
index 0000000..0eabe94
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-runtime/src/main/java/org/apache/nifi/registry/BootstrapListener.java
@@ -0,0 +1,395 @@
+/*
+ * 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;
+
+import org.apache.nifi.registry.util.LimitingInputStream;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.BufferedReader;
+import java.io.BufferedWriter;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.io.OutputStream;
+import java.io.OutputStreamWriter;
+import java.lang.management.LockInfo;
+import java.lang.management.ManagementFactory;
+import java.lang.management.MonitorInfo;
+import java.lang.management.ThreadInfo;
+import java.lang.management.ThreadMXBean;
+import java.net.InetSocketAddress;
+import java.net.ServerSocket;
+import java.net.Socket;
+import java.net.SocketTimeoutException;
+import java.nio.charset.StandardCharsets;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.List;
+import java.util.UUID;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+
+public class BootstrapListener {
+
+    private static final Logger logger = LoggerFactory.getLogger(BootstrapListener.class);
+
+    private final NiFiRegistry nifi;
+    private final int bootstrapPort;
+    private final String secretKey;
+
+    private volatile Listener listener;
+    private volatile ServerSocket serverSocket;
+
+    public BootstrapListener(final NiFiRegistry nifi, final int bootstrapPort) {
+        this.nifi = nifi;
+        this.bootstrapPort = bootstrapPort;
+        secretKey = UUID.randomUUID().toString();
+    }
+
+    public void start() throws IOException {
+        logger.debug("Starting Bootstrap Listener to communicate with Bootstrap Port {}", bootstrapPort);
+
+        serverSocket = new ServerSocket();
+        serverSocket.bind(new InetSocketAddress("localhost", 0));
+        serverSocket.setSoTimeout(2000);
+
+        final int localPort = serverSocket.getLocalPort();
+        logger.info("Started Bootstrap Listener, Listening for incoming requests on port {}", localPort);
+
+        listener = new Listener(serverSocket);
+        final Thread listenThread = new Thread(listener);
+        listenThread.setDaemon(true);
+        listenThread.setName("Listen to Bootstrap");
+        listenThread.start();
+
+        logger.debug("Notifying Bootstrap that local port is {}", localPort);
+        sendCommand("PORT", new String[] { String.valueOf(localPort), secretKey});
+    }
+
+    public void stop() {
+        if (listener != null) {
+            listener.stop();
+        }
+    }
+
+    public void sendStartedStatus(boolean status) throws IOException {
+        logger.debug("Notifying Bootstrap that the status of starting NiFi Registry is {}", status);
+        sendCommand("STARTED", new String[]{ String.valueOf(status) });
+    }
+
+    private void sendCommand(final String command, final String[] args) throws IOException {
+        try (final Socket socket = new Socket()) {
+            socket.setSoTimeout(60000);
+            socket.connect(new InetSocketAddress("localhost", bootstrapPort));
+            socket.setSoTimeout(60000);
+
+            final StringBuilder commandBuilder = new StringBuilder(command);
+            for (final String arg : args) {
+                commandBuilder.append(" ").append(arg);
+            }
+            commandBuilder.append("\n");
+
+            final String commandWithArgs = commandBuilder.toString();
+            logger.debug("Sending command to Bootstrap: " + commandWithArgs);
+
+            final OutputStream out = socket.getOutputStream();
+            out.write((commandWithArgs).getBytes(StandardCharsets.UTF_8));
+            out.flush();
+
+            logger.debug("Awaiting response from Bootstrap...");
+            final BufferedReader reader = new BufferedReader(new InputStreamReader(socket.getInputStream()));
+            final String response = reader.readLine();
+            if ("OK".equals(response)) {
+                logger.info("Successfully initiated communication with Bootstrap");
+            } else {
+                logger.error("Failed to communicate with Bootstrap. Bootstrap may be unable to issue or receive commands from NiFi Registry ");
+            }
+        }
+    }
+
+    private class Listener implements Runnable {
+
+        private final ServerSocket serverSocket;
+        private final ExecutorService executor;
+        private volatile boolean stopped = false;
+
+        public Listener(final ServerSocket serverSocket) {
+            this.serverSocket = serverSocket;
+            this.executor = Executors.newFixedThreadPool(2);
+        }
+
+        public void stop() {
+            stopped = true;
+
+            executor.shutdownNow();
+
+            try {
+                serverSocket.close();
+            } catch (final IOException ioe) {
+                // nothing to really do here. we could log this, but it would just become
+                // confusing in the logs, as we're shutting down and there's no real benefit
+            }
+        }
+
+        @Override
+        public void run() {
+            while (!stopped) {
+                try {
+                    final Socket socket;
+                    try {
+                        logger.debug("Listening for Bootstrap Requests");
+                        socket = serverSocket.accept();
+                    } catch (final SocketTimeoutException ste) {
+                        if (stopped) {
+                            return;
+                        }
+
+                        continue;
+                    } catch (final IOException ioe) {
+                        if (stopped) {
+                            return;
+                        }
+
+                        throw ioe;
+                    }
+
+                    logger.debug("Received connection from Bootstrap");
+                    socket.setSoTimeout(5000);
+
+                    executor.submit(new Runnable() {
+                        @Override
+                        public void run() {
+                            try {
+                                final BootstrapRequest request = readRequest(socket.getInputStream());
+                                final BootstrapRequest.RequestType requestType = request.getRequestType();
+
+                                switch (requestType) {
+                                    case PING:
+                                        logger.debug("Received PING request from Bootstrap; responding");
+                                        echoPing(socket.getOutputStream());
+                                        logger.debug("Responded to PING request from Bootstrap");
+                                        break;
+                                    case SHUTDOWN:
+                                        logger.info("Received SHUTDOWN request from Bootstrap");
+                                        echoShutdown(socket.getOutputStream());
+                                        nifi.shutdownHook();
+                                        return;
+                                    case DUMP:
+                                        logger.info("Received DUMP request from Bootstrap");
+                                        writeDump(socket.getOutputStream());
+                                        break;
+                                }
+                            } catch (final Throwable t) {
+                                logger.error("Failed to process request from Bootstrap due to " + t.toString(), t);
+                            } finally {
+                                try {
+                                    socket.close();
+                                } catch (final IOException ioe) {
+                                    logger.warn("Failed to close socket to Bootstrap due to {}", ioe.toString());
+                                }
+                            }
+                        }
+                    });
+                } catch (final Throwable t) {
+                    logger.error("Failed to process request from Bootstrap due to " + t.toString(), t);
+                }
+            }
+        }
+    }
+
+    private static void writeDump(final OutputStream out) throws IOException {
+        final ThreadMXBean mbean = ManagementFactory.getThreadMXBean();
+        final BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(out));
+
+        final ThreadInfo[] infos = mbean.dumpAllThreads(true, true);
+        final long[] deadlockedThreadIds = mbean.findDeadlockedThreads();
+        final long[] monitorDeadlockThreadIds = mbean.findMonitorDeadlockedThreads();
+
+        final List<ThreadInfo> sortedInfos = new ArrayList<>(infos.length);
+        for (final ThreadInfo info : infos) {
+            sortedInfos.add(info);
+        }
+        Collections.sort(sortedInfos, new Comparator<ThreadInfo>() {
+            @Override
+            public int compare(ThreadInfo o1, ThreadInfo o2) {
+                return o1.getThreadName().toLowerCase().compareTo(o2.getThreadName().toLowerCase());
+            }
+        });
+
+        final StringBuilder sb = new StringBuilder();
+        for (final ThreadInfo info : sortedInfos) {
+            sb.append("\n");
+            sb.append("\"").append(info.getThreadName()).append("\" Id=");
+            sb.append(info.getThreadId()).append(" ");
+            sb.append(info.getThreadState().toString()).append(" ");
+
+            switch (info.getThreadState()) {
+                case BLOCKED:
+                case TIMED_WAITING:
+                case WAITING:
+                    sb.append(" on ");
+                    sb.append(info.getLockInfo());
+                    break;
+                default:
+                    break;
+            }
+
+            if (info.isSuspended()) {
+                sb.append(" (suspended)");
+            }
+            if (info.isInNative()) {
+                sb.append(" (in native code)");
+            }
+
+            if (deadlockedThreadIds != null && deadlockedThreadIds.length > 0) {
+                for (final long id : deadlockedThreadIds) {
+                    if (id == info.getThreadId()) {
+                        sb.append(" ** DEADLOCKED THREAD **");
+                    }
+                }
+            }
+
+            if (monitorDeadlockThreadIds != null && monitorDeadlockThreadIds.length > 0) {
+                for (final long id : monitorDeadlockThreadIds) {
+                    if (id == info.getThreadId()) {
+                        sb.append(" ** MONITOR-DEADLOCKED THREAD **");
+                    }
+                }
+            }
+
+            final StackTraceElement[] stackTraces = info.getStackTrace();
+            for (final StackTraceElement element : stackTraces) {
+                sb.append("\n\tat ").append(element);
+
+                final MonitorInfo[] monitors = info.getLockedMonitors();
+                for (final MonitorInfo monitor : monitors) {
+                    if (monitor.getLockedStackFrame().equals(element)) {
+                        sb.append("\n\t- waiting on ").append(monitor);
+                    }
+                }
+            }
+
+            final LockInfo[] lockInfos = info.getLockedSynchronizers();
+            if (lockInfos.length > 0) {
+                sb.append("\n\t");
+                sb.append("Number of Locked Synchronizers: ").append(lockInfos.length);
+                for (final LockInfo lockInfo : lockInfos) {
+                    sb.append("\n\t- ").append(lockInfo.toString());
+                }
+            }
+
+            sb.append("\n");
+        }
+
+        if (deadlockedThreadIds != null && deadlockedThreadIds.length > 0) {
+            sb.append("\n\nDEADLOCK DETECTED!");
+            sb.append("\nThe following thread IDs are deadlocked:");
+            for (final long id : deadlockedThreadIds) {
+                sb.append("\n").append(id);
+            }
+        }
+
+        if (monitorDeadlockThreadIds != null && monitorDeadlockThreadIds.length > 0) {
+            sb.append("\n\nMONITOR DEADLOCK DETECTED!");
+            sb.append("\nThe following thread IDs are deadlocked:");
+            for (final long id : monitorDeadlockThreadIds) {
+                sb.append("\n").append(id);
+            }
+        }
+
+        writer.write(sb.toString());
+        writer.flush();
+    }
+
+    private void echoPing(final OutputStream out) throws IOException {
+        out.write("PING\n".getBytes(StandardCharsets.UTF_8));
+        out.flush();
+    }
+
+    private void echoShutdown(final OutputStream out) throws IOException {
+        out.write("SHUTDOWN\n".getBytes(StandardCharsets.UTF_8));
+        out.flush();
+    }
+
+    @SuppressWarnings("resource")  // we don't want to close the stream, as the caller will do that
+    private BootstrapRequest readRequest(final InputStream in) throws IOException {
+        // We want to ensure that we don't try to read data from an InputStream directly
+        // by a BufferedReader because any user on the system could open a socket and send
+        // a multi-gigabyte file without any new lines in order to crash the NiFi instance
+        // (or at least cause OutOfMemoryErrors, which can wreak havoc on the running instance).
+        // So we will limit the Input Stream to only 4 KB, which should be plenty for any request.
+        final LimitingInputStream limitingIn = new LimitingInputStream(in, 4096);
+        final BufferedReader reader = new BufferedReader(new InputStreamReader(limitingIn));
+
+        final String line = reader.readLine();
+        final String[] splits = line.split(" ");
+        if (splits.length < 1) {
+            throw new IOException("Received invalid request from Bootstrap: " + line);
+        }
+
+        final String requestType = splits[0];
+        final String[] args;
+        if (splits.length == 1) {
+            throw new IOException("Received invalid request from Bootstrap; request did not have a secret key; request type = " + requestType);
+        } else if (splits.length == 2) {
+            args = new String[0];
+        } else {
+            args = Arrays.copyOfRange(splits, 2, splits.length);
+        }
+
+        final String requestKey = splits[1];
+        if (!secretKey.equals(requestKey)) {
+            throw new IOException("Received invalid Secret Key for request type " + requestType);
+        }
+
+        try {
+            return new BootstrapRequest(requestType, args);
+        } catch (final Exception e) {
+            throw new IOException("Received invalid request from Bootstrap; request type = " + requestType);
+        }
+    }
+
+    private static class BootstrapRequest {
+
+        public static enum RequestType {
+
+            SHUTDOWN,
+            DUMP,
+            PING;
+        }
+
+        private final RequestType requestType;
+        private final String[] args;
+
+        public BootstrapRequest(final String request, final String[] args) {
+            this.requestType = RequestType.valueOf(request);
+            this.args = args;
+        }
+
+        public RequestType getRequestType() {
+            return requestType;
+        }
+
+        @SuppressWarnings("unused")
+        public String[] getArgs() {
+            return args;
+        }
+    }
+}

http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/6f26290d/nifi-registry-core/nifi-registry-runtime/src/main/java/org/apache/nifi/registry/NiFiRegistry.java
----------------------------------------------------------------------
diff --git a/nifi-registry-core/nifi-registry-runtime/src/main/java/org/apache/nifi/registry/NiFiRegistry.java b/nifi-registry-core/nifi-registry-runtime/src/main/java/org/apache/nifi/registry/NiFiRegistry.java
new file mode 100644
index 0000000..65fdcf4
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-runtime/src/main/java/org/apache/nifi/registry/NiFiRegistry.java
@@ -0,0 +1,197 @@
+/*
+ * 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;
+
+import org.apache.nifi.registry.jetty.JettyServer;
+import org.apache.nifi.registry.properties.NiFiRegistryProperties;
+import org.apache.nifi.registry.properties.NiFiRegistryPropertiesLoader;
+import org.apache.nifi.registry.properties.SensitivePropertyProtectionException;
+import org.apache.nifi.registry.security.crypto.BootstrapFileCryptoKeyProvider;
+import org.apache.nifi.registry.security.crypto.CryptoKeyProvider;
+import org.apache.nifi.registry.security.crypto.MissingCryptoKeyException;
+import org.apache.nifi.registry.util.FileUtils;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.slf4j.bridge.SLF4JBridgeHandler;
+
+import java.io.File;
+import java.io.IOException;
+import java.lang.reflect.InvocationTargetException;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * Main entry point for NiFiRegistry.
+ */
+public class NiFiRegistry {
+
+    private static final Logger LOGGER = LoggerFactory.getLogger(NiFiRegistry.class);
+
+    public static final String BOOTSTRAP_PORT_PROPERTY = "nifi.registry.bootstrap.listen.port";
+
+    public static final String NIFI_REGISTRY_PROPERTIES_FILE_PATH_PROPERTY = "nifi.registry.properties.file.path";
+    public static final String NIFI_REGISTRY_BOOTSTRAP_FILE_PATH_PROPERTY = "nifi.registry.bootstrap.config.file.path";
+
+    public static final String RELATIVE_BOOTSTRAP_FILE_LOCATION = "conf/bootstrap.conf";
+    public static final String RELATIVE_PROPERTIES_FILE_LOCATION = "conf/nifi-registry.properties";
+
+    private final JettyServer server;
+    private final BootstrapListener bootstrapListener;
+    private volatile boolean shutdown = false;
+
+    public NiFiRegistry(final NiFiRegistryProperties properties, CryptoKeyProvider masterKeyProvider)
+            throws ClassNotFoundException, IOException, NoSuchMethodException, InstantiationException, IllegalAccessException, IllegalArgumentException, InvocationTargetException {
+
+        Thread.setDefaultUncaughtExceptionHandler(new Thread.UncaughtExceptionHandler() {
+            @Override
+            public void uncaughtException(final Thread t, final Throwable e) {
+                LOGGER.error("An Unknown Error Occurred in Thread {}: {}", t, e.toString());
+                LOGGER.error("", e);
+            }
+        });
+
+        // register the shutdown hook
+        Runtime.getRuntime().addShutdownHook(new Thread(new Runnable() {
+            @Override
+            public void run() {
+                // shutdown the jetty server
+                shutdownHook();
+            }
+        }));
+
+        final String bootstrapPort = System.getProperty(BOOTSTRAP_PORT_PROPERTY);
+        if (bootstrapPort != null) {
+            try {
+                final int port = Integer.parseInt(bootstrapPort);
+
+                if (port < 1 || port > 65535) {
+                    throw new RuntimeException("Failed to start NiFi Registry because system property '" + BOOTSTRAP_PORT_PROPERTY + "' is not a valid integer in the range 1 - 65535");
+                }
+
+                bootstrapListener = new BootstrapListener(this, port);
+                bootstrapListener.start();
+            } catch (final NumberFormatException nfe) {
+                throw new RuntimeException("Failed to start NiFi Registry because system property '" + BOOTSTRAP_PORT_PROPERTY + "' is not a valid integer in the range 1 - 65535");
+            }
+        } else {
+            LOGGER.info("NiFi Registry started without Bootstrap Port information provided; will not listen for requests from Bootstrap");
+            bootstrapListener = null;
+        }
+
+        // delete the web working dir - if the application does not start successfully
+        // the web app directories might be in an invalid state. when this happens
+        // jetty will not attempt to re-extract the war into the directory. by removing
+        // the working directory, we can be assured that it will attempt to extract the
+        // war every time the application starts.
+        File webWorkingDir = properties.getWebWorkingDirectory();
+        FileUtils.deleteFilesInDirectory(webWorkingDir, null, LOGGER, true, true);
+        FileUtils.deleteFile(webWorkingDir, LOGGER, 3);
+
+        // redirect JUL log events
+        SLF4JBridgeHandler.removeHandlersForRootLogger();
+        SLF4JBridgeHandler.install();
+
+        final long startTime = System.nanoTime();
+        server = new JettyServer(properties, masterKeyProvider);
+
+        if (shutdown) {
+            LOGGER.info("NiFi Registry has been shutdown via NiFi Registry Bootstrap. Will not start Controller");
+        } else {
+            server.start();
+
+            if (bootstrapListener != null) {
+                bootstrapListener.sendStartedStatus(true);
+            }
+
+            final long duration = System.nanoTime() - startTime;
+            LOGGER.info("Registry initialization took " + duration + " nanoseconds "
+                    + "(" + (int) TimeUnit.SECONDS.convert(duration, TimeUnit.NANOSECONDS) + " seconds).");
+        }
+    }
+
+    protected void shutdownHook() {
+        try {
+            this.shutdown = true;
+
+            LOGGER.info("Initiating shutdown of Jetty web server...");
+            if (server != null) {
+                server.stop();
+            }
+            if (bootstrapListener != null) {
+                bootstrapListener.stop();
+            }
+            LOGGER.info("Jetty web server shutdown completed (nicely or otherwise).");
+        } catch (final Throwable t) {
+            LOGGER.warn("Problem occurred ensuring Jetty web server was properly terminated due to " + t);
+        }
+    }
+
+    /**
+     * Main entry point of the application.
+     *
+     * @param args things which are ignored
+     */
+    public static void main(String[] args) {
+        LOGGER.info("Launching NiFi Registry...");
+
+        final CryptoKeyProvider masterKeyProvider;
+        final NiFiRegistryProperties properties;
+        try {
+            final String bootstrapConfigFilePath = System.getProperty(NIFI_REGISTRY_BOOTSTRAP_FILE_PATH_PROPERTY, RELATIVE_BOOTSTRAP_FILE_LOCATION);
+            masterKeyProvider = new BootstrapFileCryptoKeyProvider(bootstrapConfigFilePath);
+            LOGGER.info("Read property protection key from {}", bootstrapConfigFilePath);
+            properties = initializeProperties(masterKeyProvider);
+        } catch (final IllegalArgumentException iae) {
+            throw new RuntimeException("Unable to load properties: " + iae, iae);
+        }
+
+        try {
+            new NiFiRegistry(properties, masterKeyProvider);
+        } catch (final Throwable t) {
+            LOGGER.error("Failure to launch NiFi Registry due to " + t, t);
+        }
+    }
+
+    private static NiFiRegistryProperties initializeProperties(CryptoKeyProvider masterKeyProvider) {
+
+        String key = CryptoKeyProvider.EMPTY_KEY;
+        try {
+            key = masterKeyProvider.getKey();
+        } catch (MissingCryptoKeyException e) {
+            LOGGER.debug("CryptoKeyProvider provided to initializeProperties method was empty - did not contain a key.");
+            // Do nothing. The key can be empty when it is passed to the loader as the loader will only use it if any properties are protected.
+        }
+
+        try {
+            try {
+                // Load properties using key. If properties are protected and key missing, throw RuntimeException
+                final String nifiRegistryPropertiesFilePath = System.getProperty(NIFI_REGISTRY_PROPERTIES_FILE_PATH_PROPERTY, RELATIVE_PROPERTIES_FILE_LOCATION);
+                final NiFiRegistryProperties properties = NiFiRegistryPropertiesLoader.withKey(key).load(nifiRegistryPropertiesFilePath);
+                LOGGER.info("Loaded {} properties", properties.size());
+                return properties;
+            } catch (SensitivePropertyProtectionException e) {
+                final String msg = "There was an issue decrypting protected properties";
+                LOGGER.error(msg, e);
+                throw new IllegalArgumentException(msg);
+            }
+        } catch (IllegalArgumentException e) {
+            final String msg = "The bootstrap process did not provide a valid key and there are protected properties present in the properties file";
+            LOGGER.error(msg, e);
+            throw new IllegalArgumentException(msg);
+        }
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/6f26290d/nifi-registry-core/nifi-registry-runtime/src/main/java/org/apache/nifi/registry/util/LimitingInputStream.java
----------------------------------------------------------------------
diff --git a/nifi-registry-core/nifi-registry-runtime/src/main/java/org/apache/nifi/registry/util/LimitingInputStream.java b/nifi-registry-core/nifi-registry-runtime/src/main/java/org/apache/nifi/registry/util/LimitingInputStream.java
new file mode 100644
index 0000000..c069ec2
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-runtime/src/main/java/org/apache/nifi/registry/util/LimitingInputStream.java
@@ -0,0 +1,107 @@
+/*
+ * 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.util;
+
+import java.io.IOException;
+import java.io.InputStream;
+
+public class LimitingInputStream extends InputStream {
+
+    private final InputStream in;
+    private final long limit;
+    private long bytesRead = 0;
+
+    public LimitingInputStream(final InputStream in, final long limit) {
+        this.in = in;
+        this.limit = limit;
+    }
+
+    @Override
+    public int read() throws IOException {
+        if (bytesRead >= limit) {
+            return -1;
+        }
+
+        final int val = in.read();
+        if (val > -1) {
+            bytesRead++;
+        }
+        return val;
+    }
+
+    @Override
+    public int read(final byte[] b) throws IOException {
+        if (bytesRead >= limit) {
+            return -1;
+        }
+
+        final int maxToRead = (int) Math.min(b.length, limit - bytesRead);
+
+        final int val = in.read(b, 0, maxToRead);
+        if (val > 0) {
+            bytesRead += val;
+        }
+        return val;
+    }
+
+    @Override
+    public int read(byte[] b, int off, int len) throws IOException {
+        if (bytesRead >= limit) {
+            return -1;
+        }
+
+        final int maxToRead = (int) Math.min(len, limit - bytesRead);
+
+        final int val = in.read(b, off, maxToRead);
+        if (val > 0) {
+            bytesRead += val;
+        }
+        return val;
+    }
+
+    @Override
+    public long skip(final long n) throws IOException {
+        final long skipped = in.skip(Math.min(n, limit - bytesRead));
+        bytesRead += skipped;
+        return skipped;
+    }
+
+    @Override
+    public int available() throws IOException {
+        return in.available();
+    }
+
+    @Override
+    public void close() throws IOException {
+        in.close();
+    }
+
+    @Override
+    public void mark(int readlimit) {
+        in.mark(readlimit);
+    }
+
+    @Override
+    public boolean markSupported() {
+        return in.markSupported();
+    }
+
+    @Override
+    public void reset() throws IOException {
+        in.reset();
+    }
+}

http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/6f26290d/nifi-registry-core/nifi-registry-security-api/pom.xml
----------------------------------------------------------------------
diff --git a/nifi-registry-core/nifi-registry-security-api/pom.xml b/nifi-registry-core/nifi-registry-security-api/pom.xml
new file mode 100644
index 0000000..5100569
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-security-api/pom.xml
@@ -0,0 +1,41 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+  ~ 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.
+  -->
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+    <parent>
+        <artifactId>nifi-registry-core</artifactId>
+        <groupId>org.apache.nifi.registry</groupId>
+        <version>0.3.0-SNAPSHOT</version>
+    </parent>
+    <modelVersion>4.0.0</modelVersion>
+
+    <artifactId>nifi-registry-security-api</artifactId>
+
+    <dependencies>
+        <dependency>
+            <groupId>org.apache.nifi.registry</groupId>
+            <artifactId>nifi-registry-utils</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>javax.servlet</groupId>
+            <artifactId>javax.servlet-api</artifactId>
+            <version>3.1.0</version>
+            <scope>provided</scope>
+        </dependency>
+    </dependencies>
+
+</project>

http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/6f26290d/nifi-registry-core/nifi-registry-security-api/src/main/java/org/apache/nifi/registry/security/authentication/AuthenticationRequest.java
----------------------------------------------------------------------
diff --git a/nifi-registry-core/nifi-registry-security-api/src/main/java/org/apache/nifi/registry/security/authentication/AuthenticationRequest.java b/nifi-registry-core/nifi-registry-security-api/src/main/java/org/apache/nifi/registry/security/authentication/AuthenticationRequest.java
new file mode 100644
index 0000000..72ae50e
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-security-api/src/main/java/org/apache/nifi/registry/security/authentication/AuthenticationRequest.java
@@ -0,0 +1,82 @@
+/*
+ * 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.security.authentication;
+
+import java.io.Serializable;
+
+public class AuthenticationRequest implements Serializable {
+
+    private String username;
+    private Object credentials;
+    private Object details;
+
+    public AuthenticationRequest(String username, Object credentials, Object details) {
+        this.username = username;
+        this.credentials = credentials;
+        this.details = details;
+    }
+
+    public AuthenticationRequest() {}
+
+    public String getUsername() {
+        return username;
+    }
+
+    public void setUsername(String username) {
+        this.username = username;
+    }
+
+    public Object getCredentials() {
+        return credentials;
+    }
+
+    public void setCredentials(Object credentials) {
+        this.credentials = credentials;
+    }
+
+    public Object getDetails() {
+        return details;
+    }
+
+    public void setDetails(Object details) {
+        this.details = details;
+    }
+
+    @Override
+    public boolean equals(Object o) {
+        if (this == o) return true;
+        if (o == null || getClass() != o.getClass()) return false;
+
+        AuthenticationRequest that = (AuthenticationRequest) o;
+
+        return username != null ? username.equals(that.username) : that.username == null;
+    }
+
+    @Override
+    public int hashCode() {
+        return username != null ? username.hashCode() : 0;
+    }
+
+    @Override
+    public String toString() {
+        return "AuthenticationRequest{" +
+                "username='" + username + '\'' +
+                ", credentials=[PROTECTED]" +
+                ", details=" + details +
+                '}';
+    }
+}

http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/6f26290d/nifi-registry-core/nifi-registry-security-api/src/main/java/org/apache/nifi/registry/security/authentication/AuthenticationResponse.java
----------------------------------------------------------------------
diff --git a/nifi-registry-core/nifi-registry-security-api/src/main/java/org/apache/nifi/registry/security/authentication/AuthenticationResponse.java b/nifi-registry-core/nifi-registry-security-api/src/main/java/org/apache/nifi/registry/security/authentication/AuthenticationResponse.java
new file mode 100644
index 0000000..b8eb721
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-security-api/src/main/java/org/apache/nifi/registry/security/authentication/AuthenticationResponse.java
@@ -0,0 +1,98 @@
+/*
+ * 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.security.authentication;
+
+import java.io.Serializable;
+
+/**
+ * Authentication response for a user login attempt.
+ */
+public class AuthenticationResponse implements Serializable {
+
+    private final String identity;
+    private final String username;
+    private final long expiration;
+    private final String issuer;
+
+    /**
+     * Creates an authentication response. The username and how long the authentication is valid in milliseconds
+     *
+     * @param identity The user identity
+     * @param username The username
+     * @param expiration The expiration in milliseconds
+     * @param issuer The issuer of the token
+     */
+    public AuthenticationResponse(final String identity, final String username, final long expiration, final String issuer) {
+        this.identity = identity;
+        this.username = username;
+        this.expiration = expiration;
+        this.issuer = issuer;
+    }
+
+    public String getIdentity() {
+        return identity;
+    }
+
+    public String getUsername() {
+        return username;
+    }
+
+    public String getIssuer() {
+        return issuer;
+    }
+
+    /**
+     * Returns the expiration of a given authentication in milliseconds.
+     *
+     * @return The expiration in milliseconds
+     */
+    public long getExpiration() {
+        return expiration;
+    }
+
+    @Override
+    public boolean equals(Object o) {
+        if (this == o) return true;
+        if (o == null || getClass() != o.getClass()) return false;
+
+        AuthenticationResponse that = (AuthenticationResponse) o;
+
+        if (expiration != that.expiration) return false;
+        if (identity != null ? !identity.equals(that.identity) : that.identity != null) return false;
+        if (username != null ? !username.equals(that.username) : that.username != null) return false;
+        return issuer != null ? issuer.equals(that.issuer) : that.issuer == null;
+    }
+
+    @Override
+    public int hashCode() {
+        int result = identity != null ? identity.hashCode() : 0;
+        result = 31 * result + (username != null ? username.hashCode() : 0);
+        result = 31 * result + (int) (expiration ^ (expiration >>> 32));
+        result = 31 * result + (issuer != null ? issuer.hashCode() : 0);
+        return result;
+    }
+
+    @Override
+    public String toString() {
+        return "AuthenticationResponse{" +
+                "identity='" + identity + '\'' +
+                ", username='" + username + '\'' +
+                ", expiration=" + expiration +
+                ", issuer='" + issuer + '\'' +
+                '}';
+    }
+}


[24/51] [partial] nifi-registry git commit: NIFIREG-201 Refactoring project structure to better isolate extensions

Posted by kd...@apache.org.
http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/6f26290d/nifi-registry-core/nifi-registry-properties/src/test/groovy/org/apache/nifi/registry/properties/AESSensitivePropertyProviderTest.groovy
----------------------------------------------------------------------
diff --git a/nifi-registry-core/nifi-registry-properties/src/test/groovy/org/apache/nifi/registry/properties/AESSensitivePropertyProviderTest.groovy b/nifi-registry-core/nifi-registry-properties/src/test/groovy/org/apache/nifi/registry/properties/AESSensitivePropertyProviderTest.groovy
new file mode 100644
index 0000000..98fdd9b
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-properties/src/test/groovy/org/apache/nifi/registry/properties/AESSensitivePropertyProviderTest.groovy
@@ -0,0 +1,471 @@
+/*
+ * 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.properties
+
+import org.bouncycastle.jce.provider.BouncyCastleProvider
+import org.bouncycastle.util.encoders.DecoderException
+import org.bouncycastle.util.encoders.Hex
+import org.junit.*
+import org.junit.runner.RunWith
+import org.junit.runners.JUnit4
+import org.slf4j.Logger
+import org.slf4j.LoggerFactory
+
+import javax.crypto.Cipher
+import javax.crypto.spec.IvParameterSpec
+import javax.crypto.spec.SecretKeySpec
+import java.nio.charset.StandardCharsets
+import java.security.SecureRandom
+import java.security.Security
+
+@RunWith(JUnit4.class)
+class AESSensitivePropertyProviderTest extends GroovyTestCase {
+    private static final Logger logger = LoggerFactory.getLogger(AESSensitivePropertyProviderTest.class)
+
+    private static final String KEY_128_HEX = "0123456789ABCDEFFEDCBA9876543210"
+    private static final String KEY_256_HEX = KEY_128_HEX * 2
+    private static final int IV_LENGTH = AESSensitivePropertyProvider.getIvLength()
+
+    private static final List<Integer> KEY_SIZES = getAvailableKeySizes()
+
+    private static final SecureRandom secureRandom = new SecureRandom()
+
+    private static final Base64.Encoder encoder = Base64.encoder
+    private static final Base64.Decoder decoder = Base64.decoder
+
+    @BeforeClass
+    static void setUpOnce() throws Exception {
+        Security.addProvider(new BouncyCastleProvider())
+
+        logger.metaClass.methodMissing = { String name, args ->
+            logger.info("[${name?.toUpperCase()}] ${(args as List).join(" ")}")
+        }
+    }
+
+    @Before
+    void setUp() throws Exception {
+
+    }
+
+    @After
+    void tearDown() throws Exception {
+
+    }
+
+    private static Cipher getCipher(boolean encrypt = true, int keySize = 256, byte[] iv = [0x00] * IV_LENGTH) {
+        Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding")
+        String key = getKeyOfSize(keySize)
+        cipher.init((encrypt ? Cipher.ENCRYPT_MODE : Cipher.DECRYPT_MODE) as int, new SecretKeySpec(Hex.decode(key), "AES"), new IvParameterSpec(iv))
+        logger.setup("Initialized a cipher in ${encrypt ? "encrypt" : "decrypt"} mode with a key of length ${keySize} bits")
+        cipher
+    }
+
+    private static String getKeyOfSize(int keySize = 256) {
+        switch (keySize) {
+            case 128:
+                return KEY_128_HEX
+            case 192:
+            case 256:
+                if (Cipher.getMaxAllowedKeyLength("AES") < keySize) {
+                    throw new IllegalArgumentException("The JCE unlimited strength cryptographic jurisdiction policies are not installed, so the max key size is 128 bits")
+                }
+                return KEY_256_HEX[0..<(keySize / 4)]
+            default:
+                throw new IllegalArgumentException("Key size ${keySize} bits is not valid")
+        }
+    }
+
+    private static List<Integer> getAvailableKeySizes() {
+        if (Cipher.getMaxAllowedKeyLength("AES") > 128) {
+            [128, 192, 256]
+        } else {
+            [128]
+        }
+    }
+
+    private static String manipulateString(String input, int start = 0, int end = input?.length()) {
+        if ((input[start..end] as List).unique().size() == 1) {
+            throw new IllegalArgumentException("Can't manipulate a String where the entire range is identical [${input[start..end]}]")
+        }
+        List shuffled = input[start..end] as List
+        Collections.shuffle(shuffled)
+        String reconstituted = input[0..<start] + shuffled.join() + input[end + 1..-1]
+        return reconstituted != input ? reconstituted : manipulateString(input, start, end)
+    }
+
+    @Test
+    void testShouldProtectValue() throws Exception {
+        final String PLAINTEXT = "This is a plaintext value"
+
+        // Act
+        Map<Integer, String> CIPHER_TEXTS = KEY_SIZES.collectEntries { int keySize ->
+            SensitivePropertyProvider spp = new AESSensitivePropertyProvider(Hex.decode(getKeyOfSize(keySize)))
+            logger.info("Initialized ${spp.name} with key size ${keySize}")
+            [(keySize): spp.protect(PLAINTEXT)]
+        }
+        CIPHER_TEXTS.each { ks, ct -> logger.info("Encrypted for ${ks} length key: ${ct}") }
+
+        // Assert
+
+        // The IV generation is part of #protect, so the expected cipher text values must be generated after #protect has run
+        Map<Integer, Cipher> decryptionCiphers = CIPHER_TEXTS.collectEntries { int keySize, String cipherText ->
+            // The 12 byte IV is the first 16 Base64-encoded characters of the "complete" cipher text
+            byte[] iv = decoder.decode(cipherText[0..<16])
+            [(keySize): getCipher(false, keySize, iv)]
+        }
+        Map<Integer, String> plaintexts = decryptionCiphers.collectEntries { Map.Entry<Integer, Cipher> e ->
+            String cipherTextWithoutIVAndDelimiter = CIPHER_TEXTS[e.key][18..-1]
+            String plaintext = new String(e.value.doFinal(decoder.decode(cipherTextWithoutIVAndDelimiter)), StandardCharsets.UTF_8)
+            [(e.key): plaintext]
+        }
+        CIPHER_TEXTS.each { key, ct -> logger.expected("Cipher text for ${key} length key: ${ct}") }
+
+        assert plaintexts.every { int ks, String pt -> pt == PLAINTEXT }
+    }
+
+    @Test
+    void testShouldHandleProtectEmptyValue() throws Exception {
+        final List<String> EMPTY_PLAINTEXTS = ["", "    ", null]
+
+        // Act
+        KEY_SIZES.collectEntries { int keySize ->
+            SensitivePropertyProvider spp = new AESSensitivePropertyProvider(Hex.decode(getKeyOfSize(keySize)))
+            logger.info("Initialized ${spp.name} with key size ${keySize}")
+            EMPTY_PLAINTEXTS.each { String emptyPlaintext ->
+                def msg = shouldFail(IllegalArgumentException) {
+                    spp.protect(emptyPlaintext)
+                }
+                logger.expected("${msg} for keySize ${keySize} and plaintext [${emptyPlaintext}]")
+
+                // Assert
+                assert msg == "Cannot encrypt an empty value"
+            }
+        }
+    }
+
+    @Test
+    void testShouldUnprotectValue() throws Exception {
+        // Arrange
+        final String PLAINTEXT = "This is a plaintext value"
+
+        Map<Integer, Cipher> encryptionCiphers = KEY_SIZES.collectEntries { int keySize ->
+            byte[] iv = new byte[IV_LENGTH]
+            secureRandom.nextBytes(iv)
+            [(keySize): getCipher(true, keySize, iv)]
+        }
+
+        Map<Integer, String> CIPHER_TEXTS = encryptionCiphers.collectEntries { Map.Entry<Integer, Cipher> e ->
+            String iv = encoder.encodeToString(e.value.getIV())
+            String cipherText = encoder.encodeToString(e.value.doFinal(PLAINTEXT.getBytes(StandardCharsets.UTF_8)))
+            [(e.key): "${iv}||${cipherText}"]
+        }
+        CIPHER_TEXTS.each { key, ct -> logger.expected("Cipher text for ${key} length key: ${ct}") }
+
+        // Act
+        Map<Integer, String> plaintexts = CIPHER_TEXTS.collectEntries { int keySize, String cipherText ->
+            SensitivePropertyProvider spp = new AESSensitivePropertyProvider(Hex.decode(getKeyOfSize(keySize)))
+            logger.info("Initialized ${spp.name} with key size ${keySize}")
+            [(keySize): spp.unprotect(cipherText)]
+        }
+        plaintexts.each { ks, pt -> logger.info("Decrypted for ${ks} length key: ${pt}") }
+
+        // Assert
+        assert plaintexts.every { int ks, String pt -> pt == PLAINTEXT }
+    }
+
+    /**
+     * Tests inputs where the entire String is empty/blank space/{@code null}.
+     *
+     * @throws Exception
+     */
+    @Test
+    void testShouldHandleUnprotectEmptyValue() throws Exception {
+        // Arrange
+        final List<String> EMPTY_CIPHER_TEXTS = ["", "    ", null]
+
+        // Act
+        KEY_SIZES.each { int keySize ->
+            SensitivePropertyProvider spp = new AESSensitivePropertyProvider(Hex.decode(getKeyOfSize(keySize)))
+            logger.info("Initialized ${spp.name} with key size ${keySize}")
+            EMPTY_CIPHER_TEXTS.each { String emptyCipherText ->
+                def msg = shouldFail(IllegalArgumentException) {
+                    spp.unprotect(emptyCipherText)
+                }
+                logger.expected("${msg} for keySize ${keySize} and cipher text [${emptyCipherText}]")
+
+                // Assert
+                assert msg == "Cannot decrypt a cipher text shorter than ${AESSensitivePropertyProvider.minCipherTextLength} chars".toString()
+            }
+        }
+    }
+
+    @Test
+    void testShouldUnprotectValueWithWhitespace() throws Exception {
+        // Arrange
+        final String PLAINTEXT = "This is a plaintext value"
+
+        Map<Integer, Cipher> encryptionCiphers = KEY_SIZES.collectEntries { int keySize ->
+            byte[] iv = new byte[IV_LENGTH]
+            secureRandom.nextBytes(iv)
+            [(keySize): getCipher(true, keySize, iv)]
+        }
+
+        Map<Integer, String> CIPHER_TEXTS = encryptionCiphers.collectEntries { Map.Entry<Integer, Cipher> e ->
+            String iv = encoder.encodeToString(e.value.getIV())
+            String cipherText = encoder.encodeToString(e.value.doFinal(PLAINTEXT.getBytes(StandardCharsets.UTF_8)))
+            [(e.key): "${iv}||${cipherText}"]
+        }
+        CIPHER_TEXTS.each { key, ct -> logger.expected("Cipher text for ${key} length key: ${ct}") }
+
+        // Act
+        Map<Integer, String> plaintexts = CIPHER_TEXTS.collectEntries { int keySize, String cipherText ->
+            SensitivePropertyProvider spp = new AESSensitivePropertyProvider(Hex.decode(getKeyOfSize(keySize)))
+            logger.info("Initialized ${spp.name} with key size ${keySize}")
+            [(keySize): spp.unprotect("\t" + cipherText + "\n")]
+        }
+        plaintexts.each { ks, pt -> logger.info("Decrypted for ${ks} length key: ${pt}") }
+
+        // Assert
+        assert plaintexts.every { int ks, String pt -> pt == PLAINTEXT }
+    }
+
+    @Test
+    void testShouldHandleUnprotectMalformedValue() throws Exception {
+        // Arrange
+        final String PLAINTEXT = "This is a plaintext value"
+
+        // Act
+        KEY_SIZES.each { int keySize ->
+            SensitivePropertyProvider spp = new AESSensitivePropertyProvider(Hex.decode(getKeyOfSize(keySize)))
+            logger.info("Initialized ${spp.name} with key size ${keySize}")
+            String cipherText = spp.protect(PLAINTEXT)
+            // Swap two characters in the cipher text
+            final String MALFORMED_CIPHER_TEXT = manipulateString(cipherText, 25, 28)
+            logger.info("Manipulated ${cipherText} to\n${MALFORMED_CIPHER_TEXT.padLeft(163)}")
+
+            def msg = shouldFail(SensitivePropertyProtectionException) {
+                spp.unprotect(MALFORMED_CIPHER_TEXT)
+            }
+            logger.expected("${msg} for keySize ${keySize} and cipher text [${MALFORMED_CIPHER_TEXT}]")
+
+            // Assert
+            assert msg == "Error decrypting a protected value"
+        }
+    }
+
+    @Test
+    void testShouldHandleUnprotectMissingIV() throws Exception {
+        // Arrange
+        final String PLAINTEXT = "This is a plaintext value"
+
+        // Act
+        KEY_SIZES.each { int keySize ->
+            SensitivePropertyProvider spp = new AESSensitivePropertyProvider(Hex.decode(getKeyOfSize(keySize)))
+            logger.info("Initialized ${spp.name} with key size ${keySize}")
+            String cipherText = spp.protect(PLAINTEXT)
+            // Remove the IV from the "complete" cipher text
+            final String MISSING_IV_CIPHER_TEXT = cipherText[18..-1]
+            logger.info("Manipulated ${cipherText} to\n${MISSING_IV_CIPHER_TEXT.padLeft(163)}")
+
+            def msg = shouldFail(IllegalArgumentException) {
+                spp.unprotect(MISSING_IV_CIPHER_TEXT)
+            }
+            logger.expected("${msg} for keySize ${keySize} and cipher text [${MISSING_IV_CIPHER_TEXT}]")
+
+            // Remove the IV from the "complete" cipher text but keep the delimiter
+            final String MISSING_IV_CIPHER_TEXT_WITH_DELIMITER = cipherText[16..-1]
+            logger.info("Manipulated ${cipherText} to\n${MISSING_IV_CIPHER_TEXT_WITH_DELIMITER.padLeft(163)}")
+
+            def msgWithDelimiter = shouldFail(DecoderException) {
+                spp.unprotect(MISSING_IV_CIPHER_TEXT_WITH_DELIMITER)
+            }
+            logger.expected("${msgWithDelimiter} for keySize ${keySize} and cipher text [${MISSING_IV_CIPHER_TEXT_WITH_DELIMITER}]")
+
+            // Assert
+            assert msg == "The cipher text does not contain the delimiter || -- it should be of the form Base64(IV) || Base64(cipherText)"
+
+            // Assert
+            assert msgWithDelimiter =~ "unable to decode base64 string"
+        }
+    }
+
+    /**
+     * Tests inputs which have a valid IV and delimiter but no "cipher text".
+     *
+     * @throws Exception
+     */
+    @Test
+    void testShouldHandleUnprotectEmptyCipherText() throws Exception {
+        // Arrange
+        final String IV_AND_DELIMITER = "${encoder.encodeToString("Bad IV value".getBytes(StandardCharsets.UTF_8))}||"
+        logger.info("IV and delimiter: ${IV_AND_DELIMITER}")
+
+        final List<String> EMPTY_CIPHER_TEXTS = ["", "      ", "\n"].collect { "${IV_AND_DELIMITER}${it}" }
+
+        // Act
+        KEY_SIZES.each { int keySize ->
+            SensitivePropertyProvider spp = new AESSensitivePropertyProvider(Hex.decode(getKeyOfSize(keySize)))
+            logger.info("Initialized ${spp.name} with key size ${keySize}")
+            EMPTY_CIPHER_TEXTS.each { String emptyCipherText ->
+                def msg = shouldFail(IllegalArgumentException) {
+                    spp.unprotect(emptyCipherText)
+                }
+                logger.expected("${msg} for keySize ${keySize} and cipher text [${emptyCipherText}]")
+
+                // Assert
+                assert msg == "Cannot decrypt a cipher text shorter than ${AESSensitivePropertyProvider.minCipherTextLength} chars".toString()
+            }
+        }
+    }
+
+    @Test
+    void testShouldHandleUnprotectMalformedIV() throws Exception {
+        // Arrange
+        final String PLAINTEXT = "This is a plaintext value"
+
+        // Act
+        KEY_SIZES.each { int keySize ->
+            SensitivePropertyProvider spp = new AESSensitivePropertyProvider(Hex.decode(getKeyOfSize(keySize)))
+            logger.info("Initialized ${spp.name} with key size ${keySize}")
+            String cipherText = spp.protect(PLAINTEXT)
+            // Swap two characters in the IV
+            final String MALFORMED_IV_CIPHER_TEXT = manipulateString(cipherText, 8, 11)
+            logger.info("Manipulated ${cipherText} to\n${MALFORMED_IV_CIPHER_TEXT.padLeft(163)}")
+
+            def msg = shouldFail(SensitivePropertyProtectionException) {
+                spp.unprotect(MALFORMED_IV_CIPHER_TEXT)
+            }
+            logger.expected("${msg} for keySize ${keySize} and cipher text [${MALFORMED_IV_CIPHER_TEXT}]")
+
+            // Assert
+            assert msg == "Error decrypting a protected value"
+        }
+    }
+
+    @Test
+    void testShouldGetIdentifierKeyWithDifferentMaxKeyLengths() throws Exception {
+        // Arrange
+        def keys = getAvailableKeySizes().collectEntries { int keySize ->
+            [(keySize): getKeyOfSize(keySize)]
+        }
+        logger.info("Keys: ${keys}")
+
+        // Act
+        keys.each { int size, String key ->
+            String identifierKey = new AESSensitivePropertyProvider(key).getIdentifierKey()
+            logger.info("Identifier key: ${identifierKey} for size ${size}")
+
+            // Assert
+            assert identifierKey =~ /aes\/gcm\/${size}/
+        }
+    }
+
+    @Test
+    void testShouldNotAllowEmptyKey() throws Exception {
+        // Arrange
+        final String INVALID_KEY = ""
+
+        // Act
+        def msg = shouldFail(SensitivePropertyProtectionException) {
+            AESSensitivePropertyProvider spp = new AESSensitivePropertyProvider(INVALID_KEY)
+        }
+
+        // Assert
+        assert msg == "The key cannot be empty"
+    }
+
+    @Test
+    void testShouldNotAllowIncorrectlySizedKey() throws Exception {
+        // Arrange
+        final String INVALID_KEY = "Z" * 31
+
+        // Act
+        def msg = shouldFail(SensitivePropertyProtectionException) {
+            AESSensitivePropertyProvider spp = new AESSensitivePropertyProvider(INVALID_KEY)
+        }
+
+        // Assert
+        assert msg == "The key must be a valid hexadecimal key"
+    }
+
+    @Test
+    void testShouldNotAllowInvalidKey() throws Exception {
+        // Arrange
+        final String INVALID_KEY = "Z" * 32
+
+        // Act
+        def msg = shouldFail(SensitivePropertyProtectionException) {
+            AESSensitivePropertyProvider spp = new AESSensitivePropertyProvider(INVALID_KEY)
+        }
+
+        // Assert
+        assert msg == "The key must be a valid hexadecimal key"
+    }
+
+    /**
+     * This test is to ensure internal consistency and allow for encrypting value for various property files
+     */
+    @Test
+    void testShouldEncryptArbitraryValues() {
+        // Arrange
+        def values = ["thisIsABadPassword", "thisIsABadSensitiveKeyPassword", "thisIsABadKeystorePassword", "thisIsABadKeyPassword", "thisIsABadTruststorePassword", "This is an encrypted banner message", "nififtw!"]
+
+        String key = "2C576A9585DB862F5ECBEE5B4FFFCCA1" //getKeyOfSize(128)
+        // key = "0" * 64
+
+        SensitivePropertyProvider spp = new AESSensitivePropertyProvider(key)
+
+        // Act
+        def encryptedValues = values.collect { String v ->
+            def encryptedValue = spp.protect(v)
+            logger.info("${v} -> ${encryptedValue}")
+            def (String iv, String cipherText) = encryptedValue.tokenize("||")
+            logger.info("Normal Base64 encoding would be ${encoder.encodeToString(decoder.decode(iv))}||${encoder.encodeToString(decoder.decode(cipherText))}")
+            encryptedValue
+        }
+
+        // Assert
+        assert values == encryptedValues.collect { spp.unprotect(it) }
+    }
+
+    /**
+     * This test is to ensure external compatibility in case someone encodes the encrypted value with Base64 and does not remove the padding
+     */
+    @Test
+    void testShouldDecryptPaddedValueWith256BitKey() {
+        // Arrange
+        Assume.assumeTrue("JCE unlimited strength crypto policy must be installed for this test", Cipher.getMaxAllowedKeyLength("AES") > 128)
+
+        final String EXPECTED_VALUE = getKeyOfSize(256) // "thisIsABadKeyPassword"
+        String cipherText = "aYDkDKys1ENr3gp+||sTBPpMlIvHcOLTGZlfWct8r9RY8BuDlDkoaYmGJ/9m9af9tZIVzcnDwvYQAaIKxRGF7vI2yrY7Xd6x9GTDnWGiGiRXlaP458BBMMgfzH2O8"
+        String unpaddedCipherText = cipherText.replaceAll("=", "")
+
+        String key = "AAAABBBBCCCCDDDDEEEEFFFF00001111" * 2 // getKeyOfSize(256)
+
+        SensitivePropertyProvider spp = new AESSensitivePropertyProvider(key)
+
+        // Act
+        String rawValue = spp.unprotect(cipherText)
+        logger.info("Decrypted ${cipherText} to ${rawValue}")
+        String rawUnpaddedValue = spp.unprotect(unpaddedCipherText)
+        logger.info("Decrypted ${unpaddedCipherText} to ${rawUnpaddedValue}")
+
+        // Assert
+        assert rawValue == EXPECTED_VALUE
+        assert rawUnpaddedValue == EXPECTED_VALUE
+    }
+}

http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/6f26290d/nifi-registry-core/nifi-registry-properties/src/test/groovy/org/apache/nifi/registry/properties/NiFiRegistryPropertiesGroovyTest.groovy
----------------------------------------------------------------------
diff --git a/nifi-registry-core/nifi-registry-properties/src/test/groovy/org/apache/nifi/registry/properties/NiFiRegistryPropertiesGroovyTest.groovy b/nifi-registry-core/nifi-registry-properties/src/test/groovy/org/apache/nifi/registry/properties/NiFiRegistryPropertiesGroovyTest.groovy
new file mode 100644
index 0000000..0c403cd
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-properties/src/test/groovy/org/apache/nifi/registry/properties/NiFiRegistryPropertiesGroovyTest.groovy
@@ -0,0 +1,121 @@
+/*
+ * 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.properties
+
+import org.junit.*
+import org.junit.runner.RunWith
+import org.junit.runners.JUnit4
+import org.slf4j.Logger
+import org.slf4j.LoggerFactory
+
+@RunWith(JUnit4.class)
+class NiFiRegistryPropertiesGroovyTest extends GroovyTestCase {
+    private static final Logger logger = LoggerFactory.getLogger(NiFiRegistryPropertiesGroovyTest.class)
+
+    @BeforeClass
+    static void setUpOnce() throws Exception {
+        logger.metaClass.methodMissing = { String name, args ->
+            logger.info("[${name?.toUpperCase()}] ${(args as List).join(" ")}")
+        }
+    }
+
+    @Before
+    void setUp() throws Exception {
+    }
+
+    @After
+    void tearDown() throws Exception {
+    }
+
+    @AfterClass
+    static void tearDownOnce() {
+    }
+
+    private static NiFiRegistryProperties loadFromFile(String propertiesFilePath) {
+        String filePath
+        try {
+            filePath = NiFiRegistryPropertiesGroovyTest.class.getResource(propertiesFilePath).toURI().getPath()
+        } catch (URISyntaxException ex) {
+            throw new RuntimeException("Cannot load properties file due to "
+                    + ex.getLocalizedMessage(), ex)
+        }
+
+        NiFiRegistryProperties properties = new NiFiRegistryProperties()
+        FileReader reader = new FileReader(filePath)
+
+        try {
+            properties.load(reader)
+            logger.info("Loaded {} properties from {}", properties.size(), filePath)
+
+            return properties
+        } catch (final Exception ex) {
+            logger.error("Cannot load properties file due to " + ex.getLocalizedMessage())
+            throw new RuntimeException("Cannot load properties file due to "
+                    + ex.getLocalizedMessage(), ex)
+        }
+    }
+
+    @Test
+    void testConstructorShouldCreateNewInstance() throws Exception {
+        // Arrange
+
+        // Act
+        NiFiRegistryProperties NiFiRegistryProperties = new NiFiRegistryProperties()
+        logger.info("NiFiRegistryProperties has ${NiFiRegistryProperties.size()} properties: ${NiFiRegistryProperties.getPropertyKeys()}")
+
+        // Assert
+        assert NiFiRegistryProperties.size() == 0
+        assert NiFiRegistryProperties.getPropertyKeys() == [] as Set
+    }
+
+    @Test
+    void testConstructorShouldAcceptDefaultProperties() throws Exception {
+        // Arrange
+        Properties rawProperties = new Properties()
+        rawProperties.setProperty("key", "value")
+        logger.info("rawProperties has ${rawProperties.size()} properties: ${rawProperties.stringPropertyNames()}")
+        assert rawProperties.size() == 1
+
+        // Act
+        NiFiRegistryProperties NiFiRegistryProperties = new NiFiRegistryProperties(rawProperties)
+        logger.info("NiFiRegistryProperties has ${NiFiRegistryProperties.size()} properties: ${NiFiRegistryProperties.getPropertyKeys()}")
+
+        // Assert
+        assert NiFiRegistryProperties.size() == 1
+        assert NiFiRegistryProperties.getPropertyKeys() == ["key"] as Set
+    }
+
+    @Test
+    void testShouldAllowMultipleInstances() throws Exception {
+        // Arrange
+
+        // Act
+        NiFiRegistryProperties properties = new NiFiRegistryProperties()
+        properties.setProperty("key", "value")
+        logger.info("niFiProperties has ${properties.size()} properties: ${properties.getPropertyKeys()}")
+        NiFiRegistryProperties emptyProperties = new NiFiRegistryProperties()
+        logger.info("emptyProperties has ${emptyProperties.size()} properties: ${emptyProperties.getPropertyKeys()}")
+
+        // Assert
+        assert properties.size() == 1
+        assert properties.getPropertyKeys() == ["key"] as Set
+
+        assert emptyProperties.size() == 0
+        assert emptyProperties.getPropertyKeys() == [] as Set
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/6f26290d/nifi-registry-core/nifi-registry-properties/src/test/groovy/org/apache/nifi/registry/properties/NiFiRegistryPropertiesLoaderGroovyTest.groovy
----------------------------------------------------------------------
diff --git a/nifi-registry-core/nifi-registry-properties/src/test/groovy/org/apache/nifi/registry/properties/NiFiRegistryPropertiesLoaderGroovyTest.groovy b/nifi-registry-core/nifi-registry-properties/src/test/groovy/org/apache/nifi/registry/properties/NiFiRegistryPropertiesLoaderGroovyTest.groovy
new file mode 100644
index 0000000..58c8087
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-properties/src/test/groovy/org/apache/nifi/registry/properties/NiFiRegistryPropertiesLoaderGroovyTest.groovy
@@ -0,0 +1,264 @@
+/*
+ * 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.properties
+
+import org.bouncycastle.jce.provider.BouncyCastleProvider
+import org.junit.After
+import org.junit.AfterClass
+import org.junit.Before
+import org.junit.BeforeClass
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.JUnit4
+import org.slf4j.Logger
+import org.slf4j.LoggerFactory
+
+import javax.crypto.Cipher
+import java.security.Security
+
+@RunWith(JUnit4.class)
+class NiFiRegistryPropertiesLoaderGroovyTest extends GroovyTestCase {
+
+    private static final Logger logger = LoggerFactory.getLogger(NiFiRegistryPropertiesLoaderGroovyTest.class)
+
+    private static final String KEYSTORE_PASSWORD_KEY = NiFiRegistryProperties.SECURITY_KEYSTORE_PASSWD
+    private static final String KEY_PASSWORD_KEY = NiFiRegistryProperties.SECURITY_KEY_PASSWD
+    private static final String TRUSTSTORE_PASSWORD_KEY = NiFiRegistryProperties.SECURITY_TRUSTSTORE_PASSWD
+
+    private static final String KEY_HEX_128 = "0123456789ABCDEFFEDCBA9876543210"
+    private static final String KEY_HEX_256 = KEY_HEX_128 * 2
+    private static final String KEY_HEX = Cipher.getMaxAllowedKeyLength("AES") < 256 ? KEY_HEX_128 : KEY_HEX_256
+
+    private static final String PASSWORD_KEY_HEX_128 = "2C576A9585DB862F5ECBEE5B4FFFCCA1"
+
+    @BeforeClass
+    public static void setUpOnce() throws Exception {
+        Security.addProvider(new BouncyCastleProvider())
+
+        logger.metaClass.methodMissing = { String name, args ->
+            logger.info("[${name?.toUpperCase()}] ${(args as List).join(" ")}")
+        }
+    }
+
+    @Before
+    public void setUp() throws Exception {
+    }
+
+    @After
+    public void tearDown() throws Exception {
+        // Clear the sensitive property providers between runs
+        NiFiRegistryPropertiesLoader.@sensitivePropertyProviderFactory = null
+    }
+
+    @AfterClass
+    public static void tearDownOnce() {
+    }
+
+    @Test
+    public void testConstructorShouldCreateNewInstance() throws Exception {
+        // Arrange
+
+        // Act
+        NiFiRegistryPropertiesLoader propertiesLoader = new NiFiRegistryPropertiesLoader()
+
+        // Assert
+        assert !propertiesLoader.@keyHex
+    }
+
+    @Test
+    public void testShouldCreateInstanceWithKey() throws Exception {
+        // Arrange
+
+        // Act
+        NiFiRegistryPropertiesLoader propertiesLoader = NiFiRegistryPropertiesLoader.withKey(KEY_HEX)
+
+        // Assert
+        assert propertiesLoader.@keyHex == KEY_HEX
+    }
+
+    @Test
+    public void testConstructorShouldCreateMultipleInstances() throws Exception {
+        // Arrange
+        NiFiRegistryPropertiesLoader propertiesLoader1 = NiFiRegistryPropertiesLoader.withKey(KEY_HEX)
+
+        // Act
+        NiFiRegistryPropertiesLoader propertiesLoader2 = new NiFiRegistryPropertiesLoader()
+
+        // Assert
+        assert propertiesLoader1.@keyHex == KEY_HEX
+        assert !propertiesLoader2.@keyHex
+    }
+
+    @Test
+    public void testShouldGetDefaultProviderKey() throws Exception {
+        // Arrange
+        final String expectedProviderKey = "aes/gcm/${Cipher.getMaxAllowedKeyLength("AES") > 128 ? 256 : 128}"
+        logger.info("Expected provider key: ${expectedProviderKey}")
+
+        // Act
+        String defaultKey = NiFiRegistryPropertiesLoader.getDefaultProviderKey()
+        logger.info("Default key: ${defaultKey}")
+        // Assert
+        assert defaultKey == expectedProviderKey
+    }
+
+    @Test
+    public void testShouldInitializeSensitivePropertyProviderFactory() throws Exception {
+        // Arrange
+        NiFiRegistryPropertiesLoader propertiesLoader = new NiFiRegistryPropertiesLoader()
+
+        // Act
+        propertiesLoader.initializeSensitivePropertyProviderFactory()
+
+        // Assert
+        assert propertiesLoader.@sensitivePropertyProviderFactory
+    }
+
+    @Test
+    public void testShouldLoadUnprotectedPropertiesFromFile() throws Exception {
+        // Arrange
+        File unprotectedFile = new File("src/test/resources/conf/nifi-registry.properties")
+        NiFiRegistryPropertiesLoader propertiesLoader = new NiFiRegistryPropertiesLoader()
+
+        // Act
+        NiFiRegistryProperties properties = propertiesLoader.load(unprotectedFile)
+
+        // Assert
+        assert properties.size() > 0
+
+        // Ensure it is not a ProtectedNiFiProperties
+        assert properties instanceof NiFiRegistryProperties
+    }
+
+    @Test
+    public void testShouldNotLoadUnprotectedPropertiesFromNullFile() throws Exception {
+        // Arrange
+        NiFiRegistryPropertiesLoader propertiesLoader = new NiFiRegistryPropertiesLoader()
+
+        // Act
+        def msg = shouldFail(IllegalArgumentException) {
+            NiFiRegistryProperties properties = propertiesLoader.load(null as File)
+        }
+        logger.info(msg)
+
+        // Assert
+        assert msg == "NiFi Registry properties file missing or unreadable"
+    }
+
+    @Test
+    public void testShouldNotLoadUnprotectedPropertiesFromMissingFile() throws Exception {
+        // Arrange
+        File missingFile = new File("src/test/resources/conf/nifi-registry.missing.properties")
+        assert !missingFile.exists()
+
+        NiFiRegistryPropertiesLoader propertiesLoader = new NiFiRegistryPropertiesLoader()
+
+        // Act
+        def msg = shouldFail(IllegalArgumentException) {
+            NiFiRegistryProperties properties = propertiesLoader.load(missingFile)
+        }
+        logger.info(msg)
+
+        // Assert
+        assert msg == "NiFi Registry properties file missing or unreadable"
+    }
+
+    @Test
+    public void testShouldLoadUnprotectedPropertiesFromPath() throws Exception {
+        // Arrange
+        File unprotectedFile = new File("src/test/resources/conf/nifi-registry.properties")
+        NiFiRegistryPropertiesLoader propertiesLoader = new NiFiRegistryPropertiesLoader()
+
+        // Act
+        NiFiRegistryProperties properties = propertiesLoader.load(unprotectedFile.path)
+
+        // Assert
+        assert properties.size() > 0
+
+        // Ensure it is not a ProtectedNiFiProperties
+        assert properties instanceof NiFiRegistryProperties
+    }
+
+    @Test
+    public void testShouldLoadUnprotectedPropertiesFromProtectedFile() throws Exception {
+        // Arrange
+        File protectedFile = new File("src/test/resources/conf/nifi-registry.with_sensitive_props_protected_aes_128.properties")
+        NiFiRegistryPropertiesLoader propertiesLoader = NiFiRegistryPropertiesLoader.withKey(KEY_HEX_128)
+
+        final def EXPECTED_PLAIN_VALUES = [
+                (KEYSTORE_PASSWORD_KEY): "thisIsABadPassword",
+                (KEY_PASSWORD_KEY): "thisIsABadPassword",
+        ]
+
+        // This method is covered in tests above, so safe to use here to retrieve protected properties
+        ProtectedNiFiRegistryProperties protectedNiFiProperties = propertiesLoader.readProtectedPropertiesFromDisk(protectedFile)
+        int totalKeysCount = protectedNiFiProperties.getPropertyKeysIncludingProtectionSchemes().size()
+        int protectedKeysCount = protectedNiFiProperties.getProtectedPropertyKeys().size()
+        logger.info("Read ${totalKeysCount} total properties (${protectedKeysCount} protected) from ${protectedFile.canonicalPath}")
+
+        // Act
+        NiFiRegistryProperties properties = propertiesLoader.load(protectedFile)
+
+        // Assert
+        assert properties.size() == totalKeysCount - protectedKeysCount
+
+        // Ensure that any key marked as protected above is different in this instance
+        protectedNiFiProperties.getProtectedPropertyKeys().keySet().each { String key ->
+            String plainValue = properties.getProperty(key)
+            String protectedValue = protectedNiFiProperties.getProperty(key)
+
+            logger.info("Checking that [${protectedValue}] -> [${plainValue}] == [${EXPECTED_PLAIN_VALUES[key]}]")
+
+            assert plainValue == EXPECTED_PLAIN_VALUES[key]
+            assert plainValue != protectedValue
+            assert plainValue.length() <= protectedValue.length()
+        }
+
+        // Ensure it is not a ProtectedNiFiProperties
+        assert properties instanceof NiFiRegistryProperties
+    }
+
+    @Test
+    public void testShouldUpdateKeyInFactory() throws Exception {
+        // Arrange
+        File originalKeyFile = new File("src/test/resources/conf/nifi-registry.with_sensitive_props_protected_aes_128.properties")
+        File passwordKeyFile = new File("src/test/resources/conf/nifi-registry.with_sensitive_props_protected_aes_128_password.properties")
+        NiFiRegistryPropertiesLoader propertiesLoader = NiFiRegistryPropertiesLoader.withKey(KEY_HEX_128)
+
+        NiFiRegistryProperties properties = propertiesLoader.load(originalKeyFile)
+        logger.info("Read ${properties.size()} total properties from ${originalKeyFile.canonicalPath}")
+
+        // Act
+        NiFiRegistryPropertiesLoader passwordNiFiRegistryPropertiesLoader = NiFiRegistryPropertiesLoader.withKey(PASSWORD_KEY_HEX_128)
+
+        NiFiRegistryProperties passwordProperties = passwordNiFiRegistryPropertiesLoader.load(passwordKeyFile)
+        logger.info("Read ${passwordProperties.size()} total properties from ${passwordKeyFile.canonicalPath}")
+
+        // Assert
+        assert properties.size() == passwordProperties.size()
+
+
+        def readPropertiesAndValues = properties.getPropertyKeys().collectEntries {
+            [(it): properties.getProperty(it)]
+        }
+        def readPasswordPropertiesAndValues = passwordProperties.getPropertyKeys().collectEntries {
+            [(it): passwordProperties.getProperty(it)]
+        }
+
+        assert readPropertiesAndValues == readPasswordPropertiesAndValues
+    }
+}

http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/6f26290d/nifi-registry-core/nifi-registry-properties/src/test/groovy/org/apache/nifi/registry/properties/ProtectedNiFiPropertiesGroovyTest.groovy
----------------------------------------------------------------------
diff --git a/nifi-registry-core/nifi-registry-properties/src/test/groovy/org/apache/nifi/registry/properties/ProtectedNiFiPropertiesGroovyTest.groovy b/nifi-registry-core/nifi-registry-properties/src/test/groovy/org/apache/nifi/registry/properties/ProtectedNiFiPropertiesGroovyTest.groovy
new file mode 100644
index 0000000..86c7fb4
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-properties/src/test/groovy/org/apache/nifi/registry/properties/ProtectedNiFiPropertiesGroovyTest.groovy
@@ -0,0 +1,739 @@
+/*
+ * 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.properties
+
+import org.bouncycastle.jce.provider.BouncyCastleProvider
+import org.junit.After
+import org.junit.AfterClass
+import org.junit.Assume
+import org.junit.Before
+import org.junit.BeforeClass
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.JUnit4
+import org.slf4j.Logger
+import org.slf4j.LoggerFactory
+
+import javax.crypto.Cipher
+import java.security.Security
+
+@RunWith(JUnit4.class)
+class ProtectedNiFiPropertiesGroovyTest extends GroovyTestCase {
+    private static final Logger logger = LoggerFactory.getLogger(ProtectedNiFiPropertiesGroovyTest.class)
+
+    private static final String KEYSTORE_PASSWORD_KEY = NiFiRegistryProperties.SECURITY_KEYSTORE_PASSWD
+    private static final String KEY_PASSWORD_KEY = NiFiRegistryProperties.SECURITY_KEY_PASSWD
+    private static final String TRUSTSTORE_PASSWORD_KEY = NiFiRegistryProperties.SECURITY_TRUSTSTORE_PASSWD
+
+    private static final def DEFAULT_SENSITIVE_PROPERTIES = [
+            KEYSTORE_PASSWORD_KEY,
+            KEY_PASSWORD_KEY,
+            TRUSTSTORE_PASSWORD_KEY
+    ]
+
+    private static final String KEY_HEX_128 = "0123456789ABCDEFFEDCBA9876543210"
+    private static final String KEY_HEX_256 = KEY_HEX_128 * 2
+    private static final String KEY_HEX = Cipher.getMaxAllowedKeyLength("AES") < 256 ? KEY_HEX_128 : KEY_HEX_256
+
+    @BeforeClass
+    static void setUpOnce() throws Exception {
+        Security.addProvider(new BouncyCastleProvider())
+
+        logger.metaClass.methodMissing = { String name, args ->
+            logger.info("[${name?.toUpperCase()}] ${(args as List).join(" ")}")
+        }
+    }
+
+    @Before
+    void setUp() throws Exception {
+    }
+
+    @After
+    void tearDown() throws Exception {
+    }
+
+    @AfterClass
+    static void tearDownOnce() {
+    }
+
+    private static ProtectedNiFiRegistryProperties loadFromResourceFile(String propertiesFilePath) {
+        return loadFromResourceFile(propertiesFilePath, KEY_HEX)
+    }
+
+    private static ProtectedNiFiRegistryProperties loadFromResourceFile(String propertiesFilePath, String keyHex) {
+        File file = fileForResource(propertiesFilePath)
+
+        if (file == null || !file.exists() || !file.canRead()) {
+            String path = (file == null ? "missing file" : file.getAbsolutePath())
+            logger.error("Cannot read from '{}' -- file is missing or not readable", path)
+            throw new IllegalArgumentException("NiFi Registry properties file missing or unreadable")
+        }
+
+        NiFiRegistryProperties properties = new NiFiRegistryProperties()
+        FileReader reader = new FileReader(file)
+
+        try {
+            properties.load(reader)
+            logger.info("Loaded {} properties from {}", properties.size(), file.getAbsolutePath())
+
+            ProtectedNiFiRegistryProperties protectedNiFiProperties = new ProtectedNiFiRegistryProperties(properties)
+
+            // If it has protected keys, inject the SPP
+            if (protectedNiFiProperties.hasProtectedKeys()) {
+                protectedNiFiProperties.addSensitivePropertyProvider(new AESSensitivePropertyProvider(keyHex))
+            }
+
+            return protectedNiFiProperties
+        } catch (final Exception ex) {
+            logger.error("Cannot load properties file due to " + ex.getLocalizedMessage())
+            throw new RuntimeException("Cannot load properties file due to "
+                    + ex.getLocalizedMessage(), ex)
+        }
+    }
+
+    private static File fileForResource(String resourcePath) {
+        String filePath
+        try {
+            URL resourceURL = ProtectedNiFiPropertiesGroovyTest.class.getResource(resourcePath)
+            if (!resourceURL) {
+                throw new RuntimeException("File ${resourcePath} not found in class resources, cannot load.")
+            }
+            filePath = resourceURL.toURI().getPath()
+        } catch (URISyntaxException ex) {
+            throw new RuntimeException("Cannot load resource file due to "
+                    + ex.getLocalizedMessage(), ex)
+        }
+        File file = new File(filePath)
+        return file
+    }
+
+    @Test
+    void testShouldDetectIfPropertyIsSensitive() throws Exception {
+        // Arrange
+        final String INSENSITIVE_PROPERTY_KEY = "nifi.registry.web.http.port"
+        final String SENSITIVE_PROPERTY_KEY = "nifi.registry.security.keystorePasswd"
+
+        ProtectedNiFiRegistryProperties properties = loadFromResourceFile("/conf/nifi-registry.properties")
+
+        // Act
+        boolean bannerIsSensitive = properties.isPropertySensitive(INSENSITIVE_PROPERTY_KEY)
+        logger.info("${INSENSITIVE_PROPERTY_KEY} is ${bannerIsSensitive ? "SENSITIVE" : "NOT SENSITIVE"}")
+        boolean passwordIsSensitive = properties.isPropertySensitive(SENSITIVE_PROPERTY_KEY)
+        logger.info("${SENSITIVE_PROPERTY_KEY} is ${passwordIsSensitive ? "SENSITIVE" : "NOT SENSITIVE"}")
+
+        // Assert
+        assert !bannerIsSensitive
+        assert passwordIsSensitive
+    }
+
+    @Test
+    void testShouldGetDefaultSensitiveProperties() throws Exception {
+        // Arrange
+        logger.info("${DEFAULT_SENSITIVE_PROPERTIES.size()} default sensitive properties: ${DEFAULT_SENSITIVE_PROPERTIES.join(", ")}")
+
+        ProtectedNiFiRegistryProperties properties = loadFromResourceFile("/conf/nifi-registry.properties")
+
+        // Act
+        List defaultSensitiveProperties = properties.getSensitivePropertyKeys()
+        logger.info("${defaultSensitiveProperties.size()} default sensitive properties: ${defaultSensitiveProperties.join(", ")}")
+
+        // Assert
+        assert defaultSensitiveProperties.size() == DEFAULT_SENSITIVE_PROPERTIES.size()
+        assert defaultSensitiveProperties.containsAll(DEFAULT_SENSITIVE_PROPERTIES)
+    }
+
+    @Test
+    void testShouldGetAdditionalSensitiveProperties() throws Exception {
+        // Arrange
+        def completeSensitiveProperties = DEFAULT_SENSITIVE_PROPERTIES + ["nifi.registry.web.http.port", "nifi.registry.web.http.host"]
+        logger.info("${completeSensitiveProperties.size()} total sensitive properties: ${completeSensitiveProperties.join(", ")}")
+
+        ProtectedNiFiRegistryProperties properties = loadFromResourceFile("/conf/nifi-registry.with_additional_sensitive_keys.properties")
+
+        // Act
+        List retrievedSensitiveProperties = properties.getSensitivePropertyKeys()
+        logger.info("${retrievedSensitiveProperties.size()} retrieved sensitive properties: ${retrievedSensitiveProperties.join(", ")}")
+
+        // Assert
+        assert retrievedSensitiveProperties.size() == completeSensitiveProperties.size()
+        assert retrievedSensitiveProperties.containsAll(completeSensitiveProperties)
+    }
+
+    @Test
+    void testGetAdditionalSensitivePropertiesShouldNotIncludeSelf() throws Exception {
+        // Arrange
+        def completeSensitiveProperties = DEFAULT_SENSITIVE_PROPERTIES + ["nifi.registry.web.http.port", "nifi.registry.web.http.host"]
+        logger.info("${completeSensitiveProperties.size()} total sensitive properties: ${completeSensitiveProperties.join(", ")}")
+
+        ProtectedNiFiRegistryProperties properties = loadFromResourceFile("/conf/nifi-registry.with_additional_sensitive_keys.properties")
+
+        // Act
+        List retrievedSensitiveProperties = properties.getSensitivePropertyKeys()
+        logger.info("${retrievedSensitiveProperties.size()} retrieved sensitive properties: ${retrievedSensitiveProperties.join(", ")}")
+
+        // Assert
+        assert retrievedSensitiveProperties.size() == completeSensitiveProperties.size()
+        assert retrievedSensitiveProperties.containsAll(completeSensitiveProperties)
+    }
+
+    /**
+     * In the default (no protection enabled) scenario, a call to retrieve a sensitive property should return the raw value transparently.
+     * @throws Exception
+     */
+    @Test
+    void testShouldGetUnprotectedValueOfSensitiveProperty() throws Exception {
+        // Arrange
+        final String expectedKeystorePassword = "thisIsABadKeystorePassword"
+
+        ProtectedNiFiRegistryProperties properties = loadFromResourceFile("/conf/nifi-registry.with_sensitive_props_unprotected.properties")
+
+        boolean isSensitive = properties.isPropertySensitive(KEYSTORE_PASSWORD_KEY)
+        boolean isProtected = properties.isPropertyProtected(KEYSTORE_PASSWORD_KEY)
+        logger.info("The property is ${isSensitive ? "sensitive" : "not sensitive"} and ${isProtected ? "protected" : "not protected"}")
+
+        // Act
+        String retrievedKeystorePassword = properties.getProperty(KEYSTORE_PASSWORD_KEY)
+        logger.info("${KEYSTORE_PASSWORD_KEY}: ${retrievedKeystorePassword}")
+
+        // Assert
+        assert retrievedKeystorePassword == expectedKeystorePassword
+        assert isSensitive
+        assert !isProtected
+    }
+
+    /**
+     * In the default (no protection enabled) scenario, a call to retrieve a sensitive property (which is empty) should return the raw value transparently.
+     * @throws Exception
+     */
+    @Test
+    void testShouldGetEmptyUnprotectedValueOfSensitiveProperty() throws Exception {
+        // Arrange
+        final String expectedTruststorePassword = ""
+
+        ProtectedNiFiRegistryProperties properties = loadFromResourceFile("/conf/nifi-registry.with_sensitive_props_unprotected.properties")
+
+        boolean isSensitive = properties.isPropertySensitive(TRUSTSTORE_PASSWORD_KEY)
+        boolean isProtected = properties.isPropertyProtected(TRUSTSTORE_PASSWORD_KEY)
+        logger.info("The property is ${isSensitive ? "sensitive" : "not sensitive"} and ${isProtected ? "protected" : "not protected"}")
+
+        // Act
+        NiFiRegistryProperties unprotectedProperties = properties.getUnprotectedProperties()
+        String retrievedTruststorePassword = unprotectedProperties.getProperty(TRUSTSTORE_PASSWORD_KEY)
+        logger.info("${TRUSTSTORE_PASSWORD_KEY}: ${retrievedTruststorePassword}")
+
+        // Assert
+        assert retrievedTruststorePassword == expectedTruststorePassword
+        assert isSensitive
+        assert !isProtected
+    }
+
+    /**
+     * The new model no longer needs to maintain the protected state -- it is used as a wrapper/decorator during load to unprotect the sensitive properties and then return an instance of raw properties.
+     *
+     * @throws Exception
+     */
+    @Test
+    void testShouldGetUnprotectedValueOfSensitivePropertyWhenProtected() throws Exception {
+        // Arrange
+        final String expectedKeystorePassword = "thisIsABadPassword"
+
+        ProtectedNiFiRegistryProperties properties = loadFromResourceFile("/conf/nifi-registry.with_sensitive_props_protected_aes_128.properties", KEY_HEX_128)
+
+        boolean isSensitive = properties.isPropertySensitive(KEYSTORE_PASSWORD_KEY)
+        boolean isProtected = properties.isPropertyProtected(KEYSTORE_PASSWORD_KEY)
+        logger.info("The property is ${isSensitive ? "sensitive" : "not sensitive"} and ${isProtected ? "protected" : "not protected"}")
+
+        // Act
+        NiFiRegistryProperties unprotectedProperties = properties.getUnprotectedProperties()
+        String retrievedKeystorePassword = unprotectedProperties.getProperty(KEYSTORE_PASSWORD_KEY)
+        logger.info("${KEYSTORE_PASSWORD_KEY}: ${retrievedKeystorePassword}")
+
+        // Assert
+        assert retrievedKeystorePassword == expectedKeystorePassword
+        assert isSensitive
+        assert isProtected
+    }
+
+    /**
+     * In the protection enabled scenario, a call to retrieve a sensitive property should handle if the property is protected with an unknown protection scheme.
+     * @throws Exception
+     */
+    @Test
+    void testGetValueOfSensitivePropertyShouldHandleUnknownProtectionScheme() throws Exception {
+        // Arrange
+
+        // Raw properties
+        Properties rawProperties = new Properties()
+        rawProperties.load(new FileReader(fileForResource("/conf/nifi-registry.with_sensitive_props_protected_unknown.properties")))
+        final String expectedKeystorePassword = rawProperties.getProperty(KEYSTORE_PASSWORD_KEY)
+        logger.info("Raw value for ${KEYSTORE_PASSWORD_KEY}: ${expectedKeystorePassword}")
+
+        ProtectedNiFiRegistryProperties properties = loadFromResourceFile("/conf/nifi-registry.with_sensitive_props_protected_unknown.properties")
+
+        boolean isSensitive = properties.isPropertySensitive(KEYSTORE_PASSWORD_KEY)
+        boolean isProtected = properties.isPropertyProtected(KEYSTORE_PASSWORD_KEY)
+
+        // While the value is "protected", the scheme is not registered, so treat it as raw
+        logger.info("The property is ${isSensitive ? "sensitive" : "not sensitive"} and ${isProtected ? "protected" : "not protected"}")
+
+        // Act
+        NiFiRegistryProperties unprotectedProperties = properties.getUnprotectedProperties()
+        String retrievedKeystorePassword = unprotectedProperties.getProperty(KEYSTORE_PASSWORD_KEY)
+        logger.info("${KEYSTORE_PASSWORD_KEY}: ${retrievedKeystorePassword}")
+
+        // Assert
+        assert retrievedKeystorePassword == expectedKeystorePassword
+        assert isSensitive
+        assert isProtected
+    }
+
+    /**
+     * In the protection enabled scenario, a call to retrieve a sensitive property should handle if the property is unable to be unprotected due to a malformed value.
+     * @throws Exception
+     */
+    @Test
+    void testGetValueOfSensitivePropertyShouldHandleSingleMalformedValue() throws Exception {
+        // Arrange
+        ProtectedNiFiRegistryProperties properties = loadFromResourceFile("/conf/nifi-registry.with_sensitive_props_protected_aes_single_malformed.properties", KEY_HEX_128)
+        boolean isSensitive = properties.isPropertySensitive(KEYSTORE_PASSWORD_KEY)
+        boolean isProtected = properties.isPropertyProtected(KEYSTORE_PASSWORD_KEY)
+        logger.info("The property is ${isSensitive ? "sensitive" : "not sensitive"} and ${isProtected ? "protected" : "not protected"}")
+
+        // Act
+        def msg = shouldFail(SensitivePropertyProtectionException) {
+            NiFiRegistryProperties unprotectedProperties = properties.getUnprotectedProperties()
+            String retrievedKeystorePassword = unprotectedProperties.getProperty(KEYSTORE_PASSWORD_KEY)
+            logger.info("${KEYSTORE_PASSWORD_KEY}: ${retrievedKeystorePassword}")
+        }
+        logger.info(msg)
+
+        // Assert
+        assert msg =~ "Failed to unprotect key ${KEYSTORE_PASSWORD_KEY}"
+        assert isSensitive
+        assert isProtected
+    }
+
+    /**
+     * In the protection enabled scenario, a call to retrieve a sensitive property should handle if the property is unable to be unprotected due to a malformed value.
+     * @throws Exception
+     */
+    @Test
+    void testGetValueOfSensitivePropertyShouldHandleMultipleMalformedValues() throws Exception {
+        // Arrange
+
+        // Raw properties
+        ProtectedNiFiRegistryProperties properties =
+                loadFromResourceFile("/conf/nifi-registry.with_sensitive_props_protected_aes_multiple_malformed.properties", KEY_HEX_128)
+
+        // Iterate over the protected keys and track the ones that fail to decrypt
+        SensitivePropertyProvider spp = new AESSensitivePropertyProvider(KEY_HEX_128)
+        Set<String> malformedKeys = properties.getProtectedPropertyKeys()
+                .findAll { String key, String scheme -> scheme == spp.identifierKey }
+                .keySet()
+                .findAll { String key ->
+            try {
+                spp.unprotect(properties.getProperty(key))
+                return false
+            } catch (SensitivePropertyProtectionException e) {
+                logger.expected("Caught a malformed value for ${key}")
+                return true
+            }
+        }
+
+        logger.expected("Malformed keys: ${malformedKeys.join(", ")}")
+
+        // Act
+        def e = groovy.test.GroovyAssert.shouldFail(SensitivePropertyProtectionException) {
+            NiFiRegistryProperties unprotectedProperties = properties.getUnprotectedProperties()
+            String retrievedKeystorePassword = unprotectedProperties.getProperty(KEYSTORE_PASSWORD_KEY)
+            logger.info("${KEYSTORE_PASSWORD_KEY}: ${retrievedKeystorePassword}")
+        }
+        logger.expected(e.getMessage())
+
+        // Assert
+        assert e instanceof MultipleSensitivePropertyProtectionException
+        assert e.getMessage() =~ "Failed to unprotect keys"
+        assert e.getFailedKeys() == malformedKeys
+
+    }
+
+    /**
+     * In the protection enabled scenario, a call to retrieve a sensitive property should handle if the internal cache of providers is empty.
+     * @throws Exception
+     */
+    @Test
+    void testGetValueOfSensitivePropertyShouldHandleInvalidatedInternalCache() throws Exception {
+        // Arrange
+        ProtectedNiFiRegistryProperties properties =
+                loadFromResourceFile("/conf/nifi-registry.with_sensitive_props_protected_aes_128.properties", KEY_HEX_128)
+        final String expectedKeystorePassword = properties.getProperty(KEYSTORE_PASSWORD_KEY)
+        logger.info("Read raw value from properties: ${expectedKeystorePassword}")
+
+        // Overwrite the internal cache
+        properties.localProviderCache = [:]
+
+        boolean isSensitive = properties.isPropertySensitive(KEYSTORE_PASSWORD_KEY)
+        boolean isProtected = properties.isPropertyProtected(KEYSTORE_PASSWORD_KEY)
+        logger.info("The property is ${isSensitive ? "sensitive" : "not sensitive"} and ${isProtected ? "protected" : "not protected"}")
+
+        // Act
+        NiFiRegistryProperties unprotectedProperties = properties.getUnprotectedProperties()
+        String retrievedKeystorePassword = unprotectedProperties.getProperty(KEYSTORE_PASSWORD_KEY)
+        logger.info("${KEYSTORE_PASSWORD_KEY}: ${retrievedKeystorePassword}")
+
+        // Assert
+        assert retrievedKeystorePassword == expectedKeystorePassword
+        assert isSensitive
+        assert isProtected
+    }
+
+    @Test
+    void testShouldDetectIfPropertyIsProtected() throws Exception {
+        // Arrange
+        final String unprotectedPropertyKey = TRUSTSTORE_PASSWORD_KEY
+        final String protectedPropertyKey = KEYSTORE_PASSWORD_KEY
+
+        ProtectedNiFiRegistryProperties properties =
+                loadFromResourceFile("/conf/nifi-registry.with_sensitive_props_protected_aes_128.properties", KEY_HEX_128)
+
+        // Act
+        boolean unprotectedPasswordIsSensitive = properties.isPropertySensitive(unprotectedPropertyKey)
+        boolean unprotectedPasswordIsProtected = properties.isPropertyProtected(unprotectedPropertyKey)
+        logger.info("${unprotectedPropertyKey} is ${unprotectedPasswordIsSensitive ? "SENSITIVE" : "NOT SENSITIVE"}")
+        logger.info("${unprotectedPropertyKey} is ${unprotectedPasswordIsProtected ? "PROTECTED" : "NOT PROTECTED"}")
+        boolean protectedPasswordIsSensitive = properties.isPropertySensitive(protectedPropertyKey)
+        boolean protectedPasswordIsProtected = properties.isPropertyProtected(protectedPropertyKey)
+        logger.info("${protectedPropertyKey} is ${protectedPasswordIsSensitive ? "SENSITIVE" : "NOT SENSITIVE"}")
+        logger.info("${protectedPropertyKey} is ${protectedPasswordIsProtected ? "PROTECTED" : "NOT PROTECTED"}")
+
+        // Assert
+        assert unprotectedPasswordIsSensitive
+        assert !unprotectedPasswordIsProtected
+
+        assert protectedPasswordIsSensitive
+        assert protectedPasswordIsProtected
+    }
+
+    @Test
+    void testShouldDetectIfPropertyWithEmptyProtectionSchemeIsProtected() throws Exception {
+        // Arrange
+        final String unprotectedPropertyKey = KEY_PASSWORD_KEY
+        ProtectedNiFiRegistryProperties properties =
+                loadFromResourceFile("/conf/nifi-registry.with_sensitive_props_unprotected_extra_line.properties")
+
+        // Act
+        boolean unprotectedPasswordIsSensitive = properties.isPropertySensitive(unprotectedPropertyKey)
+        boolean unprotectedPasswordIsProtected = properties.isPropertyProtected(unprotectedPropertyKey)
+        logger.info("${unprotectedPropertyKey} is ${unprotectedPasswordIsSensitive ? "SENSITIVE" : "NOT SENSITIVE"}")
+        logger.info("${unprotectedPropertyKey} is ${unprotectedPasswordIsProtected ? "PROTECTED" : "NOT PROTECTED"}")
+
+        // Assert
+        assert unprotectedPasswordIsSensitive
+        assert !unprotectedPasswordIsProtected
+    }
+
+    @Test
+    void testShouldGetPercentageOfSensitivePropertiesProtected_0() throws Exception {
+        // Arrange
+        ProtectedNiFiRegistryProperties properties = loadFromResourceFile("/conf/nifi-registry.properties")
+
+        logger.info("Sensitive property keys: ${properties.getSensitivePropertyKeys()}")
+        logger.info("Protected property keys: ${properties.getProtectedPropertyKeys().keySet()}")
+
+        // Act
+        double percentProtected = properties.getPercentOfSensitivePropertiesProtected()
+        logger.info("${percentProtected}% (${properties.getProtectedPropertyKeys().size()} of ${properties.getPopulatedSensitivePropertyKeys().size()}) protected")
+
+        // Assert
+        assert percentProtected == 0.0
+    }
+
+    @Test
+    void testShouldGetPercentageOfSensitivePropertiesProtected_75() throws Exception {
+        // Arrange
+        ProtectedNiFiRegistryProperties properties =
+                loadFromResourceFile("/conf/nifi-registry.with_sensitive_props_protected_aes_128.properties", KEY_HEX_128)
+
+        logger.info("Sensitive property keys: ${properties.getSensitivePropertyKeys()}")
+        logger.info("Protected property keys: ${properties.getProtectedPropertyKeys().keySet()}")
+
+        // Act
+        double percentProtected = properties.getPercentOfSensitivePropertiesProtected()
+        logger.info("${percentProtected}% (${properties.getProtectedPropertyKeys().size()} of ${properties.getPopulatedSensitivePropertyKeys().size()}) protected")
+
+        // Assert
+        assert percentProtected == 67.0
+    }
+
+    @Test
+    void testShouldGetPercentageOfSensitivePropertiesProtected_100() throws Exception {
+        // Arrange
+        ProtectedNiFiRegistryProperties properties =
+                loadFromResourceFile("/conf/nifi-registry.with_sensitive_props_fully_protected_aes_128.properties", KEY_HEX_128)
+
+        logger.info("Sensitive property keys: ${properties.getSensitivePropertyKeys()}")
+        logger.info("Protected property keys: ${properties.getProtectedPropertyKeys().keySet()}")
+
+        // Act
+        double percentProtected = properties.getPercentOfSensitivePropertiesProtected()
+        logger.info("${percentProtected}% (${properties.getProtectedPropertyKeys().size()} of ${properties.getPopulatedSensitivePropertyKeys().size()}) protected")
+
+        // Assert
+        assert percentProtected == 100.0
+    }
+
+    @Test
+    void testInstanceWithNoProtectedPropertiesShouldNotLoadSPP() throws Exception {
+        // Arrange
+        ProtectedNiFiRegistryProperties properties = loadFromResourceFile("/conf/nifi-registry.properties")
+        assert properties.@localProviderCache?.isEmpty()
+
+        logger.info("Has protected properties: ${properties.hasProtectedKeys()}")
+        assert !properties.hasProtectedKeys()
+
+        // Act
+        Map localCache = properties.@localProviderCache
+        logger.info("Internal cache ${localCache} has ${localCache.size()} providers loaded")
+
+        // Assert
+        assert localCache.isEmpty()
+    }
+
+    @Test
+    void testShouldAddSensitivePropertyProvider() throws Exception {
+        // Arrange
+        ProtectedNiFiRegistryProperties properties = new ProtectedNiFiRegistryProperties()
+        assert properties.getSensitivePropertyProviders().isEmpty()
+
+        SensitivePropertyProvider mockProvider =
+                [unprotect       : { String input ->
+                    logger.mock("Mock call to #unprotect(${input})")
+                    input.reverse()
+                },
+                 getIdentifierKey: { -> "mockProvider" }] as SensitivePropertyProvider
+
+        // Act
+        properties.addSensitivePropertyProvider(mockProvider)
+
+        // Assert
+        assert properties.getSensitivePropertyProviders().size() == 1
+    }
+
+    @Test
+    void testShouldNotAddNullSensitivePropertyProvider() throws Exception {
+        // Arrange
+        ProtectedNiFiRegistryProperties properties = new ProtectedNiFiRegistryProperties()
+        assert properties.@localProviderCache?.isEmpty()
+
+        // Act
+        def msg = shouldFail(IllegalArgumentException) {
+            properties.addSensitivePropertyProvider(null)
+        }
+        logger.info(msg)
+
+        // Assert
+        assert properties.getSensitivePropertyProviders().size() == 0
+        assert msg == "Cannot add null SensitivePropertyProvider"
+    }
+
+    @Test
+    void testShouldNotAllowOverwriteOfProvider() throws Exception {
+        // Arrange
+        ProtectedNiFiRegistryProperties properties = new ProtectedNiFiRegistryProperties()
+        assert properties.getSensitivePropertyProviders().isEmpty()
+
+        SensitivePropertyProvider mockProvider =
+                [unprotect       : { String input ->
+                    logger.mock("Mock call to 1#unprotect(${input})")
+                    input.reverse()
+                },
+                 getIdentifierKey: { -> "mockProvider" }] as SensitivePropertyProvider
+        properties.addSensitivePropertyProvider(mockProvider)
+        assert properties.getSensitivePropertyProviders().size() == 1
+
+        SensitivePropertyProvider mockProvider2 =
+                [unprotect       : { String input ->
+                    logger.mock("Mock call to 2#unprotect(${input})")
+                    input.reverse()
+                },
+                 getIdentifierKey: { -> "mockProvider" }] as SensitivePropertyProvider
+
+        // Act
+        def msg = shouldFail(UnsupportedOperationException) {
+            properties.addSensitivePropertyProvider(mockProvider2)
+        }
+        logger.info(msg)
+
+        // Assert
+        assert msg == "Cannot overwrite existing sensitive property provider registered for mockProvider"
+        assert properties.getSensitivePropertyProviders().size() == 1
+    }
+
+    @Test
+    void testGetUnprotectedPropertiesShouldReturnInternalInstanceWhenNoneProtected() {
+        // Arrange
+        ProtectedNiFiRegistryProperties protectedNiFiProperties = loadFromResourceFile("/conf/nifi-registry.properties")
+        logger.info("Loaded ${protectedNiFiProperties.size()} properties from conf/nifi.properties")
+
+        int hashCode = protectedNiFiProperties.internalNiFiProperties.hashCode()
+        logger.info("Hash code of internal instance: ${hashCode}")
+
+        // Act
+        NiFiRegistryProperties unprotectedNiFiProperties = protectedNiFiProperties.getUnprotectedProperties()
+        logger.info("Unprotected ${unprotectedNiFiProperties.size()} properties")
+
+        // Assert
+        assert unprotectedNiFiProperties.size() == protectedNiFiProperties.size()
+        assert unprotectedNiFiProperties.getPropertyKeys().every {
+            !unprotectedNiFiProperties.getProperty(it).endsWith(".protected")
+        }
+        logger.info("Hash code from returned unprotected instance: ${unprotectedNiFiProperties.hashCode()}")
+        assert unprotectedNiFiProperties.hashCode() == hashCode
+    }
+
+    @Test
+    void testGetUnprotectedPropertiesShouldDecryptProtectedProperties() {
+        // Arrange
+        ProtectedNiFiRegistryProperties protectedNiFiProperties =
+                loadFromResourceFile("/conf/nifi-registry.with_sensitive_props_protected_aes_128.properties", KEY_HEX_128)
+
+        int protectedPropertyCount = protectedNiFiProperties.getProtectedPropertyKeys().size()
+        int protectionSchemeCount = protectedNiFiProperties
+                .getPropertyKeysIncludingProtectionSchemes()
+                .findAll { it.endsWith(".protected") }
+                .size()
+        int expectedUnprotectedPropertyCount = protectedNiFiProperties.size()
+
+        String protectedProps = protectedNiFiProperties
+                .getProtectedPropertyKeys()
+                .collectEntries {
+            [(it.key): protectedNiFiProperties.getProperty(it.key)]
+        }.entrySet()
+                .join("\n")
+
+        logger.info("Detected ${protectedPropertyCount} protected properties and ${protectionSchemeCount} protection scheme properties")
+        logger.info("Protected properties: \n${protectedProps}")
+
+        logger.info("Expected unprotected property count: ${expectedUnprotectedPropertyCount}")
+
+        int hashCode = protectedNiFiProperties.internalNiFiProperties.hashCode()
+        logger.info("Hash code of internal instance: ${hashCode}")
+
+        // Act
+        NiFiRegistryProperties unprotectedNiFiProperties = protectedNiFiProperties.getUnprotectedProperties()
+        logger.info("Unprotected ${unprotectedNiFiProperties.size()} properties")
+
+        // Assert
+        assert unprotectedNiFiProperties.size() == expectedUnprotectedPropertyCount
+        assert unprotectedNiFiProperties.getPropertyKeys().every {
+            !unprotectedNiFiProperties.getProperty(it).endsWith(".protected")
+        }
+        logger.info("Hash code from returned unprotected instance: ${unprotectedNiFiProperties.hashCode()}")
+        assert unprotectedNiFiProperties.hashCode() != hashCode
+    }
+
+    @Test
+    void testGetUnprotectedPropertiesShouldDecryptProtectedPropertiesWith256Bit() {
+        // Arrange
+        Assume.assumeTrue("JCE unlimited strength crypto policy must be installed for this test", Cipher.getMaxAllowedKeyLength("AES") > 128)
+        ProtectedNiFiRegistryProperties protectedNiFiProperties =
+                loadFromResourceFile("/conf/nifi-registry.with_sensitive_props_protected_aes_256.properties", KEY_HEX_256)
+
+        int protectedPropertyCount = protectedNiFiProperties.getProtectedPropertyKeys().size()
+        int protectionSchemeCount = protectedNiFiProperties
+                .getPropertyKeysIncludingProtectionSchemes()
+                .findAll { it.endsWith(".protected") }
+                .size()
+        int expectedUnprotectedPropertyCount = protectedNiFiProperties.size()
+
+        String protectedProps = protectedNiFiProperties
+                .getProtectedPropertyKeys()
+                .collectEntries {
+            [(it.key): protectedNiFiProperties.getProperty(it.key)]
+        }.entrySet()
+                .join("\n")
+
+        logger.info("Detected ${protectedPropertyCount} protected properties and ${protectionSchemeCount} protection scheme properties")
+        logger.info("Protected properties: \n${protectedProps}")
+
+        logger.info("Expected unprotected property count: ${expectedUnprotectedPropertyCount}")
+
+        int hashCode = protectedNiFiProperties.internalNiFiProperties.hashCode()
+        logger.info("Hash code of internal instance: ${hashCode}")
+
+        // Act
+        NiFiRegistryProperties unprotectedNiFiProperties = protectedNiFiProperties.getUnprotectedProperties()
+        logger.info("Unprotected ${unprotectedNiFiProperties.size()} properties")
+
+        // Assert
+        assert unprotectedNiFiProperties.size() == expectedUnprotectedPropertyCount
+        assert unprotectedNiFiProperties.getPropertyKeys().every {
+            !unprotectedNiFiProperties.getProperty(it).endsWith(".protected")
+        }
+        logger.info("Hash code from returned unprotected instance: ${unprotectedNiFiProperties.hashCode()}")
+        assert unprotectedNiFiProperties.hashCode() != hashCode
+    }
+
+    @Test
+    void testShouldCalculateSize() {
+        // Arrange
+        NiFiRegistryProperties rawProperties = [key: "protectedValue", "key.protected": "scheme", "key2": "value2"] as NiFiRegistryProperties
+        ProtectedNiFiRegistryProperties protectedNiFiProperties = new ProtectedNiFiRegistryProperties(rawProperties)
+        logger.info("Raw properties (${rawProperties.size()}): ${rawProperties.keySet().join(", ")}")
+
+        // Act
+        int protectedSize = protectedNiFiProperties.size()
+        logger.info("Protected properties (${protectedNiFiProperties.size()}): " +
+                "${protectedNiFiProperties.getPropertyKeysExcludingProtectionSchemes().join(", ")}")
+
+        // Assert
+        assert protectedSize == rawProperties.size() - 1
+    }
+
+    @Test
+    void testGetPropertyKeysShouldMatchSize() {
+        // Arrange
+        NiFiRegistryProperties rawProperties = [key: "protectedValue", "key.protected": "scheme", "key2": "value2"] as NiFiRegistryProperties
+        ProtectedNiFiRegistryProperties protectedNiFiProperties = new ProtectedNiFiRegistryProperties(rawProperties)
+        logger.info("Raw properties (${rawProperties.size()}): ${rawProperties.keySet().join(", ")}")
+
+        // Act
+        def filteredKeys = protectedNiFiProperties.getPropertyKeysExcludingProtectionSchemes()
+        logger.info("Protected properties (${protectedNiFiProperties.size()}): ${filteredKeys.join(", ")}")
+
+        // Assert
+        assert protectedNiFiProperties.size() == rawProperties.size() - 1
+        assert filteredKeys == rawProperties.keySet() - "key.protected"
+    }
+
+    @Test
+    void testShouldGetPropertyKeysIncludingProtectionSchemes() {
+        // Arrange
+        NiFiRegistryProperties rawProperties = [key: "protectedValue", "key.protected": "scheme", "key2": "value2"] as NiFiRegistryProperties
+        ProtectedNiFiRegistryProperties protectedNiFiProperties = new ProtectedNiFiRegistryProperties(rawProperties)
+        logger.info("Raw properties (${rawProperties.size()}): ${rawProperties.keySet().join(", ")}")
+
+        // Act
+        def allKeys = protectedNiFiProperties.getPropertyKeysIncludingProtectionSchemes()
+        logger.info("Protected properties with schemes (${allKeys.size()}): ${allKeys.join(", ")}")
+
+        // Assert
+        assert allKeys.size() == rawProperties.size()
+        assert allKeys == rawProperties.keySet()
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/6f26290d/nifi-registry-core/nifi-registry-properties/src/test/groovy/org/apache/nifi/security/crypto/CryptoKeyLoaderGroovyTest.groovy
----------------------------------------------------------------------
diff --git a/nifi-registry-core/nifi-registry-properties/src/test/groovy/org/apache/nifi/security/crypto/CryptoKeyLoaderGroovyTest.groovy b/nifi-registry-core/nifi-registry-properties/src/test/groovy/org/apache/nifi/security/crypto/CryptoKeyLoaderGroovyTest.groovy
new file mode 100644
index 0000000..4f69682
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-properties/src/test/groovy/org/apache/nifi/security/crypto/CryptoKeyLoaderGroovyTest.groovy
@@ -0,0 +1,121 @@
+/*
+ * 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.security.crypto
+
+import org.apache.nifi.registry.security.crypto.CryptoKeyLoader
+import org.apache.nifi.registry.security.crypto.CryptoKeyProvider
+import org.bouncycastle.jce.provider.BouncyCastleProvider
+import org.junit.BeforeClass
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.JUnit4
+import org.slf4j.Logger
+import org.slf4j.LoggerFactory
+
+import java.nio.file.Files
+import java.nio.file.attribute.PosixFilePermission
+import java.security.Security
+
+@RunWith(JUnit4.class)
+class CryptoKeyLoaderGroovyTest extends GroovyTestCase {
+
+    private static final Logger logger = LoggerFactory.getLogger(CryptoKeyLoaderGroovyTest.class)
+
+    private static final String KEY_HEX_128 = "0123456789ABCDEFFEDCBA9876543210"
+    private static final String KEY_HEX_256 = KEY_HEX_128 * 2
+
+    @BeforeClass
+    public static void setUpOnce() throws Exception {
+        Security.addProvider(new BouncyCastleProvider())
+
+        logger.metaClass.methodMissing = { String name, args ->
+            logger.info("[${name?.toUpperCase()}] ${(args as List).join(" ")}")
+        }
+    }
+
+    @Test
+    public void testShouldExtractKeyFromBootstrapFile() throws Exception {
+        // Arrange
+        final String expectedKey = KEY_HEX_256
+
+        // Act
+        String key = CryptoKeyLoader.extractKeyFromBootstrapFile("src/test/resources/conf/bootstrap.conf")
+
+        // Assert
+        assert key == expectedKey
+    }
+
+    @Test
+    public void testShouldNotExtractKeyFromBootstrapFileWithoutKeyLine() throws Exception {
+        // Arrange
+
+        // Act
+        String key = CryptoKeyLoader.extractKeyFromBootstrapFile("src/test/resources/conf/bootstrap.with_missing_key_line.conf")
+
+        // Assert
+        assert key == CryptoKeyProvider.EMPTY_KEY
+    }
+
+    @Test
+    public void testShouldNotExtractKeyFromBootstrapFileWithoutKey() throws Exception {
+        // Arrange
+
+        // Act
+        String key = CryptoKeyLoader.extractKeyFromBootstrapFile("src/test/resources/conf/bootstrap.with_missing_key.conf")
+
+        // Assert
+        assert key == CryptoKeyProvider.EMPTY_KEY
+    }
+
+    @Test
+    public void testShouldNotExtractKeyFromMissingBootstrapFile() throws Exception {
+        // Arrange
+
+        // Act
+        def msg = shouldFail(IOException) {
+            CryptoKeyLoader.extractKeyFromBootstrapFile("src/test/resources/conf/bootstrap.missing.conf")
+        }
+        logger.info(msg)
+
+        // Assert
+        assert msg == "Cannot read from bootstrap.conf"
+    }
+
+    @Test
+    public void testShouldNotExtractKeyFromUnreadableBootstrapFile() throws Exception {
+        // Arrange
+        File unreadableFile = new File("src/test/resources/conf/bootstrap.unreadable_file_permissions.conf")
+        Set<PosixFilePermission> originalPermissions = Files.getPosixFilePermissions(unreadableFile.toPath())
+        Files.setPosixFilePermissions(unreadableFile.toPath(), [] as Set)
+        try {
+            assert !unreadableFile.canRead()
+
+            // Act
+            def msg = shouldFail(IOException) {
+                CryptoKeyLoader.extractKeyFromBootstrapFile("src/test/resources/conf/bootstrap.unreadable_file_permissions.conf")
+            }
+            logger.info(msg)
+
+            // Assert
+            assert msg == "Cannot read from bootstrap.conf"
+        } finally {
+            // Clean up to allow for indexing, etc.
+            Files.setPosixFilePermissions(unreadableFile.toPath(), originalPermissions)
+        }
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/6f26290d/nifi-registry-core/nifi-registry-properties/src/test/resources/conf/bootstrap.conf
----------------------------------------------------------------------
diff --git a/nifi-registry-core/nifi-registry-properties/src/test/resources/conf/bootstrap.conf b/nifi-registry-core/nifi-registry-properties/src/test/resources/conf/bootstrap.conf
new file mode 100644
index 0000000..4321bca
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-properties/src/test/resources/conf/bootstrap.conf
@@ -0,0 +1,60 @@
+#
+# 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.
+#
+
+# Java command to use when running nifi-registry
+java=java
+
+# Username to use when running nifi-registry. This value will be ignored on Windows.
+run.as=
+
+# Configure where nifi-registry's lib and conf directories live
+lib.dir=./lib
+conf.dir=./conf
+
+# How long to wait after telling nifi-registry to shutdown before explicitly killing the Process
+graceful.shutdown.seconds=20
+
+# Disable JSR 199 so that we can use JSP's without running a JDK
+java.arg.1=-Dorg.apache.jasper.compiler.disablejsr199=true
+
+# JVM memory settings
+java.arg.2=-Xms512m
+java.arg.3=-Xmx512m
+
+# Enable Remote Debugging
+#java.arg.debug=-agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=8000
+
+java.arg.4=-Djava.net.preferIPv4Stack=true
+
+# allowRestrictedHeaders is required for Cluster/Node communications to work properly
+java.arg.5=-Dsun.net.http.allowRestrictedHeaders=true
+java.arg.6=-Djava.protocol.handler.pkgs=sun.net.www.protocol
+
+# Java 7 and below have issues with Code Cache. The following lines allow us to run well even with
+# many classes loaded in the JVM.
+#java.arg.7=-XX:ReservedCodeCacheSize=256m
+#java.arg.8=-XX:CodeCacheFlushingMinimumFreeSpace=10m
+#java.arg.9=-XX:+UseCodeCacheFlushing
+#java.arg.11=-XX:PermSize=128M
+#java.arg.12=-XX:MaxPermSize=128M
+
+# The G1GC is still considered experimental but has proven to be very advantageous in providing great
+# performance without significant "stop-the-world" delays.
+#java.arg.10=-XX:+UseG1GC
+
+# Master key in hexadecimal format for encrypted sensitive configuration values
+nifi.registry.bootstrap.sensitive.key=0123456789ABCDEFFEDCBA98765432100123456789ABCDEFFEDCBA9876543210
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/6f26290d/nifi-registry-core/nifi-registry-properties/src/test/resources/conf/bootstrap.unreadable_file_permissions.conf
----------------------------------------------------------------------
diff --git a/nifi-registry-core/nifi-registry-properties/src/test/resources/conf/bootstrap.unreadable_file_permissions.conf b/nifi-registry-core/nifi-registry-properties/src/test/resources/conf/bootstrap.unreadable_file_permissions.conf
new file mode 100644
index 0000000..3043635
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-properties/src/test/resources/conf/bootstrap.unreadable_file_permissions.conf
@@ -0,0 +1,22 @@
+#
+# 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.
+#
+
+# The POSIX file permissions for this file are emptied (i.e., chmod 000) during test and then reverted
+# See org.apache.nifi.registry.properties.NiFiRegistryPropertiesLoaderGroovyTest#testShouldNotExtractKeyFromUnreadableBootstrapFile
+
+# Master key in hexadecimal format for encrypted sensitive configuration values
+nifi.registry.bootstrap.sensitive.key=0123456789ABCDEFFEDCBA98765432100123456789ABCDEFFEDCBA9876543210
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/6f26290d/nifi-registry-core/nifi-registry-properties/src/test/resources/conf/bootstrap.with_missing_key.conf
----------------------------------------------------------------------
diff --git a/nifi-registry-core/nifi-registry-properties/src/test/resources/conf/bootstrap.with_missing_key.conf b/nifi-registry-core/nifi-registry-properties/src/test/resources/conf/bootstrap.with_missing_key.conf
new file mode 100644
index 0000000..7317ab0
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-properties/src/test/resources/conf/bootstrap.with_missing_key.conf
@@ -0,0 +1,60 @@
+#
+# 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.
+#
+
+# Java command to use when running nifi-registry
+java=java
+
+# Username to use when running nifi-registry. This value will be ignored on Windows.
+run.as=
+
+# Configure where nifi-registry's lib and conf directories live
+lib.dir=./lib
+conf.dir=./conf
+
+# How long to wait after telling nifi-registry to shutdown before explicitly killing the Process
+graceful.shutdown.seconds=20
+
+# Disable JSR 199 so that we can use JSP's without running a JDK
+java.arg.1=-Dorg.apache.jasper.compiler.disablejsr199=true
+
+# JVM memory settings
+java.arg.2=-Xms512m
+java.arg.3=-Xmx512m
+
+# Enable Remote Debugging
+#java.arg.debug=-agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=8000
+
+java.arg.4=-Djava.net.preferIPv4Stack=true
+
+# allowRestrictedHeaders is required for Cluster/Node communications to work properly
+java.arg.5=-Dsun.net.http.allowRestrictedHeaders=true
+java.arg.6=-Djava.protocol.handler.pkgs=sun.net.www.protocol
+
+# Java 7 and below have issues with Code Cache. The following lines allow us to run well even with
+# many classes loaded in the JVM.
+#java.arg.7=-XX:ReservedCodeCacheSize=256m
+#java.arg.8=-XX:CodeCacheFlushingMinimumFreeSpace=10m
+#java.arg.9=-XX:+UseCodeCacheFlushing
+#java.arg.11=-XX:PermSize=128M
+#java.arg.12=-XX:MaxPermSize=128M
+
+# The G1GC is still considered experimental but has proven to be very advantageous in providing great
+# performance without significant "stop-the-world" delays.
+#java.arg.10=-XX:+UseG1GC
+
+# Master key in hexadecimal format for encrypted sensitive configuration values
+nifi.registry.bootstrap.sensitive.key=
\ No newline at end of file


[03/51] [partial] nifi-registry git commit: NIFIREG-201 Refactoring project structure to better isolate extensions

Posted by kd...@apache.org.
http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/6f26290d/nifi-registry-core/nifi-registry-web-ui/src/main/webapp/components/administration/users/nf-registry-users-administration.html
----------------------------------------------------------------------
diff --git a/nifi-registry-core/nifi-registry-web-ui/src/main/webapp/components/administration/users/nf-registry-users-administration.html b/nifi-registry-core/nifi-registry-web-ui/src/main/webapp/components/administration/users/nf-registry-users-administration.html
new file mode 100644
index 0000000..7062f6b
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-web-ui/src/main/webapp/components/administration/users/nf-registry-users-administration.html
@@ -0,0 +1,178 @@
+<!--
+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.
+-->
+
+<div id="nifi-registry-users-administration-perspective" class="mat-elevation-z5">
+    <div fxFlex class="pad-top-md pad-bottom-sm pad-left-md pad-right-md">
+        <span class="md-card-title">Authorized Users ({{nfRegistryService.users.length + nfRegistryService.groups.length}})</span>
+        <div flex class="push-right-sm" fxLayout="row" fxLayoutAlign="end center">
+            <td-chips class="push-right-sm"
+                      [items]="nfRegistryService.autoCompleteUsersAndGroups"
+                      (add)="nfRegistryService.usersSearchAdd($event)"
+                      (remove)="nfRegistryService.usersSearchRemove($event)"></td-chips>
+            <div matTooltip="{{(!nfRegistryService.currentUser.resourcePermissions.tenants.canWrite) ? 'You do not have permission. Please contact your system administrator.': ''}}">
+                <button id="add-user-button" class="push-top-sm push-right-sm" color="fds-secondary" mat-raised-button (click)="addUser()" [disabled]="!canEditUsers()">
+                    Add User
+                </button>
+            </div>
+            <button class="push-top-sm" color="fds-primary"
+                    mat-raised-button [matMenuTriggerFor]="userActionMenu">
+                Actions<i class="fa fa-caret-down" aria-hidden="true"></i>
+            </button>
+        </div>
+        <mat-menu class="fds-primary-dropdown-button-menu" #userActionMenu="matMenu" [overlapTrigger]="false">
+            <div matTooltip="{{nfRegistryService.disableMultiDeleteAction ? 'Please deselect any non-configurable users or groups to enable multi-delete.': ''}}">
+                <button mat-menu-item
+                    [disabled]="((nfRegistryService.getSelectedGroups().length === 0) && (nfRegistryService.getSelectedUsers().length === 0)) || nfRegistryService.disableMultiDeleteAction || !canEditUsers()"
+                    (click)="nfRegistryService.deleteSelectedUsersAndGroups()">
+                    <span>Delete</span>
+                </button>
+            </div>
+            <div matTooltip="{{(nfRegistryService.getSelectedGroups().length > 0) ? 'Only users can be added when creating a new group. Please deselect any groups to enable.': (!nfRegistryService.currentUser.resourcePermissions.tenants.canWrite) ? 'You do not have permission. Please contact your system administrator.': ''}}">
+                <button mat-menu-item [disabled]="(nfRegistryService.getSelectedGroups().length > 0) || !canEditUsers()"
+                    (click)="createNewGroup()">
+                    <span>Create new group</span>
+                </button>
+            </div>
+        </mat-menu>
+        <div id="nifi-registry-users-administration-list-container-column-header" fxLayout="row"
+             fxLayoutAlign="space-between center" class="td-data-table">
+            <div class="td-data-table-column" (click)="nfRegistryService.sortUsersAndGroups(column)"
+                 *ngFor="let column of nfRegistryService.userColumns"
+                 fxFlex="{{column.width}}">
+                {{column.label}}
+                <i *ngIf="column.active && column.sortable && column.sortOrder === 'ASC'" class="fa fa-caret-up"
+                   aria-hidden="true"></i>
+                <i *ngIf="column.active && column.sortable && column.sortOrder === 'DESC'" class="fa fa-caret-down"
+                   aria-hidden="true"></i>
+            </div>
+            <div class="td-data-table-column">
+                <div fxLayout="row" fxLayoutAlign="end center">
+                    <mat-checkbox class="pad-left-sm" [(ngModel)]="nfRegistryService.allUsersAndGroupsSelected"
+                                  (checked)="nfRegistryService.allUsersAndGroupsSelected"
+                                  (change)="nfRegistryService.toggleUsersSelectAll()"></mat-checkbox>
+                </div>
+            </div>
+        </div>
+        <div id="nifi-registry-users-administration-list-container">
+            <div
+                 *ngFor="let row of nfRegistryService.filteredUserGroups"
+                 (click)="row.checked = !row.checked;nfRegistryService.determineAllUsersAndGroupsSelectedState(row)">
+                <div [ngClass]="{'nonconfigurable' : row.configurable === false, 'selected-nonconfigurable' : (row.checked === true && row.configurable === false), 'selected' : row.checked === true}" *ngFor="let column of nfRegistryService.userColumns" fxLayout="row" fxLayoutAlign="space-between center" class="td-data-table-row">
+                    <div class="td-data-table-cell" fxFlex="{{column.width}}">
+                        <div class="ellipsis" matTooltip="{{column.format ? column.format(row[column.name]) : row[column.name]}}">
+                            <i class="fa fa-users push-right-sm" aria-hidden="true"></i>{{column.format ? column.format(row[column.name]) : row[column.name]}}
+                        </div>
+                    </div>
+                    <div class="td-data-table-cell">
+                        <div>
+                            <div *ngIf="nfRegistryService.userGroupsActions.length <= 4" fxLayout="row" fxLayoutAlign="end center">
+                                <button (click)="row.checked = !row.checked;nfRegistryService.executeGroupAction(action, row)"
+                                        *ngFor="let action of nfRegistryService.userGroupsActions"
+                                        matTooltip="{{action.tooltip}}" mat-icon-button color="accent"
+                                        [disabled]="action.disabled(row)">
+                                    <i class="{{action.icon}}" aria-hidden="true"></i>
+                                </button>
+                                <mat-checkbox class="pad-left-sm" [(ngModel)]="row.checked" [checked]="row.checked"
+                                              (change)="nfRegistryService.determineAllUsersAndGroupsSelectedState(row)"
+                                              (click)="row.checked = !row.checked;nfRegistryService.determineAllUsersAndGroupsSelectedState(row)"></mat-checkbox>
+                            </div>
+                            <div *ngIf="nfRegistryService.userGroupsActions.length > 4" fxLayout="row" fxLayoutAlign="end center">
+                                <button (click)="row.checked = !row.checked" matTooltip="Actions" mat-icon-button
+                                        [matMenuTriggerFor]="userTableActionMenu">
+                                    <i class="fa fa-ellipsis-h" aria-hidden="true"></i>
+                                </button>
+                                <mat-menu #userTableActionMenu="matMenu" [overlapTrigger]="false">
+                                    <button (click)="nfRegistryService.executeGroupAction(action, row)"
+                                            *ngFor="let action of nfRegistryService.userGroupsActions"
+                                            matTooltip="{{action.tooltip}}" mat-menu-item
+                                            [disabled]="action.disabled(row)">
+                                            (click)="nfRegistryService.sidenav.toggle()">
+                                        <i class="{{action.icon}}" aria-hidden="true"></i>
+                                        <span>{{action.name}}</span>
+                                    </button>
+                                </mat-menu>
+                                <mat-checkbox class="pad-left-sm" [(ngModel)]="row.checked" [checked]="row.checked"
+                                              (change)="nfRegistryService.determineAllUsersAndGroupsSelectedState(row)"
+                                              (click)="row.checked = !row.checked;nfRegistryService.determineAllUsersAndGroupsSelectedState(row)"></mat-checkbox>
+                            </div>
+                        </div>
+                        <div *ngIf="!nfRegistryService.userGroupsActions" fxLayout="row" fxLayoutAlign="end center">
+                            <mat-checkbox class="pad-left-sm" [(ngModel)]="row.checked" [checked]="row.checked"
+                                          (change)="nfRegistryService.determineAllUsersAndGroupsSelectedState(row)"
+                                          (click)="row.checked = !row.checked;nfRegistryService.determineAllUsersAndGroupsSelectedState(row)"></mat-checkbox>
+                        </div>
+                    </div>
+                </div>
+            </div>
+            <div
+                 *ngFor="let row of nfRegistryService.filteredUsers"
+                 (click)="row.checked = !row.checked;nfRegistryService.determineAllUsersAndGroupsSelectedState(row)">
+                <div [ngClass]="{'nonconfigurable' : row.configurable === false, 'selected-nonconfigurable' : (row.checked === true && row.configurable === false), 'selected' : row.checked === true}" *ngFor="let column of nfRegistryService.userColumns" fxLayout="row" fxLayoutAlign="space-between center" class="td-data-table-row">
+                    <div class="td-data-table-cell" fxFlex="{{column.width}}">
+                        <div class="ellipsis" matTooltip="{{column.format ? column.format(row[column.name]) : row[column.name]}}">
+                            {{column.format ? column.format(row[column.name]) : row[column.name]}}
+                        </div>
+                    </div>
+                    <div class="td-data-table-cell">
+                        <div>
+                            <div *ngIf="nfRegistryService.usersActions.length <= 4" fxLayout="row" fxLayoutAlign="end center">
+                                <button (click)="row.checked = !row.checked;nfRegistryService.executeUserAction(action, row)"
+                                        *ngFor="let action of nfRegistryService.usersActions"
+                                        matTooltip="{{action.tooltip}}" mat-icon-button color="accent"
+                                        [disabled]="action.disabled(row)">
+                                    <i class="{{action.icon}}" aria-hidden="true"></i>
+                                </button>
+                                <mat-checkbox class="pad-left-sm" [(ngModel)]="row.checked" [checked]="row.checked"
+                                              (change)="nfRegistryService.determineAllUsersAndGroupsSelectedState(row)"
+                                              (click)="row.checked = !row.checked;nfRegistryService.determineAllUsersAndGroupsSelectedState(row)"></mat-checkbox>
+                            </div>
+                            <div *ngIf="nfRegistryService.usersActions.length > 4" fxLayout="row" fxLayoutAlign="end center">
+                                <button (click)="row.checked = !row.checked" matTooltip="Actions" mat-icon-button
+                                        [matMenuTriggerFor]="userTableActionMenu">
+                                    <i class="fa fa-ellipsis-h" aria-hidden="true"></i>
+                                </button>
+                                <mat-menu #userTableActionMenu="matMenu" [overlapTrigger]="false">
+                                    <button (click)="nfRegistryService.executeUserAction(action, row)"
+                                            *ngFor="let action of nfRegistryService.usersActions"
+                                            matTooltip="{{action.tooltip}}" mat-menu-item
+                                            [disabled]="action.disabled(row)">
+                                            (click)="nfRegistryService.sidenav.toggle()">
+                                        <i class="{{action.icon}}" aria-hidden="true"></i>
+                                        <span>{{action.name}}</span>
+                                    </button>
+                                </mat-menu>
+                                <mat-checkbox class="pad-left-sm" [(ngModel)]="row.checked" [checked]="row.checked"
+                                              (change)="nfRegistryService.determineAllUsersAndGroupsSelectedState(row)"
+                                              (click)="row.checked = !row.checked;nfRegistryService.determineAllUsersAndGroupsSelectedState(row)"></mat-checkbox>
+                            </div>
+                        </div>
+                        <div *ngIf="!nfRegistryService.usersActions" fxLayout="row" fxLayoutAlign="end center">
+                            <mat-checkbox class="pad-left-sm" [(ngModel)]="row.checked" [checked]="row.checked"
+                                          (change)="nfRegistryService.determineAllUsersAndGroupsSelectedState(row)"
+                                          (click)="row.checked = !row.checked;nfRegistryService.determineAllUsersAndGroupsSelectedState(row)"></mat-checkbox>
+                        </div>
+                    </div>
+                </div>
+            </div>
+        </div>
+        <div class="mat-padding" *ngIf="nfRegistryService.filteredUsers.length === 0 && nfRegistryService.filteredUserGroups.length === 0" layout="row"
+             layout-align="center center">
+            <h3>No results to display.</h3>
+        </div>
+    </div>
+</div>
+<router-outlet></router-outlet>

http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/6f26290d/nifi-registry-core/nifi-registry-web-ui/src/main/webapp/components/administration/users/nf-registry-users-administration.js
----------------------------------------------------------------------
diff --git a/nifi-registry-core/nifi-registry-web-ui/src/main/webapp/components/administration/users/nf-registry-users-administration.js b/nifi-registry-core/nifi-registry-web-ui/src/main/webapp/components/administration/users/nf-registry-users-administration.js
new file mode 100644
index 0000000..08def45
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-web-ui/src/main/webapp/components/administration/users/nf-registry-users-administration.js
@@ -0,0 +1,150 @@
+/*
+ * 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.
+ */
+var ngCore = require('@angular/core');
+var rxjs = require('rxjs/Observable');
+var NfRegistryService = require('nifi-registry/services/nf-registry.service.js');
+var NfRegistryApi = require('nifi-registry/services/nf-registry.api.js');
+var NfStorage = require('nifi-registry/services/nf-storage.service.js');
+var ngRouter = require('@angular/router');
+var nfRegistryAnimations = require('nifi-registry/nf-registry.animations.js');
+var ngMaterial = require('@angular/material');
+var fdsDialogsModule = require('@flow-design-system/dialogs');
+var NfRegistryAddUser = require('nifi-registry/components/administration/users/dialogs/add-user/nf-registry-add-user.js');
+var NfRegistryCreateNewGroup = require('nifi-registry/components/administration/users/dialogs/create-new-group/nf-registry-create-new-group.js');
+
+/**
+ * NfRegistryUsersAdministration constructor.
+ *
+ * @param nfRegistryApi         The api service.
+ * @param nfStorage             A wrapper for the browser's local storage.
+ * @param nfRegistryService     The nf-registry.service module.
+ * @param activatedRoute        The angular activated route module.
+ * @param fdsDialogService      The FDS dialog service.
+ * @param matDialog             The angular material dialog module.
+ * @param router                The angular router module.
+ * @constructor
+ */
+function NfRegistryUsersAdministration(nfRegistryApi, nfStorage, nfRegistryService, activatedRoute, fdsDialogService, matDialog, router) {
+    // Services
+    this.route = activatedRoute;
+    this.nfStorage = nfStorage;
+    this.nfRegistryService = nfRegistryService;
+    this.nfRegistryApi = nfRegistryApi;
+    this.dialogService = fdsDialogService;
+    this.dialog = matDialog;
+    this.router = router;
+};
+
+NfRegistryUsersAdministration.prototype = {
+    constructor: NfRegistryUsersAdministration,
+
+    /**
+     * Initialize the component.
+     */
+    ngOnInit: function () {
+        var self = this;
+        this.nfRegistryService.inProgress = true;
+        this.$subscription = this.route.params
+            .switchMap(function (params) {
+                self.nfRegistryService.adminPerspective = 'users';
+                return new rxjs.Observable.forkJoin(
+                    self.nfRegistryApi.getUsers(),
+                    self.nfRegistryApi.getUserGroups()
+                );
+            })
+            .subscribe(function (response) {
+                if (!response[0].status || response[0].status === 200) {
+                    var users = response[0];
+                    self.nfRegistryService.users = users;
+                } else if (response[0].status === 404) {
+                    self.router.navigateByUrl('/nifi-registry/administration/users');
+                } else if (response[0].status === 409) {
+                    self.router.navigateByUrl('/nifi-registry/administration/workflow');
+                }
+                if (!response[1].status || response[1].status === 200) {
+                    var groups = response[1];
+                    self.nfRegistryService.groups = groups;
+                } else if (response[1].status === 404) {
+                    self.router.navigateByUrl('/nifi-registry/administration/users');
+                } else if (response[1].status === 409) {
+                    self.router.navigateByUrl('/nifi-registry/administration/workflow');
+                }
+                self.nfRegistryService.filterUsersAndGroups();
+                self.nfRegistryService.inProgress = false;
+            });
+    },
+
+    /**
+     * Destroy the component.
+     */
+    ngOnDestroy: function () {
+        this.nfRegistryService.adminPerspective = '';
+        this.nfRegistryService.users = this.nfRegistryService.filteredUsers = [];
+        this.nfRegistryService.groups = this.nfRegistryService.filteredUserGroups = [];
+        this.nfRegistryService.allUsersAndGroupsSelected = false;
+        this.$subscription.unsubscribe();
+    },
+
+    /**
+     * Opens the create new bucket dialog.
+     */
+    addUser: function () {
+        this.dialog.open(NfRegistryAddUser, {
+            disableClose: true
+        });
+    },
+
+    /**
+     * Opens the create new group dialog.
+     */
+    createNewGroup: function () {
+        this.dialog.open(NfRegistryCreateNewGroup, {
+            disableClose: true
+        });
+    },
+
+    /**
+     * Determine if users can be edited.
+     * @returns {boolean}
+     */
+    canEditUsers: function () {
+        return this.nfRegistryService.currentUser.resourcePermissions.tenants.canWrite
+                && this.nfRegistryService.registry.config.supportsConfigurableUsersAndGroups;
+    }
+};
+
+NfRegistryUsersAdministration.annotations = [
+    new ngCore.Component({
+        template: require('./nf-registry-users-administration.html!text'),
+        animations: [nfRegistryAnimations.slideInLeftAnimation],
+        host: {
+            '[@routeAnimation]': 'routeAnimation'
+        }
+    })
+];
+
+NfRegistryUsersAdministration.parameters = [
+    NfRegistryApi,
+    NfStorage,
+    NfRegistryService,
+    ngRouter.ActivatedRoute,
+    ngRouter.Router,
+    fdsDialogsModule.FdsDialogService,
+    ngMaterial.MatDialog
+];
+
+module.exports = NfRegistryUsersAdministration;

http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/6f26290d/nifi-registry-core/nifi-registry-web-ui/src/main/webapp/components/administration/users/nf-registry-users-adminstration.spec.js
----------------------------------------------------------------------
diff --git a/nifi-registry-core/nifi-registry-web-ui/src/main/webapp/components/administration/users/nf-registry-users-adminstration.spec.js b/nifi-registry-core/nifi-registry-web-ui/src/main/webapp/components/administration/users/nf-registry-users-adminstration.spec.js
new file mode 100644
index 0000000..23751c8
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-web-ui/src/main/webapp/components/administration/users/nf-registry-users-adminstration.spec.js
@@ -0,0 +1,191 @@
+/*
+ * 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.
+ */
+
+var NfRegistryRoutes = require('nifi-registry/nf-registry.routes.js');
+var ngCoreTesting = require('@angular/core/testing');
+var ngCommonHttpTesting = require('@angular/common/http/testing');
+var ngCommon = require('@angular/common');
+var ngRouter = require('@angular/router');
+var ngPlatformBrowser = require('@angular/platform-browser');
+var NfRegistry = require('nifi-registry/nf-registry.js');
+var NfRegistryApi = require('nifi-registry/services/nf-registry.api.js');
+var NfRegistryService = require('nifi-registry/services/nf-registry.service.js');
+var NfPageNotFoundComponent = require('nifi-registry/components/page-not-found/nf-registry-page-not-found.js');
+var NfRegistryExplorer = require('nifi-registry/components/explorer/nf-registry-explorer.js');
+var NfRegistryAdministration = require('nifi-registry/components/administration/nf-registry-administration.js');
+var NfRegistryUsersAdministration = require('nifi-registry/components/administration/users/nf-registry-users-administration.js');
+var NfRegistryAddUser = require('nifi-registry/components/administration/users/dialogs/add-user/nf-registry-add-user.js');
+var NfRegistryManageUser = require('nifi-registry/components/administration/users/sidenav/manage-user/nf-registry-manage-user.js');
+var NfRegistryManageGroup = require('nifi-registry/components/administration/users/sidenav/manage-group/nf-registry-manage-group.js');
+var NfRegistryManageBucket = require('nifi-registry/components/administration/workflow/sidenav/manage-bucket/nf-registry-manage-bucket.js');
+var NfRegistryWorkflowAdministration = require('nifi-registry/components/administration/workflow/nf-registry-workflow-administration.js');
+var NfRegistryCreateBucket = require('nifi-registry/components/administration/workflow/dialogs/create-bucket/nf-registry-create-bucket.js');
+var NfRegistryGridListViewer = require('nifi-registry/components/explorer/grid-list/registry/nf-registry-grid-list-viewer.js');
+var NfRegistryBucketGridListViewer = require('nifi-registry/components/explorer/grid-list/registry/nf-registry-bucket-grid-list-viewer.js');
+var NfRegistryDropletGridListViewer = require('nifi-registry/components/explorer/grid-list/registry/nf-registry-droplet-grid-list-viewer.js');
+var fdsCore = require('@flow-design-system/core');
+var ngMoment = require('angular2-moment');
+var rxjs = require('rxjs/Rx');
+var ngCommonHttp = require('@angular/common/http');
+var NfRegistryTokenInterceptor = require('nifi-registry/services/nf-registry.token.interceptor.js');
+var NfStorage = require('nifi-registry/services/nf-storage.service.js');
+var NfLoginComponent = require('nifi-registry/components/login/nf-registry-login.js');
+var NfUserLoginComponent = require('nifi-registry/components/login/dialogs/nf-registry-user-login.js');
+
+describe('NfRegistryUsersAdministration Component', function () {
+    var comp;
+    var fixture;
+    var de;
+    var el;
+    var nfRegistryService;
+    var nfRegistryApi;
+
+    beforeEach(function () {
+        ngCoreTesting.TestBed.configureTestingModule({
+            imports: [
+                ngMoment.MomentModule,
+                ngCommonHttp.HttpClientModule,
+                fdsCore,
+                NfRegistryRoutes,
+                ngCommonHttpTesting.HttpClientTestingModule
+            ],
+            declarations: [
+                NfRegistry,
+                NfRegistryExplorer,
+                NfRegistryAdministration,
+                NfRegistryUsersAdministration,
+                NfRegistryManageUser,
+                NfRegistryManageGroup,
+                NfRegistryManageBucket,
+                NfRegistryAddUser,
+                NfRegistryWorkflowAdministration,
+                NfRegistryCreateBucket,
+                NfRegistryGridListViewer,
+                NfRegistryBucketGridListViewer,
+                NfRegistryDropletGridListViewer,
+                NfPageNotFoundComponent,
+                NfLoginComponent,
+                NfUserLoginComponent
+            ],
+            entryComponents: [
+                NfRegistryCreateBucket
+            ],
+            providers: [
+                NfRegistryService,
+                NfRegistryApi,
+                NfStorage,
+                {
+                    provide: ngCommonHttp.HTTP_INTERCEPTORS,
+                    useClass: NfRegistryTokenInterceptor,
+                    multi: true
+                },
+                {
+                    provide: ngCommon.APP_BASE_HREF,
+                    useValue: '/'
+                }, {
+                    provide: ngRouter.ActivatedRoute,
+                    useValue: {
+                        params: rxjs.Observable.of({})
+                    }
+                }
+            ]
+        });
+
+        fixture = ngCoreTesting.TestBed.createComponent(NfRegistryUsersAdministration);
+
+        // test instance
+        comp = fixture.componentInstance;
+
+        // from the root injector
+        nfRegistryService = ngCoreTesting.TestBed.get(NfRegistryService);
+        nfRegistryApi = ngCoreTesting.TestBed.get(NfRegistryApi);
+        de = fixture.debugElement.query(ngPlatformBrowser.By.css('#nifi-registry-users-administration-perspective'));
+        el = de.nativeElement;
+
+        // Spy
+        spyOn(nfRegistryApi, 'ticketExchange').and.callFake(function () {}).and.returnValue(rxjs.Observable.of({}));
+        spyOn(nfRegistryApi, 'loadCurrentUser').and.callFake(function () {}).and.returnValue(rxjs.Observable.of({}));
+        spyOn(nfRegistryApi, 'getUsers').and.callFake(function () {
+        }).and.returnValue(rxjs.Observable.of([{
+            "identifier": "2e04b4fb-9513-47bb-aa74-1ae34616bfdc",
+            "identity": "User #1"
+        }]));
+        spyOn(nfRegistryApi, 'getUserGroups').and.callFake(function () {
+        }).and.returnValue(rxjs.Observable.of([{
+            "identifier": "5e04b4fb-9513-47bb-aa74-1ae34616bfdc",
+            "identity": "Group #1"}]));
+        spyOn(nfRegistryService, 'filterUsersAndGroups');
+    });
+
+    it('should have a defined component', ngCoreTesting.async(function () {
+        fixture.detectChanges();
+        fixture.whenStable().then(function () { // wait for async getBuckets
+            fixture.detectChanges();
+
+            //assertions
+            expect(comp).toBeDefined();
+            expect(de).toBeDefined();
+            expect(nfRegistryService.adminPerspective).toBe('users');
+            expect(nfRegistryService.inProgress).toBe(false);
+            expect(nfRegistryService.users[0].identity).toEqual('User #1');
+            expect(nfRegistryService.users.length).toBe(1);
+            expect(nfRegistryService.groups[0].identity).toEqual('Group #1');
+            expect(nfRegistryService.groups.length).toBe(1);
+            expect(nfRegistryService.filterUsersAndGroups).toHaveBeenCalled();
+        });
+    }));
+
+    it('should open a dialog to create a new user', function () {
+        spyOn(comp.dialog, 'open')
+        fixture.detectChanges();
+
+        // the function to test
+        comp.addUser();
+
+        //assertions
+        expect(comp.dialog.open).toHaveBeenCalled();
+    });
+
+    it('should open a dialog to create a new group', function () {
+        spyOn(comp.dialog, 'open')
+        fixture.detectChanges();
+
+        // the function to test
+        comp.createNewGroup();
+
+        //assertions
+        expect(comp.dialog.open).toHaveBeenCalled();
+    });
+
+    it('should destroy the component', ngCoreTesting.fakeAsync(function () {
+        fixture.detectChanges();
+        // wait for async getBucket call
+        ngCoreTesting.tick();
+        fixture.detectChanges();
+
+        // The function to test
+        comp.ngOnDestroy();
+
+        //assertions
+        expect(nfRegistryService.adminPerspective).toBe('');
+        expect(nfRegistryService.users.length).toBe(0);
+        expect(nfRegistryService.groups.length).toBe(0);
+        expect(nfRegistryService.filteredUsers.length).toBe(0);
+        expect(nfRegistryService.filteredUserGroups.length).toBe(0);
+        expect(nfRegistryService.allUsersAndGroupsSelected).toBe(false);
+    }));
+});
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/6f26290d/nifi-registry-core/nifi-registry-web-ui/src/main/webapp/components/administration/users/sidenav/manage-group/nf-registry-manage-group.html
----------------------------------------------------------------------
diff --git a/nifi-registry-core/nifi-registry-web-ui/src/main/webapp/components/administration/users/sidenav/manage-group/nf-registry-manage-group.html b/nifi-registry-core/nifi-registry-web-ui/src/main/webapp/components/administration/users/sidenav/manage-group/nf-registry-manage-group.html
new file mode 100644
index 0000000..8317ee2
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-web-ui/src/main/webapp/components/administration/users/sidenav/manage-group/nf-registry-manage-group.html
@@ -0,0 +1,204 @@
+<!--
+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.
+-->
+
+<div fxFill>
+    <div fxLayout="row" fxLayoutAlign="space-between center" class="pad-top-sm pad-bottom-md pad-left-md pad-right-md">
+        <span class="md-card-title ellipsis"><i class="fa fa-users push-right-sm" aria-hidden="true"></i>{{nfRegistryService.group.identity}}</span>
+        <button mat-icon-button (click)="closeSideNav()">
+            <mat-icon color="primary">close</mat-icon>
+        </button>
+    </div>
+    <div class="sidenav-content">
+        <div class="pad-bottom-md pad-left-md pad-right-md" flex fxLayoutAlign="start center">
+            <mat-input-container flex>
+                <input #groupnameInput matInput
+                       [disabled]="!nfRegistryService.currentUser.resourcePermissions.tenants.canWrite || !nfRegistryService.group.configurable"
+                       placeholder="Identity/Group Name"
+                       value="{{nfRegistryService.group.identity}}"
+                       [(ngModel)]="_groupname">
+            </mat-input-container>
+            <button [disabled]="nfRegistryService.group.identity === _groupname || !nfRegistryService.group.configurable"
+                    (click)="updateGroupName(groupnameInput.value)"
+                    class="input-button"
+                    color="fds-regular"
+                    mat-raised-button>
+                Save
+            </button>
+        </div>
+        <div class="pad-bottom-md pad-left-md pad-right-md" flex fxLayout="column" fxLayoutAlign="space-between start">
+            <div>
+            <span class="header">Special Privileges
+                <i matTooltip="Additional permissions that allow a user to manage or access certain aspects of the registry."
+                   class="pad-left-sm fa fa-question-circle-o help-icon"></i>
+            </span>
+            </div>
+            <mat-checkbox [disabled]="!canEditSpecialPrivileges()"
+                          [checked]="nfRegistryService.group.resourcePermissions.buckets.canRead && nfRegistryService.group.resourcePermissions.buckets.canWrite && nfRegistryService.group.resourcePermissions.buckets.canDelete"
+                          (change)="toggleGroupManageBucketsPrivileges($event)">
+            <span class="description">Can manage buckets<i
+                    matTooltip="Allow a user to manage all buckets in the registry, as well as provide the user access to all buckets from a connected system (e.g., NiFi)."
+                    class="pad-left-sm fa fa-question-circle-o help-icon"></i></span>
+            </mat-checkbox>
+            <div flex fxLayout="row" fxLayoutAlign="space-around center">
+                <mat-checkbox class="pad-left-md"
+                              [disabled]="!canEditSpecialPrivileges()"
+                              [(checked)]="nfRegistryService.group.resourcePermissions.buckets.canRead"
+                              (change)="toggleGroupManageBucketsPrivileges($event, 'read')">
+                    <span class="description">Read</span>
+                </mat-checkbox>
+                <mat-checkbox class="pad-left-md"
+                              [disabled]="!canEditSpecialPrivileges()"
+                              [(checked)]="nfRegistryService.group.resourcePermissions.buckets.canWrite"
+                              (change)="toggleGroupManageBucketsPrivileges($event, 'write')">
+                    <span class="description">Write</span>
+                </mat-checkbox>
+                <mat-checkbox class="pad-left-md"
+                              [disabled]="!canEditSpecialPrivileges()"
+                              [(checked)]="nfRegistryService.group.resourcePermissions.buckets.canDelete"
+                              (change)="toggleGroupManageBucketsPrivileges($event, 'delete')">
+                    <span class="description">Delete</span>
+                </mat-checkbox>
+            </div>
+            <mat-checkbox [disabled]="!canEditSpecialPrivileges()"
+                          [checked]="nfRegistryService.group.resourcePermissions.tenants.canRead && nfRegistryService.group.resourcePermissions.tenants.canWrite && nfRegistryService.group.resourcePermissions.tenants.canDelete"
+                          (change)="toggleGroupManageTenantsPrivileges($event)">
+            <span class="description">Can manage users<i
+                    matTooltip="Allow a user to manage all registry users and groups."
+                    class="pad-left-sm fa fa-question-circle-o help-icon"></i></span>
+            </mat-checkbox>
+            <div flex fxLayout="row" fxLayoutAlign="space-around center">
+                <mat-checkbox class="pad-left-md"
+                              [disabled]="!canEditSpecialPrivileges()"
+                              [(checked)]="nfRegistryService.group.resourcePermissions.tenants.canRead"
+                              (change)="toggleGroupManageTenantsPrivileges($event, 'read')">
+                    <span class="description">Read</span>
+                </mat-checkbox>
+                <mat-checkbox class="pad-left-md"
+                              [disabled]="!canEditSpecialPrivileges()"
+                              [(checked)]="nfRegistryService.group.resourcePermissions.tenants.canWrite"
+                              (change)="toggleGroupManageTenantsPrivileges($event, 'write')">
+                    <span class="description">Write</span>
+                </mat-checkbox>
+                <mat-checkbox class="pad-left-md"
+                              [disabled]="!canEditSpecialPrivileges()"
+                              [(checked)]="nfRegistryService.group.resourcePermissions.tenants.canDelete"
+                              (change)="toggleGroupManageTenantsPrivileges($event, 'delete')">
+                    <span class="description">Delete</span>
+                </mat-checkbox>
+            </div>
+            <mat-checkbox [disabled]="!canEditSpecialPrivileges()"
+                          [checked]="nfRegistryService.group.resourcePermissions.policies.canRead && nfRegistryService.group.resourcePermissions.policies.canWrite && nfRegistryService.group.resourcePermissions.policies.canDelete"
+                          (change)="toggleGroupManagePoliciesPrivileges($event)">
+            <span class="description">Can manage policies<i
+                    matTooltip="Allow a user to grant all registry users read, write, and delete permission to a bucket."
+                    class="pad-left-sm fa fa-question-circle-o help-icon"></i></span>
+            </mat-checkbox>
+            <div flex fxLayout="row" fxLayoutAlign="space-around center">
+                <mat-checkbox class="pad-left-md"
+                              [disabled]="!canEditSpecialPrivileges()"
+                              [(checked)]="nfRegistryService.group.resourcePermissions.policies.canRead"
+                              (change)="toggleGroupManagePoliciesPrivileges($event, 'read')">
+                    <span class="description">Read</span>
+                </mat-checkbox>
+                <mat-checkbox class="pad-left-md"
+                              [disabled]="!canEditSpecialPrivileges()"
+                              [(checked)]="nfRegistryService.group.resourcePermissions.policies.canWrite"
+                              (change)="toggleGroupManagePoliciesPrivileges($event, 'write')">
+                    <span class="description">Write</span>
+                </mat-checkbox>
+                <mat-checkbox class="pad-left-md"
+                              [disabled]="!canEditSpecialPrivileges()"
+                              [(checked)]="nfRegistryService.group.resourcePermissions.policies.canDelete"
+                              (change)="toggleGroupManagePoliciesPrivileges($event, 'delete')">
+                    <span class="description">Delete</span>
+                </mat-checkbox>
+            </div>
+            <mat-checkbox [disabled]="!canEditSpecialPrivileges()"
+                          [checked]="nfRegistryService.group.resourcePermissions.proxy.canWrite"
+                          (change)="toggleGroupManageProxyPrivileges($event)">
+            <span class="description">Can proxy user requests<i
+                    matTooltip="Allow a connected system (e.g., NiFi) to process requests of authorized users of that system."
+                    class="pad-left-sm fa fa-question-circle-o help-icon"></i></span>
+            </mat-checkbox>
+        </div>
+        <mat-button-toggle-group name="nifi-registry-manage-group-perspective" class="pad-left-md tab-toggle-group">
+            <mat-button-toggle [checked]="manageGroupPerspective === 'membership'"
+                               value="membership"
+                               class="uppercase"
+                               (change)="manageGroupPerspective = 'membership'"
+                               i18n="User membership tab, group management sidenav|View the users that belong to this group.@@nf-admin-group-management-sidenav-membership-tab-title">
+                Membership
+            </mat-button-toggle>
+        </mat-button-toggle-group>
+        <div *ngIf="manageGroupPerspective === 'membership'">
+            <div *ngIf="nfRegistryService.group.users" class="pad-top-md pad-bottom-sm pad-left-md pad-right-md">
+                <div flex fxLayout="row" fxLayoutAlign="space-between center">
+                    <span class="md-card-title">Membership ({{nfRegistryService.group.users.length}})</span>
+                    <button color="fds-secondary"
+                            [disabled]="!nfRegistryService.currentUser.resourcePermissions.tenants.canWrite || !nfRegistryService.group.configurable"
+                            mat-raised-button
+                            (click)="addUsersToGroup()">
+                        Add Users
+                    </button>
+                </div>
+                <div id="nifi-registry-group-membership-list-container-column-header" fxLayout="row"
+                     fxLayoutAlign="space-between center" class="td-data-table">
+                    <div class="td-data-table-column" (click)="sortUsers(column)"
+                         *ngFor="let column of usersColumns"
+                         fxFlex="{{column.width}}">
+                        {{column.label}}
+                        <i *ngIf="column.active && column.sortable && column.sortOrder === 'ASC'" class="fa fa-caret-up"
+                           aria-hidden="true"></i>
+                        <i *ngIf="column.active && column.sortable && column.sortOrder === 'DESC'"
+                           class="fa fa-caret-down"
+                           aria-hidden="true"></i>
+                    </div>
+                </div>
+                <div id="nifi-registry-group-membership-list-container">
+                    <div fxLayout="row" fxLayoutAlign="space-between center" class="td-data-table-row"
+                         [ngClass]="{'selected' : row.checked}" *ngFor="let row of filteredUsers"
+                         (click)="row.checked = !row.checked">
+                        <div class="td-data-table-cell" *ngFor="let column of usersColumns"
+                             fxFlex="{{column.width}}">
+                            <div class="ellipsis"
+                                 matTooltip="{{column.format ? column.format(row[column.name]) : row[column.name]}}">
+                                {{column.format ? column.format(row[column.name]) : row[column.name]}}
+                            </div>
+                        </div>
+                        <div class="td-data-table-cell">
+                            <div>
+                                <button (click)="removeUserFromGroup(row);row.checked = !row.checked;"
+                                        [disabled]="!nfRegistryService.currentUser.resourcePermissions.tenants.canWrite || !nfRegistryService.group.configurable"
+                                        matTooltip="'Remove user from group'" mat-icon-button color="accent">
+                                    <i class="fa fa-trash" aria-hidden="true"></i>
+                                </button>
+                            </div>
+                        </div>
+                    </div>
+                </div>
+                <div class="mat-padding" *ngIf="nfRegistryService.group.users.length === 0" layout="row"
+                     layout-align="center center">
+                    <h3>This group does not have any users yet.</h3>
+                </div>
+            </div>
+        </div>
+    </div>
+    <button id="nf-registry-user-permissions-side-nav-container" class="push-right-md" mat-raised-button
+            color="fds-primary"
+            (click)="closeSideNav()">Close
+    </button>
+</div>

http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/6f26290d/nifi-registry-core/nifi-registry-web-ui/src/main/webapp/components/administration/users/sidenav/manage-group/nf-registry-manage-group.js
----------------------------------------------------------------------
diff --git a/nifi-registry-core/nifi-registry-web-ui/src/main/webapp/components/administration/users/sidenav/manage-group/nf-registry-manage-group.js b/nifi-registry-core/nifi-registry-web-ui/src/main/webapp/components/administration/users/sidenav/manage-group/nf-registry-manage-group.js
new file mode 100644
index 0000000..94f99a5
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-web-ui/src/main/webapp/components/administration/users/sidenav/manage-group/nf-registry-manage-group.js
@@ -0,0 +1,609 @@
+/*
+ * 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.
+ */
+
+var covalentCore = require('@covalent/core');
+var fdsDialogsModule = require('@flow-design-system/dialogs');
+var fdsSnackBarsModule = require('@flow-design-system/snackbars');
+var ngCore = require('@angular/core');
+var NfRegistryService = require('nifi-registry/services/nf-registry.service.js');
+var ngRouter = require('@angular/router');
+var NfRegistryApi = require('nifi-registry/services/nf-registry.api.js');
+var ngMaterial = require('@angular/material');
+var NfRegistryAddUsersToGroup = require('nifi-registry/components/administration/users/dialogs/add-users-to-group/nf-registry-add-users-to-group.js');
+
+/**
+ * NfRegistryManageGroup constructor.
+ *
+ * @param nfRegistryApi         The api service.
+ * @param nfRegistryService     The nf-registry.service module.
+ * @param tdDataTableService    The covalent data table service module.
+ * @param fdsDialogService      The FDS dialog service.
+ * @param fdsSnackBarService    The FDS snack bar service module.
+ * @param activatedRoute        The angular route module.
+ * @param router                The angular router module.
+ * @param matDialog             The angular material dialog module.
+ * @constructor
+ */
+function NfRegistryManageGroup(nfRegistryApi, nfRegistryService, tdDataTableService, fdsDialogService, fdsSnackBarService, activatedRoute, router, matDialog) {
+    // local state
+    this.sortBy;
+    this.sortOrder;
+    this.filteredUsers = [];
+    this.usersSearchTerms = [];
+    this._groupname = '';
+    this.manageGroupPerspective = 'membership';
+    this.usersColumns = [
+        {
+            name: 'identity',
+            label: 'Display Name',
+            sortable: true,
+            tooltip: 'Group name.',
+            width: 100
+        }
+    ];
+
+    // Services
+    this.nfRegistryService = nfRegistryService;
+    this.route = activatedRoute;
+    this.router = router;
+    this.dialog = matDialog;
+    this.nfRegistryApi = nfRegistryApi;
+    this.dialogService = fdsDialogService;
+    this.snackBarService = fdsSnackBarService;
+    this.dataTableService = tdDataTableService;
+};
+
+NfRegistryManageGroup.prototype = {
+    constructor: NfRegistryManageGroup,
+
+    /**
+     * Initialize the component.
+     */
+    ngOnInit: function () {
+        var self = this;
+        // subscribe to the route params
+        this.$subscription = self.route.params
+            .switchMap(function (params) {
+                return self.nfRegistryApi.getUserGroup(params['groupId']);
+            })
+            .subscribe(function (response) {
+                if (!response.status || response.status === 200) {
+                    self.nfRegistryService.sidenav.open();
+                    self.nfRegistryService.group = response;
+                    self._groupname = response.identity;
+                    self.filterUsers();
+                } else if (response.status === 404) {
+                    self.router.navigateByUrl('/nifi-registry/administration/users');
+                } else if (response.status === 409) {
+                    self.router.navigateByUrl('/nifi-registry/administration/workflow');
+                }
+            });
+    },
+
+    /**
+     * Destroy the component.
+     */
+    ngOnDestroy: function () {
+        this.nfRegistryService.sidenav.close();
+        this.$subscription.unsubscribe();
+    },
+
+    /**
+     * Navigate to administer users for current registry.
+     */
+    closeSideNav: function () {
+        this.router.navigateByUrl('/nifi-registry/administration/users');
+    },
+
+    /**
+     * Toggles the manage bucket privileges for the group.
+     *
+     * @param $event
+     * @param policyAction      The action to be toggled
+     */
+    toggleGroupManageBucketsPrivileges: function ($event, policyAction) {
+        var self = this;
+        if ($event.checked) {
+            for (var resource in this.nfRegistryService.BUCKETS_PRIVS) {
+                if (this.nfRegistryService.BUCKETS_PRIVS.hasOwnProperty(resource)) {
+                    this.nfRegistryService.BUCKETS_PRIVS[resource].forEach(function (action) {
+                        if (!policyAction || (action === policyAction)) {
+                            self.nfRegistryApi.getPolicyActionResource(action, resource).subscribe(function (policy) {
+                                if (policy.status && policy.status === 404) {
+                                    // resource does NOT exist, let's create it
+                                    self.nfRegistryApi.postPolicyActionResource(action, resource, self.nfRegistryService.group.users, []).subscribe(
+                                        function (response) {
+                                            // can manage buckets privileges created and granted!!!...now update the view
+                                            response.userGroups.forEach(function (group) {
+                                                if (group.identifier === self.nfRegistryService.group.identifier) {
+                                                    self.nfRegistryApi.getUserGroup(self.nfRegistryService.group.identifier).subscribe(function (response) {
+                                                        self.nfRegistryService.group = response;
+                                                    });
+                                                }
+                                            });
+                                        });
+                                } else {
+                                    // resource exists, let's update it
+                                    policy.userGroups.push(self.nfRegistryService.group);
+                                    self.nfRegistryApi.putPolicyActionResource(policy.identifier, policy.action,
+                                        policy.resource, policy.users, policy.userGroups).subscribe(
+                                        function (response) {
+                                            // can manage buckets privileges updated!!!...now update the view
+                                            response.userGroups.forEach(function (group) {
+                                                if (group.identifier === self.nfRegistryService.group.identifier) {
+                                                    self.nfRegistryApi.getUserGroup(self.nfRegistryService.group.identifier).subscribe(function (response) {
+                                                        self.nfRegistryService.group = response;
+                                                    });
+                                                }
+                                            });
+                                        });
+                                }
+                            });
+                        }
+                    });
+                }
+            }
+        } else {
+            // Remove the current group from the administrator resources
+            for (var resource in this.nfRegistryService.BUCKETS_PRIVS) {
+                if (this.nfRegistryService.BUCKETS_PRIVS.hasOwnProperty(resource)) {
+                    this.nfRegistryService.BUCKETS_PRIVS[resource].forEach(function (action) {
+                        if (!policyAction || (action === policyAction)) {
+                            self.nfRegistryApi.getPolicyActionResource(action, resource).subscribe(function (policy) {
+                                if (policy.status && policy.status === 404) {
+                                    // resource does NOT exist
+                                } else {
+                                    // resource exists, let's filter out the current group and update it
+                                    policy.userGroups = policy.userGroups.filter(function (group) {
+                                        return (group.identifier !== self.nfRegistryService.group.identifier) ? true : false;
+                                    });
+                                    self.nfRegistryApi.putPolicyActionResource(policy.identifier, policy.action,
+                                        policy.resource, policy.users, policy.userGroups).subscribe(
+                                        function (response) {
+                                            // can manage buckets privileges updated!!!...now update the view
+                                            self.nfRegistryApi.getUserGroup(self.nfRegistryService.group.identifier).subscribe(function (response) {
+                                                self.nfRegistryService.group = response;
+                                            });
+                                        });
+                                }
+                            });
+                        }
+                    });
+                }
+            }
+        }
+    },
+
+    /**
+     * Toggles the manage tenants privileges for the group.
+     *
+     * @param $event
+     * @param policyAction      The action to be toggled
+     */
+    toggleGroupManageTenantsPrivileges: function ($event, policyAction) {
+        var self = this;
+        if ($event.checked) {
+            for (var resource in this.nfRegistryService.TENANTS_PRIVS) {
+                if (this.nfRegistryService.TENANTS_PRIVS.hasOwnProperty(resource)) {
+                    this.nfRegistryService.TENANTS_PRIVS[resource].forEach(function (action) {
+                        if (!policyAction || (action === policyAction)) {
+                            self.nfRegistryApi.getPolicyActionResource(action, resource).subscribe(function (policy) {
+                                if (policy.status && policy.status === 404) {
+                                    // resource does NOT exist, let's create it
+                                    self.nfRegistryApi.postPolicyActionResource(action, resource, self.nfRegistryService.group.users, []).subscribe(
+                                        function (response) {
+                                            // can manage tenants privileges created and granted!!!...now update the view
+                                            response.userGroups.forEach(function (group) {
+                                                if (group.identifier === self.nfRegistryService.group.identifier) {
+                                                    self.nfRegistryApi.getUserGroup(self.nfRegistryService.group.identifier).subscribe(function (response) {
+                                                        self.nfRegistryService.group = response;
+                                                    });
+                                                }
+                                            });
+                                        });
+                                } else {
+                                    // resource exists, let's update it
+                                    policy.userGroups.push(self.nfRegistryService.group);
+                                    self.nfRegistryApi.putPolicyActionResource(policy.identifier, policy.action,
+                                        policy.resource, policy.users, policy.userGroups).subscribe(
+                                        function (response) {
+                                            // can manage tenants privileges updated!!!...now update the view
+                                            response.userGroups.forEach(function (group) {
+                                                if (group.identifier === self.nfRegistryService.group.identifier) {
+                                                    self.nfRegistryApi.getUserGroup(self.nfRegistryService.group.identifier).subscribe(function (response) {
+                                                        self.nfRegistryService.group = response;
+                                                    });
+                                                }
+                                            });
+                                        });
+                                }
+                            });
+                        }
+                    });
+                }
+            }
+        } else {
+            // Remove the current group from the administrator resources
+            for (var resource in this.nfRegistryService.TENANTS_PRIVS) {
+                if (this.nfRegistryService.TENANTS_PRIVS.hasOwnProperty(resource)) {
+                    this.nfRegistryService.TENANTS_PRIVS[resource].forEach(function (action) {
+                        if (!policyAction || (action === policyAction)) {
+                            self.nfRegistryApi.getPolicyActionResource(action, resource).subscribe(function (policy) {
+                                if (policy.status && policy.status === 404) {
+                                    // resource does NOT exist
+                                } else {
+                                    // resource exists, let's filter out the current group and update it
+                                    policy.userGroups = policy.userGroups.filter(function (group) {
+                                        return (group.identifier !== self.nfRegistryService.group.identifier) ? true : false;
+                                    });
+                                    self.nfRegistryApi.putPolicyActionResource(policy.identifier, policy.action,
+                                        policy.resource, policy.users, policy.userGroups).subscribe(
+                                        function (response) {
+                                            // can manage tenants privileges updated!!!...now update the view
+                                            self.nfRegistryApi.getUserGroup(self.nfRegistryService.group.identifier).subscribe(function (response) {
+                                                self.nfRegistryService.group = response;
+                                            });
+                                        });
+                                }
+                            });
+                        }
+                    });
+                }
+            }
+        }
+    },
+
+    /**
+     * Toggles the manage policies privileges for the group.
+     *
+     * @param $event
+     * @param policyAction      The action to be toggled
+     */
+    toggleGroupManagePoliciesPrivileges: function ($event, policyAction) {
+        var self = this;
+        if ($event.checked) {
+            for (var resource in this.nfRegistryService.POLICIES_PRIVS) {
+                if (this.nfRegistryService.POLICIES_PRIVS.hasOwnProperty(resource)) {
+                    this.nfRegistryService.POLICIES_PRIVS[resource].forEach(function (action) {
+                        if (!policyAction || (action === policyAction)) {
+                            self.nfRegistryApi.getPolicyActionResource(action, resource).subscribe(function (policy) {
+                                if (policy.status && policy.status === 404) {
+                                    // resource does NOT exist, let's create it
+                                    self.nfRegistryApi.postPolicyActionResource(action, resource, self.nfRegistryService.group.users, []).subscribe(
+                                        function (response) {
+                                            // can manage policies privileges created and granted!!!...now update the view
+                                            response.userGroups.forEach(function (group) {
+                                                if (group.identifier === self.nfRegistryService.group.identifier) {
+                                                    self.nfRegistryApi.getUserGroup(self.nfRegistryService.group.identifier).subscribe(function (response) {
+                                                        self.nfRegistryService.group = response;
+                                                    });
+                                                }
+                                            });
+                                        });
+                                } else {
+                                    // resource exists, let's update it
+                                    policy.userGroups.push(self.nfRegistryService.group);
+                                    self.nfRegistryApi.putPolicyActionResource(policy.identifier, policy.action,
+                                        policy.resource, policy.users, policy.userGroups).subscribe(
+                                        function (response) {
+                                            // can manage policies privileges updated!!!...now update the view
+                                            response.userGroups.forEach(function (group) {
+                                                if (group.identifier === self.nfRegistryService.group.identifier) {
+                                                    self.nfRegistryApi.getUserGroup(self.nfRegistryService.group.identifier).subscribe(function (response) {
+                                                        self.nfRegistryService.group = response;
+                                                    });
+                                                }
+                                            });
+                                        });
+                                }
+                            });
+                        }
+                    });
+                }
+            }
+        } else {
+            // Remove the current group from the administrator resources
+            for (var resource in this.nfRegistryService.POLICIES_PRIVS) {
+                if (this.nfRegistryService.POLICIES_PRIVS.hasOwnProperty(resource)) {
+                    this.nfRegistryService.POLICIES_PRIVS[resource].forEach(function (action) {
+                        if (!policyAction || (action === policyAction)) {
+                            self.nfRegistryApi.getPolicyActionResource(action, resource).subscribe(function (policy) {
+                                if (policy.status && policy.status === 404) {
+                                    // resource does NOT exist
+                                } else {
+                                    // resource exists, let's filter out the current group and update it
+                                    policy.userGroups = policy.userGroups.filter(function (group) {
+                                        return (group.identifier !== self.nfRegistryService.group.identifier) ? true : false;
+                                    });
+                                    self.nfRegistryApi.putPolicyActionResource(policy.identifier, policy.action,
+                                        policy.resource, policy.users, policy.userGroups).subscribe(
+                                        function (response) {
+                                            // can manage policies privileges updated!!!...now update the view
+                                            self.nfRegistryApi.getUserGroup(self.nfRegistryService.group.identifier).subscribe(function (response) {
+                                                self.nfRegistryService.group = response;
+                                            });
+                                        });
+                                }
+                            });
+                        }
+                    });
+                }
+            }
+        }
+    },
+
+    /**
+     * Toggles the manage proxy privileges for the group.
+     *
+     * @param $event
+     * @param policyAction      The action to be toggled
+     */
+    toggleGroupManageProxyPrivileges: function ($event, policyAction) {
+        var self = this;
+        if ($event.checked) {
+            for (var resource in this.nfRegistryService.PROXY_PRIVS) {
+                if (this.nfRegistryService.PROXY_PRIVS.hasOwnProperty(resource)) {
+                    this.nfRegistryService.PROXY_PRIVS[resource].forEach(function (action) {
+                        if (!policyAction || (action === policyAction)) {
+                            self.nfRegistryApi.getPolicyActionResource(action, resource).subscribe(function (policy) {
+                                if (policy.status && policy.status === 404) {
+                                    // resource does NOT exist, let's create it
+                                    self.nfRegistryApi.postPolicyActionResource(action, resource, self.nfRegistryService.group.users, []).subscribe(
+                                        function (response) {
+                                            // can manage proxy privileges created and granted!!!...now update the view
+                                            response.userGroups.forEach(function (group) {
+                                                if (group.identifier === self.nfRegistryService.group.identifier) {
+                                                    self.nfRegistryApi.getUserGroup(self.nfRegistryService.group.identifier).subscribe(function (response) {
+                                                        self.nfRegistryService.group = response;
+                                                    });
+                                                }
+                                            });
+                                        });
+                                } else {
+                                    // resource exists, let's update it
+                                    policy.userGroups.push(self.nfRegistryService.group);
+                                    self.nfRegistryApi.putPolicyActionResource(policy.identifier, policy.action,
+                                        policy.resource, policy.users, policy.userGroups).subscribe(
+                                        function (response) {
+                                            // can manage proxy privileges updated!!!...now update the view
+                                            response.userGroups.forEach(function (group) {
+                                                if (group.identifier === self.nfRegistryService.group.identifier) {
+                                                    self.nfRegistryApi.getUserGroup(self.nfRegistryService.group.identifier).subscribe(function (response) {
+                                                        self.nfRegistryService.group = response;
+                                                    });
+                                                }
+                                            });
+                                        });
+                                }
+                            });
+                        }
+                    });
+                }
+            }
+        } else {
+            // Remove the current group from the administrator resources
+            for (var resource in this.nfRegistryService.PROXY_PRIVS) {
+                if (this.nfRegistryService.PROXY_PRIVS.hasOwnProperty(resource)) {
+                    this.nfRegistryService.PROXY_PRIVS[resource].forEach(function (action) {
+                        if (!policyAction || (action === policyAction)) {
+                            self.nfRegistryApi.getPolicyActionResource(action, resource).subscribe(function (policy) {
+                                if (policy.status && policy.status === 404) {
+                                    // resource does NOT exist
+                                } else {
+                                    // resource exists, let's filter out the current group and update it
+                                    policy.userGroups = policy.userGroups.filter(function (group) {
+                                        return (group.identifier !== self.nfRegistryService.group.identifier) ? true : false;
+                                    });
+                                    self.nfRegistryApi.putPolicyActionResource(policy.identifier, policy.action,
+                                        policy.resource, policy.users, policy.userGroups).subscribe(
+                                        function (response) {
+                                            // can manage proxy privileges updated!!!...now update the view
+                                            self.nfRegistryApi.getUserGroup(self.nfRegistryService.group.identifier).subscribe(function (response) {
+                                                self.nfRegistryService.group = response;
+                                            });
+                                        });
+                                }
+                            });
+                        }
+                    });
+                }
+            }
+        }
+    },
+
+    /**
+     * Opens a modal dialog UX enabling the addition of users to this group.
+     */
+    addUsersToGroup: function () {
+        var self = this;
+        this.dialog.open(NfRegistryAddUsersToGroup, {
+            data: {
+                group: this.nfRegistryService.group,
+                disableClose: true
+            }
+        }).afterClosed().subscribe(function () {
+            self.nfRegistryApi.getUserGroup(self.nfRegistryService.group.identifier)
+                .subscribe(function (response) {
+                    self.nfRegistryService.group = response;
+                    self._groupname = response.identity;
+                    self.filterUsers();
+                });
+        });
+    },
+
+    /**
+     * Filter users.
+     *
+     * @param {string} [sortBy]       The column name to sort `usersColumns` by.
+     * @param {string} [sortOrder]    The order. Either 'ASC' or 'DES'
+     */
+    filterUsers: function (sortBy, sortOrder) {
+        // if `sortOrder` is `undefined` then use 'ASC'
+        if (sortOrder === undefined) {
+            if (this.sortOrder === undefined) {
+                sortOrder = 'ASC'
+            } else {
+                sortOrder = this.sortOrder
+            }
+        }
+        // if `sortBy` is `undefined` then find the first sortable column in `usersColumns`
+        if (sortBy === undefined) {
+            if (this.sortBy === undefined) {
+                var arrayLength = this.usersColumns.length;
+                for (var i = 0; i < arrayLength; i++) {
+                    if (this.usersColumns[i].sortable === true) {
+                        sortBy = this.usersColumns[i].name;
+                        //only one column can be actively sorted so we reset all to inactive
+                        this.usersColumns.forEach(function (c) {
+                            c.active = false;
+                        });
+                        //and set this column as the actively sorted column
+                        this.usersColumns[i].active = true;
+                        this.usersColumns[i].sortOrder = sortOrder;
+                        break;
+                    }
+                }
+            } else {
+                sortBy = this.sortBy
+            }
+        }
+
+        var newUsersData = this.nfRegistryService.group.users || [];
+
+        for (var i = 0; i < this.usersSearchTerms.length; i++) {
+            newUsersData = this.filterData(newUsersData, this.usersSearchTerms[i], true);
+        }
+
+        newUsersData = this.dataTableService.sortData(newUsersData, sortBy, sortOrder);
+        this.filteredUsers = newUsersData;
+    },
+
+    /**
+     * Sort `users` by `column`.
+     *
+     * @param column    The column to sort by.
+     */
+    sortUsers: function (column) {
+        if (column.sortable) {
+            this.sortBy = column.name;
+            this.sortOrder = column.sortOrder = (column.sortOrder === 'ASC') ? 'DESC' : 'ASC';
+            this.filterUsers(this.sortBy, this.sortOrder);
+
+            //only one column can be actively sorted so we reset all to inactive
+            this.usersColumns.forEach(function (c) {
+                c.active = false;
+            });
+            //and set this column as the actively sorted column
+            column.active = true;
+        }
+    },
+
+    /**
+     * Remove user from group.
+     *
+     * @param user
+     */
+    removeUserFromGroup: function (user) {
+        var self = this;
+        var users = this.nfRegistryService.group.users.filter(function (u) {
+            if (u.identifier !== user.identifier) {
+                return u;
+            }
+        });
+
+        this.nfRegistryApi.updateUserGroup(this.nfRegistryService.group.identifier, this.nfRegistryService.group.identity, users).subscribe(function (response) {
+            self.nfRegistryApi.getUserGroup(self.nfRegistryService.group.identifier)
+                .subscribe(function (response) {
+                    self.nfRegistryService.group = response;
+                    self.filterUsers();
+                });
+            var snackBarRef = self.snackBarService.openCoaster({
+                title: 'Success',
+                message: 'The user has been removed from the ' + self.nfRegistryService.group.identity + ' group.',
+                verticalPosition: 'bottom',
+                horizontalPosition: 'right',
+                icon: 'fa fa-check-circle-o',
+                color: '#1EB475',
+                duration: 3000
+            });
+        });
+    },
+
+    /**
+     * Update group name.
+     *
+     * @param groupname
+     */
+    updateGroupName: function (groupname) {
+        var self = this;
+        this.nfRegistryApi.updateUserGroup(this.nfRegistryService.group.identifier, groupname, this.nfRegistryService.group.users).subscribe(function (response) {
+            if (!response.status || response.status === 200) {
+                self.nfRegistryService.group = response;
+                self.nfRegistryService.groups.filter(function (group) {
+                    if (self.nfRegistryService.group.identifier === group.identifier) {
+                        group.identity = response.identity;
+                    }
+                });
+                var snackBarRef = self.snackBarService.openCoaster({
+                    title: 'Success',
+                    message: 'This group name has been updated.',
+                    verticalPosition: 'bottom',
+                    horizontalPosition: 'right',
+                    icon: 'fa fa-check-circle-o',
+                    color: '#1EB475',
+                    duration: 3000
+                });
+            } else if (response.status === 409) {
+                self._groupname = self.nfRegistryService.group.identity;
+                self.dialogService.openConfirm({
+                    title: 'Error',
+                    message: 'This group already exists. Please enter a different identity/group name.',
+                    acceptButton: 'Ok',
+                    acceptButtonColor: 'fds-warn'
+                });
+            }
+        });
+    },
+
+    /**
+     * Determine if 'Special Privileges' can be edited.
+     * @returns {boolean}
+     */
+    canEditSpecialPrivileges: function() {
+        return this.nfRegistryService.currentUser.resourcePermissions.policies.canWrite
+                && this.nfRegistryService.registry.config.supportsConfigurableAuthorizer;
+    }
+};
+
+NfRegistryManageGroup.annotations = [
+    new ngCore.Component({
+        template: require('./nf-registry-manage-group.html!text')
+    })
+];
+
+NfRegistryManageGroup.parameters = [
+    NfRegistryApi,
+    NfRegistryService,
+    covalentCore.TdDataTableService,
+    fdsDialogsModule.FdsDialogService,
+    fdsSnackBarsModule.FdsSnackBarService,
+    ngRouter.ActivatedRoute,
+    ngRouter.Router,
+    ngMaterial.MatDialog
+];
+
+module.exports = NfRegistryManageGroup;


[51/51] [partial] nifi-registry git commit: NIFIREG-201 Refactoring project structure to better isolate extensions

Posted by kd...@apache.org.
NIFIREG-201 Refactoring project structure to better isolate extensions

This closes #143.

Signed-off-by: Kevin Doran <kd...@apache.org>


Project: http://git-wip-us.apache.org/repos/asf/nifi-registry/repo
Commit: http://git-wip-us.apache.org/repos/asf/nifi-registry/commit/6f26290d
Tree: http://git-wip-us.apache.org/repos/asf/nifi-registry/tree/6f26290d
Diff: http://git-wip-us.apache.org/repos/asf/nifi-registry/diff/6f26290d

Branch: refs/heads/master
Commit: 6f26290d78ffb3a7cf0ba44ef62346f55960054c
Parents: 9258ad5
Author: Bryan Bende <bb...@apache.org>
Authored: Fri Sep 21 13:52:53 2018 -0400
Committer: Kevin Doran <kd...@apache.org>
Committed: Fri Sep 21 22:10:04 2018 -0400

----------------------------------------------------------------------
 nifi-registry-bootstrap/pom.xml                 |   40 -
 .../nifi/registry/bootstrap/BootstrapCodec.java |  108 -
 .../bootstrap/NiFiRegistryListener.java         |  141 -
 .../registry/bootstrap/RunNiFiRegistry.java     | 1246 ---
 .../nifi/registry/bootstrap/ShutdownHook.java   |   97 -
 .../exception/InvalidCommandException.java      |   38 -
 .../bootstrap/util/LimitingInputStream.java     |  107 -
 .../nifi/registry/bootstrap/util/OSUtils.java   |  107 -
 nifi-registry-client/pom.xml                    |   61 -
 .../nifi/registry/client/BucketClient.java      |   76 -
 .../apache/nifi/registry/client/FlowClient.java |  119 -
 .../registry/client/FlowSnapshotClient.java     |  133 -
 .../nifi/registry/client/ItemsClient.java       |   64 -
 .../registry/client/NiFiRegistryClient.java     |   89 -
 .../client/NiFiRegistryClientConfig.java        |  257 -
 .../registry/client/NiFiRegistryException.java  |   32 -
 .../apache/nifi/registry/client/UserClient.java |   42 -
 .../client/impl/AbstractJerseyClient.java       |  120 -
 .../client/impl/BucketItemDeserializer.java     |   76 -
 .../client/impl/JerseyBucketClient.java         |  140 -
 .../registry/client/impl/JerseyFlowClient.java  |  205 -
 .../client/impl/JerseyFlowSnapshotClient.java   |  246 -
 .../registry/client/impl/JerseyItemsClient.java |   85 -
 .../client/impl/JerseyNiFiRegistryClient.java   |  247 -
 .../registry/client/impl/JerseyUserClient.java  |   47 -
 .../nifi-registry-bootstrap/pom.xml             |   40 +
 .../nifi/registry/bootstrap/BootstrapCodec.java |  108 +
 .../bootstrap/NiFiRegistryListener.java         |  141 +
 .../registry/bootstrap/RunNiFiRegistry.java     | 1246 +++
 .../nifi/registry/bootstrap/ShutdownHook.java   |   97 +
 .../exception/InvalidCommandException.java      |   38 +
 .../bootstrap/util/LimitingInputStream.java     |  107 +
 .../nifi/registry/bootstrap/util/OSUtils.java   |  107 +
 nifi-registry-core/nifi-registry-client/pom.xml |   61 +
 .../nifi/registry/client/BucketClient.java      |   76 +
 .../apache/nifi/registry/client/FlowClient.java |  119 +
 .../registry/client/FlowSnapshotClient.java     |  133 +
 .../nifi/registry/client/ItemsClient.java       |   64 +
 .../registry/client/NiFiRegistryClient.java     |   89 +
 .../client/NiFiRegistryClientConfig.java        |  257 +
 .../registry/client/NiFiRegistryException.java  |   32 +
 .../apache/nifi/registry/client/UserClient.java |   42 +
 .../client/impl/AbstractJerseyClient.java       |  120 +
 .../client/impl/BucketItemDeserializer.java     |   76 +
 .../client/impl/JerseyBucketClient.java         |  140 +
 .../registry/client/impl/JerseyFlowClient.java  |  205 +
 .../client/impl/JerseyFlowSnapshotClient.java   |  246 +
 .../registry/client/impl/JerseyItemsClient.java |   85 +
 .../client/impl/JerseyNiFiRegistryClient.java   |  247 +
 .../registry/client/impl/JerseyUserClient.java  |   47 +
 .../nifi-registry-data-model/pom.xml            |   41 +
 .../nifi/registry/RegistryConfiguration.java    |   78 +
 .../registry/authorization/AccessPolicy.java    |   72 +
 .../authorization/AccessPolicySummary.java      |   74 +
 .../registry/authorization/CurrentUser.java     |   55 +
 .../registry/authorization/Permissions.java     |  130 +
 .../nifi/registry/authorization/Resource.java   |   56 +
 .../authorization/ResourcePermissions.java      |  127 +
 .../nifi/registry/authorization/Tenant.java     |  117 +
 .../nifi/registry/authorization/User.java       |   58 +
 .../nifi/registry/authorization/UserGroup.java  |   70 +
 .../org/apache/nifi/registry/bucket/Bucket.java |  109 +
 .../apache/nifi/registry/bucket/BucketItem.java |  155 +
 .../nifi/registry/bucket/BucketItemType.java    |   27 +
 .../nifi/registry/diff/ComponentDifference.java |   77 +
 .../registry/diff/ComponentDifferenceGroup.java |   96 +
 .../registry/diff/VersionedFlowDifference.java  |   79 +
 .../org/apache/nifi/registry/field/Fields.java  |   41 +
 .../apache/nifi/registry/flow/BatchSize.java    |   76 +
 .../org/apache/nifi/registry/flow/Bundle.java   |   83 +
 .../nifi/registry/flow/ComponentType.java       |   49 +
 .../registry/flow/ConnectableComponent.java     |   95 +
 .../registry/flow/ConnectableComponentType.java |   27 +
 .../registry/flow/ControllerServiceAPI.java     |   65 +
 .../org/apache/nifi/registry/flow/PortType.java |   23 +
 .../org/apache/nifi/registry/flow/Position.java |   87 +
 .../flow/SiteToSiteTransportProtocol.java       |   23 +
 .../nifi/registry/flow/VersionedComponent.java  |  103 +
 .../flow/VersionedConfigurableComponent.java    |   34 +
 .../nifi/registry/flow/VersionedConnection.java |  177 +
 .../flow/VersionedControllerService.java        |  103 +
 .../flow/VersionedExtensionComponent.java       |   32 +
 .../nifi/registry/flow/VersionedFlow.java       |   56 +
 .../registry/flow/VersionedFlowCoordinates.java |  101 +
 .../registry/flow/VersionedFlowSnapshot.java    |  128 +
 .../flow/VersionedFlowSnapshotMetadata.java     |  130 +
 .../nifi/registry/flow/VersionedFunnel.java     |   25 +
 .../nifi/registry/flow/VersionedLabel.java      |   73 +
 .../nifi/registry/flow/VersionedPort.java       |   52 +
 .../registry/flow/VersionedProcessGroup.java    |  148 +
 .../nifi/registry/flow/VersionedProcessor.java  |  197 +
 .../flow/VersionedPropertyDescriptor.java       |   63 +
 .../registry/flow/VersionedRemoteGroupPort.java |  109 +
 .../flow/VersionedRemoteProcessGroup.java       |  161 +
 .../apache/nifi/registry/link/LinkAdapter.java  |   67 +
 .../nifi/registry/link/LinkableEntity.java      |   45 +
 .../apache/nifi/registry/params/SortOrder.java  |   47 +
 .../nifi/registry/params/SortParameter.java     |   85 +
 .../flow/TestVersionedRemoteProcessGroup.java   |  102 +
 .../dockerhub/.dockerignore                     |   19 +
 .../dockerhub/DockerBuild.sh                    |   36 +
 .../dockerhub/DockerImage.txt                   |   16 +
 .../nifi-registry-docker/dockerhub/Dockerfile   |   56 +
 .../nifi-registry-docker/dockerhub/README.md    |  148 +
 .../nifi-registry-docker/dockerhub/sh/common.sh |   28 +
 .../nifi-registry-docker/dockerhub/sh/secure.sh |   56 +
 .../nifi-registry-docker/dockerhub/sh/start.sh  |   55 +
 .../dockerhub/sh/update_database.sh             |   24 +
 .../dockerhub/sh/update_flow_provider.sh        |   42 +
 .../dockerhub/sh/update_login_providers.sh      |   47 +
 nifi-registry-core/nifi-registry-docker/pom.xml |   27 +
 nifi-registry-core/nifi-registry-docs/LICENSE   |  235 +
 nifi-registry-core/nifi-registry-docs/NOTICE    |    5 +
 nifi-registry-core/nifi-registry-docs/pom.xml   |  152 +
 .../src/main/asciidoc/administration-guide.adoc | 1212 +++
 .../src/main/asciidoc/asciidoc-mod.css          |  418 +
 .../src/main/asciidoc/getting-started.adoc      |  171 +
 .../main/asciidoc/images/ABCD_flow_changes.png  |  Bin 0 -> 119728 bytes
 .../images/ABCD_flow_in_test_bucket.png         |  Bin 0 -> 65823 bytes
 .../main/asciidoc/images/ABCD_flow_saved.png    |  Bin 0 -> 156665 bytes
 .../asciidoc/images/ABCD_process_group_menu.png |  Bin 0 -> 185393 bytes
 .../images/ABCD_save_flow_version_2.png         |  Bin 0 -> 111920 bytes
 .../src/main/asciidoc/images/ABCD_version_2.png |  Bin 0 -> 157133 bytes
 .../main/asciidoc/images/add_user_button.png    |  Bin 0 -> 59897 bytes
 .../main/asciidoc/images/add_user_dialog.png    |  Bin 0 -> 17823 bytes
 .../images/add_user_to_groups_dialog.png        |  Bin 0 -> 29156 bytes
 .../src/main/asciidoc/images/bucket_menu.png    |  Bin 0 -> 54430 bytes
 .../asciidoc/images/bucket_nav_name_edit.png    |  Bin 0 -> 61106 bytes
 .../asciidoc/images/buckets_filter_by_name.png  |  Bin 0 -> 49859 bytes
 .../asciidoc/images/buckets_sort_by_name.png    |  Bin 0 -> 59859 bytes
 .../asciidoc/images/changed_flow_options.png    |  Bin 0 -> 212864 bytes
 .../asciidoc/images/check_multiple_buckets.png  |  Bin 0 -> 62281 bytes
 .../asciidoc/images/check_multiple_users.png    |  Bin 0 -> 60431 bytes
 .../images/controller-settings-selection.png    |  Bin 0 -> 142806 bytes
 .../main/asciidoc/images/create_new_group.png   |  Bin 0 -> 63792 bytes
 .../asciidoc/images/create_new_group_dialog.png |  Bin 0 -> 21564 bytes
 .../asciidoc/images/delete_bucket_dialog.png    |  Bin 0 -> 17736 bytes
 .../asciidoc/images/delete_bucket_policy.png    |  Bin 0 -> 69265 bytes
 .../images/delete_bucket_policy_dialog.png      |  Bin 0 -> 20881 bytes
 .../asciidoc/images/delete_bucket_single.png    |  Bin 0 -> 62781 bytes
 .../asciidoc/images/delete_buckets_dialog.png   |  Bin 0 -> 19357 bytes
 .../asciidoc/images/delete_multiple_buckets.png |  Bin 0 -> 65092 bytes
 .../asciidoc/images/delete_multiple_users.png   |  Bin 0 -> 64823 bytes
 .../main/asciidoc/images/delete_user_dialog.png |  Bin 0 -> 15769 bytes
 .../main/asciidoc/images/delete_user_single.png |  Bin 0 -> 61305 bytes
 .../images/delete_users_groups_dialog.png       |  Bin 0 -> 26343 bytes
 .../main/asciidoc/images/drag_process_group.png |  Bin 0 -> 163108 bytes
 .../src/main/asciidoc/images/empty_registry.png |  Bin 0 -> 34853 bytes
 .../main/asciidoc/images/flow_change_log.png    |  Bin 0 -> 77032 bytes
 .../main/asciidoc/images/flow_delete_action.png |  Bin 0 -> 69145 bytes
 .../asciidoc/images/flow_delete_confirm.png     |  Bin 0 -> 46657 bytes
 .../src/main/asciidoc/images/flows_all.png      |  Bin 0 -> 47974 bytes
 .../asciidoc/images/flows_filter_by_name.png    |  Bin 0 -> 37004 bytes
 .../main/asciidoc/images/flows_sort_menu.png    |  Bin 0 -> 52627 bytes
 .../src/main/asciidoc/images/group_added.png    |  Bin 0 -> 86646 bytes
 .../src/main/asciidoc/images/iconDelete.png     |  Bin 0 -> 695 bytes
 .../src/main/asciidoc/images/iconHelp.png       |  Bin 0 -> 970 bytes
 .../asciidoc/images/iconLocallyModified.png     |  Bin 0 -> 1247 bytes
 .../src/main/asciidoc/images/iconManage.png     |  Bin 0 -> 748 bytes
 .../src/main/asciidoc/images/iconSettings.png   |  Bin 0 -> 887 bytes
 .../src/main/asciidoc/images/iconUpToDate.png   |  Bin 0 -> 996 bytes
 .../asciidoc/images/import_ABCD_version_2.png   |  Bin 0 -> 113797 bytes
 .../images/import_flow_from_registry.png        |  Bin 0 -> 117875 bytes
 .../src/main/asciidoc/images/local_registry.png |  Bin 0 -> 70503 bytes
 .../src/main/asciidoc/images/loginRegistry.png  |  Bin 0 -> 15560 bytes
 .../src/main/asciidoc/images/manage_bucket.png  |  Bin 0 -> 63105 bytes
 .../src/main/asciidoc/images/manage_user.png    |  Bin 0 -> 62620 bytes
 .../main/asciidoc/images/new_bucket_button.png  |  Bin 0 -> 61133 bytes
 .../main/asciidoc/images/new_bucket_dialog.png  |  Bin 0 -> 19275 bytes
 .../asciidoc/images/new_bucket_policy_added.png |  Bin 0 -> 60618 bytes
 .../images/new_bucket_policy_create.png         |  Bin 0 -> 61821 bytes
 .../new_bucket_policy_user_permission.png       |  Bin 0 -> 28727 bytes
 .../main/asciidoc/images/new_test_bucket.png    |  Bin 0 -> 54131 bytes
 .../images/nifi-registry-components.png         |  Bin 0 -> 68544 bytes
 .../images/nifi_user1_template.snagproj         |  Bin 0 -> 9758309 bytes
 .../asciidoc/images/nifi_user_template.snagproj |  Bin 0 -> 11249836 bytes
 .../asciidoc/images/remove_group_from_user.png  |  Bin 0 -> 107323 bytes
 .../asciidoc/images/remove_user_from_group.png  |  Bin 0 -> 111803 bytes
 .../asciidoc/images/save_ABCD_flow_dialog.png   |  Bin 0 -> 106088 bytes
 .../images/select_users_create_new_group.png    |  Bin 0 -> 73927 bytes
 .../select_users_create_new_group_dialog.png    |  Bin 0 -> 21597 bytes
 .../images/select_users_new_group_added.png     |  Bin 0 -> 106978 bytes
 .../src/main/asciidoc/images/test_bucket.png    |  Bin 0 -> 52937 bytes
 .../main/asciidoc/images/test_bucket_dialog.png |  Bin 0 -> 71005 bytes
 .../src/main/asciidoc/images/two_ABCD_flows.png |  Bin 0 -> 174863 bytes
 .../asciidoc/images/user_nav_add_to_group.png   |  Bin 0 -> 92238 bytes
 .../main/asciidoc/images/user_nav_name_edit.png |  Bin 0 -> 77991 bytes
 .../asciidoc/images/user_special_privileges.png |  Bin 0 -> 115491 bytes
 .../asciidoc/images/users_filter_by_name.png    |  Bin 0 -> 50482 bytes
 .../asciidoc/images/users_non_configurable.png  |  Bin 0 -> 27133 bytes
 .../main/asciidoc/images/users_sort_by_name.png |  Bin 0 -> 59613 bytes
 .../src/main/asciidoc/user-guide.adoc           |  365 +
 .../src/main/assembly/dependencies.xml          |   44 +
 .../nifi-registry-flow-diff/pom.xml             |   30 +
 .../registry/flow/diff/ComparableDataFlow.java  |   26 +
 .../ConciseEvolvingDifferenceDescriptor.java    |   85 +
 .../flow/diff/DifferenceDescriptor.java         |   36 +
 .../nifi/registry/flow/diff/DifferenceType.java |  255 +
 .../flow/diff/EvolvingDifferenceDescriptor.java |   61 +
 .../nifi/registry/flow/diff/FlowComparator.java |   35 +
 .../nifi/registry/flow/diff/FlowComparison.java |   28 +
 .../nifi/registry/flow/diff/FlowDifference.java |   38 +
 .../flow/diff/StandardComparableDataFlow.java   |   41 +
 .../flow/diff/StandardFlowComparator.java       |  404 +
 .../flow/diff/StandardFlowComparison.java       |   60 +
 .../flow/diff/StandardFlowDifference.java       |  119 +
 .../flow/diff/StaticDifferenceDescriptor.java   |   89 +
 .../nifi-registry-framework/pom.xml             |  366 +
 .../db/CustomFlywayMigrationStrategy.java       |  157 +
 .../nifi/registry/db/DataSourceFactory.java     |   97 +
 .../nifi/registry/db/DatabaseKeyService.java    |  136 +
 .../registry/db/DatabaseMetadataService.java    |  441 +
 .../nifi/registry/db/entity/BucketEntity.java   |   83 +
 .../registry/db/entity/BucketItemEntity.java    |  123 +
 .../db/entity/BucketItemEntityType.java         |   42 +
 .../nifi/registry/db/entity/FlowEntity.java     |   42 +
 .../registry/db/entity/FlowSnapshotEntity.java  |   93 +
 .../nifi/registry/db/entity/KeyEntity.java      |   51 +
 .../db/mapper/BucketEntityRowMapper.java        |   39 +
 .../db/mapper/BucketItemEntityRowMapper.java    |   58 +
 .../registry/db/mapper/FlowEntityRowMapper.java |   43 +
 .../db/mapper/FlowSnapshotEntityRowMapper.java  |   39 +
 .../registry/db/mapper/KeyEntityRowMapper.java  |   37 +
 .../registry/db/migration/BucketEntityV1.java   |   86 +
 .../registry/db/migration/FlowEntityV1.java     |  106 +
 .../db/migration/FlowSnapshotEntityV1.java      |   96 +
 .../db/migration/LegacyDataSourceFactory.java   |   81 +
 .../db/migration/LegacyDatabaseService.java     |   77 +
 .../db/migration/LegacyEntityMapper.java        |   63 +
 .../nifi/registry/event/EventFactory.java       |   97 +
 .../nifi/registry/event/EventService.java       |  115 +
 .../nifi/registry/event/StandardEvent.java      |  124 +
 .../nifi/registry/event/StandardEventField.java |   49 +
 .../exception/AdministrationException.java      |   39 +
 .../exception/ResourceNotFoundException.java    |   32 +
 .../registry/extension/ExtensionCloseable.java  |   49 +
 .../registry/extension/ExtensionManager.java    |  217 +
 .../nifi/registry/provider/ProviderFactory.java |   46 +
 .../provider/ProviderFactoryException.java      |   38 +
 .../StandardProviderConfigurationContext.java   |   39 +
 .../provider/StandardProviderFactory.java       |  217 +
 .../flow/FileSystemFlowPersistenceProvider.java |  186 +
 .../flow/StandardFlowSnapshotContext.java       |  172 +
 .../nifi/registry/provider/flow/git/Bucket.java |   87 +
 .../nifi/registry/provider/flow/git/Flow.java   |  105 +
 .../provider/flow/git/GitFlowMetaData.java      |  426 +
 .../flow/git/GitFlowPersistenceProvider.java    |  258 +
 .../provider/hook/LoggingEventHookProvider.java |   59 +
 .../provider/hook/ScriptEventHookProvider.java  |  102 +
 .../authentication/IdentityProviderFactory.java |  291 +
 ...ardIdentityProviderConfigurationContext.java |   54 +
 .../AbstractPolicyBasedAuthorizer.java          |  824 ++
 .../authorization/AuthorizableLookup.java       |   84 +
 .../AuthorizerCapabilityDetection.java          |   75 +
 .../authorization/AuthorizerFactory.java        |  915 ++
 .../AuthorizerFactoryException.java             |   33 +
 .../CompositeConfigurableUserGroupProvider.java |  242 +
 .../authorization/CompositeUserAndGroups.java   |   79 +
 .../CompositeUserGroupProvider.java             |  207 +
 .../StandardAuthorizableLookup.java             |  220 +
 .../StandardAuthorizerConfigurationContext.java |   54 +
 ...StandardAuthorizerInitializationContext.java |   52 +
 .../StandardManagedAuthorizer.java              |  264 +
 .../authorization/UsersAndAccessPolicies.java   |   52 +
 .../file/AuthorizationsHolder.java              |  187 +
 .../file/FileAccessPolicyProvider.java          |  777 ++
 .../authorization/file/FileAuthorizer.java      |  288 +
 .../file/FileUserGroupProvider.java             |  746 ++
 .../authorization/file/IdentifierUtil.java      |   35 +
 .../authorization/file/UserGroupHolder.java     |  241 +
 .../authorization/resource/Authorizable.java    |  300 +
 .../resource/InheritingAuthorizable.java        |   85 +
 .../authorization/resource/ResourceFactory.java |  235 +
 .../authorization/resource/ResourceType.java    |   87 +
 .../security/authorization/user/NiFiUser.java   |   52 +
 .../authorization/user/NiFiUserDetails.java     |   91 +
 .../authorization/user/NiFiUserUtils.java       |   91 +
 .../authorization/user/StandardNiFiUser.java    |  189 +
 .../SensitivePropertyProviderConfiguration.java |   67 +
 .../apache/nifi/registry/security/key/Key.java  |   69 +
 .../nifi/registry/security/key/KeyService.java  |   46 +
 .../security/ldap/IdentityStrategy.java         |   22 +
 .../ldap/LdapAuthenticationStrategy.java        |   24 +
 .../security/ldap/LdapIdentityProvider.java     |  355 +
 .../security/ldap/LdapsSocketFactory.java       |  106 +
 .../security/ldap/ReferralStrategy.java         |   35 +
 .../ldap/tenants/LdapUserGroupProvider.java     |  815 ++
 .../security/ldap/tenants/SearchScope.java      |   28 +
 .../security/ldap/tenants/TenantHolder.java     |  165 +
 .../nifi/registry/security/util/XmlUtils.java   |   44 +
 .../serialization/SerializationException.java   |   35 +
 .../nifi/registry/serialization/Serializer.java |   43 +
 .../VersionedProcessGroupSerializer.java        |  137 +
 .../serialization/VersionedSerializer.java      |   65 +
 .../jackson/JacksonSerializer.java              |  127 +
 .../JacksonVersionedProcessGroupSerializer.java |   33 +
 .../jackson/ObjectMapperProvider.java           |   43 +
 .../jackson/SerializationContainer.java         |   50 +
 .../serialization/jaxb/JAXBSerializer.java      |  127 +
 .../JAXBVersionedProcessGroupSerializer.java    |   30 +
 .../registry/service/AuthorizationService.java  |  811 ++
 .../nifi/registry/service/DataModelMapper.java  |  168 +
 .../nifi/registry/service/MetadataService.java  |  232 +
 .../nifi/registry/service/QueryParameters.java  |  114 +
 .../nifi/registry/service/RegistryService.java  |  994 +++
 ...e.nifi.registry.flow.FlowPersistenceProvider |   16 +
 ....apache.nifi.registry.hook.EventHookProvider |   16 +
 ...try.security.authentication.IdentityProvider |   15 +
 ....security.authorization.AccessPolicyProvider |   15 +
 ...i.registry.security.authorization.Authorizer |   16 +
 ...try.security.authorization.UserGroupProvider |   18 +
 .../main/resources/db/migration/V2__Initial.sql |   60 +
 .../db/original/V1.2__IncreaseColumnSizes.sql   |   25 +
 .../V1.3__DropBucketItemNameUniqueness.sql      |   27 +
 .../main/resources/db/original/V1__Initial.sql  |   54 +
 .../src/main/xsd/authorizations.xsd             |   87 +
 .../src/main/xsd/authorizers.xsd                |   68 +
 .../src/main/xsd/identity-providers.xsd         |   50 +
 .../src/main/xsd/providers.xsd                  |   51 +
 .../src/main/xsd/tenants.xsd                    |   96 +
 .../authorization/AuthorizerFactorySpec.groovy  |  129 +
 .../service/AuthorizationServiceSpec.groovy     |  615 ++
 .../nifi/registry/db/DatabaseBaseTest.java      |   33 +
 .../registry/db/DatabaseTestApplication.java    |   52 +
 .../registry/db/TestDatabaseKeyService.java     |   76 +
 .../db/TestDatabaseMetadataService.java         |  386 +
 .../db/migration/TestLegacyDatabaseService.java |  141 +
 .../db/migration/TestLegacyEntityMapper.java    |   79 +
 .../nifi/registry/event/TestEventFactory.java   |  169 +
 .../nifi/registry/event/TestEventService.java   |   97 +
 .../nifi/registry/event/TestStandardEvent.java  |   47 +
 .../provider/MockFlowPersistenceProvider.java   |   57 +
 .../provider/TestStandardProviderFactory.java   |   90 +
 .../TestFileSystemFlowPersistenceProvider.java  |  204 +
 .../flow/TestStandardFlowSnapshotContext.java   |   57 +
 .../git/TestGitFlowPersistenceProvider.java     |  290 +
 .../hook/TestScriptEventHookProvider.java       |   45 +
 .../ldap/tenants/LdapUserGroupProviderTest.java |  639 ++
 .../TestVersionedProcessGroupSerializer.java    |  130 +
 ...TestJAXBVersionedProcessGroupSerializer.java |   72 +
 .../registry/service/TestRegistryService.java   | 1236 +++
 .../src/test/resources/application.properties   |   25 +
 .../db/migration/V999999.1__test-setup.sql      |   70 +
 .../src/test/resources/nifi-example.ldif        |  166 +
 .../provider/hook/bad-script-provider.xml       |   30 +
 .../provider/providers-class-not-found.xml      |   24 +
 .../test/resources/provider/providers-good.xml  |   24 +
 .../authorizers-bad-ap-provider-ids.xml         |   47 +
 .../security/authorizers-bad-authorizer-ids.xml |   46 +
 .../security/authorizers-bad-composite.xml      |   54 +
 .../authorizers-bad-configurable-composite.xml  |   54 +
 .../authorizers-bad-ug-provider-ids.xml         |   46 +
 .../authorizers-good-file-providers.xml         |   39 +
 .../serialization/json/no-version.snapshot      |    5 +
 .../json/non-integer-version.snapshot           |    6 +
 .../test/resources/serialization/ver1.snapshot  |  Bin 0 -> 4421 bytes
 .../test/resources/serialization/ver2.snapshot  |   97 +
 .../test/resources/serialization/ver3.snapshot  |    6 +
 nifi-registry-core/nifi-registry-jetty/pom.xml  |   66 +
 .../apache/nifi/registry/jetty/JettyServer.java |  489 ++
 .../org/apache/nifi-registry/web/webdefault.xml |  556 ++
 .../nifi-registry-properties/pom.xml            |   73 +
 .../AESSensitivePropertyProvider.java           |  265 +
 .../AESSensitivePropertyProviderFactory.java    |   54 +
 ...pleSensitivePropertyProtectionException.java |  129 +
 .../properties/NiFiRegistryProperties.java      |  305 +
 .../NiFiRegistryPropertiesLoader.java           |  148 +
 .../ProtectedNiFiRegistryProperties.java        |  528 ++
 .../SensitivePropertyProtectionException.java   |   89 +
 .../properties/SensitivePropertyProvider.java   |   52 +
 .../SensitivePropertyProviderFactory.java       |   23 +
 .../properties/util/IdentityMapping.java        |   48 +
 .../properties/util/IdentityMappingUtil.java    |  145 +
 .../crypto/BootstrapFileCryptoKeyProvider.java  |   81 +
 .../security/crypto/CryptoKeyLoader.java        |   87 +
 .../security/crypto/CryptoKeyProvider.java      |   68 +
 .../crypto/MissingCryptoKeyException.java       |   47 +
 ...SSensitivePropertyProviderFactoryTest.groovy |   81 +
 .../AESSensitivePropertyProviderTest.groovy     |  471 ++
 .../NiFiRegistryPropertiesGroovyTest.groovy     |  121 +
 ...iFiRegistryPropertiesLoaderGroovyTest.groovy |  264 +
 .../ProtectedNiFiPropertiesGroovyTest.groovy    |  739 ++
 .../crypto/CryptoKeyLoaderGroovyTest.groovy     |  121 +
 .../src/test/resources/conf/bootstrap.conf      |   60 +
 .../bootstrap.unreadable_file_permissions.conf  |   22 +
 .../conf/bootstrap.with_missing_key.conf        |   60 +
 .../conf/bootstrap.with_missing_key_line.conf   |   60 +
 .../resources/conf/nifi-registry.properties     |   45 +
 ...ry.with_additional_sensitive_keys.properties |   55 +
 ...ive_props_fully_protected_aes_128.properties |   43 +
 ...sensitive_props_protected_aes_128.properties |   43 +
 ..._props_protected_aes_128_password.properties |   43 +
 ...sensitive_props_protected_aes_256.properties |   43 +
 ..._protected_aes_multiple_malformed.properties |   43 +
 ...ps_protected_aes_single_malformed.properties |   43 +
 ...sensitive_props_protected_unknown.properties |   43 +
 ....with_sensitive_props_unprotected.properties |   41 +
 ...tive_props_unprotected_extra_line.properties |   42 +
 .../nifi-registry-provider-api/pom.xml          |   28 +
 .../registry/flow/FlowPersistenceException.java |   31 +
 .../registry/flow/FlowPersistenceProvider.java  |   71 +
 .../nifi/registry/flow/FlowSnapshotContext.java |   64 +
 .../org/apache/nifi/registry/hook/Event.java    |   50 +
 .../apache/nifi/registry/hook/EventField.java   |   33 +
 .../nifi/registry/hook/EventFieldName.java      |   30 +
 .../nifi/registry/hook/EventHookException.java  |   31 +
 .../nifi/registry/hook/EventHookProvider.java   |   53 +
 .../apache/nifi/registry/hook/EventType.java    |   71 +
 .../WhitelistFilteringEventHookProvider.java    |   70 +
 .../apache/nifi/registry/provider/Provider.java |   32 +
 .../provider/ProviderConfigurationContext.java  |   36 +
 .../provider/ProviderCreationException.java     |   39 +
 .../nifi-registry-resources/pom.xml             |   50 +
 .../src/main/assembly/dependencies.xml          |   36 +
 .../main/resources/bin/dump-nifi-registry.bat   |   49 +
 .../src/main/resources/bin/nifi-registry-env.sh |   28 +
 .../src/main/resources/bin/nifi-registry.sh     |  357 +
 .../main/resources/bin/run-nifi-registry.bat    |   50 +
 .../main/resources/bin/status-nifi-registry.bat |   49 +
 .../src/main/resources/conf/authorizers.xml     |  256 +
 .../src/main/resources/conf/bootstrap.conf      |   48 +
 .../main/resources/conf/identity-providers.xml  |  106 +
 .../src/main/resources/conf/logback.xml         |  124 +
 .../resources/conf/nifi-registry.properties     |   80 +
 .../src/main/resources/conf/providers.xml       |   54 +
 .../nifi-registry-runtime/pom.xml               |   46 +
 .../apache/nifi/registry/BootstrapListener.java |  395 +
 .../org/apache/nifi/registry/NiFiRegistry.java  |  197 +
 .../nifi/registry/util/LimitingInputStream.java |  107 +
 .../nifi-registry-security-api/pom.xml          |   41 +
 .../authentication/AuthenticationRequest.java   |   82 +
 .../authentication/AuthenticationResponse.java  |   98 +
 .../BasicAuthIdentityProvider.java              |  100 +
 .../BearerAuthIdentityProvider.java             |   77 +
 .../authentication/IdentityProvider.java        |  157 +
 .../IdentityProviderConfigurationContext.java   |   50 +
 .../authentication/IdentityProviderLookup.java  |   23 +
 .../authentication/IdentityProviderUsage.java   |  135 +
 .../UsernamePasswordAuthenticationRequest.java  |   25 +
 .../annotation/IdentityProviderContext.java     |   31 +
 .../exception/IdentityAccessException.java      |   33 +
 .../exception/InvalidCredentialsException.java  |   33 +
 .../security/authorization/AccessPolicy.java    |  367 +
 .../authorization/AccessPolicyProvider.java     |   90 +
 ...cessPolicyProviderInitializationContext.java |   30 +
 .../AccessPolicyProviderLookup.java             |   31 +
 .../authorization/AuthorizationAuditor.java     |   30 +
 .../authorization/AuthorizationRequest.java     |  245 +
 .../authorization/AuthorizationResult.java      |  103 +
 .../security/authorization/Authorizer.java      |   63 +
 .../AuthorizerConfigurationContext.java         |   48 +
 .../AuthorizerInitializationContext.java        |   30 +
 .../authorization/AuthorizerLookup.java         |   31 +
 .../ConfigurableAccessPolicyProvider.java       |  108 +
 .../ConfigurableUserGroupProvider.java          |  163 +
 .../registry/security/authorization/Group.java  |  263 +
 .../authorization/ManagedAuthorizer.java        |   59 +
 .../security/authorization/RequestAction.java   |   56 +
 .../security/authorization/Resource.java        |   44 +
 .../registry/security/authorization/User.java   |  188 +
 .../security/authorization/UserAndGroups.java   |   55 +
 .../security/authorization/UserContextKeys.java |   26 +
 .../authorization/UserGroupProvider.java        |  108 +
 .../UserGroupProviderInitializationContext.java |   37 +
 .../authorization/UserGroupProviderLookup.java  |   31 +
 .../annotation/AuthorizerContext.java           |   35 +
 .../exception/AccessDeniedException.java        |   39 +
 .../exception/AuthorizationAccessException.java |   32 +
 .../UninheritableAuthorizationsException.java   |   28 +
 .../SecurityProviderCreationException.java      |   38 +
 .../SecurityProviderDestructionException.java   |   38 +
 .../nifi-registry-security-utils/pom.xml        |   42 +
 .../security/util/CertificateUtils.java         |  670 ++
 .../registry/security/util/CryptoUtils.java     |   75 +
 .../registry/security/util/KeyStoreUtils.java   |   82 +
 .../registry/security/util/KeystoreType.java    |   25 +
 .../security/util/ProxiedEntitiesUtils.java     |  127 +
 .../security/util/SslContextFactory.java        |  249 +
 nifi-registry-core/nifi-registry-utils/pom.xml  |   26 +
 .../org/apache/nifi/registry/util/DataUnit.java |  245 +
 .../apache/nifi/registry/util/EscapeUtils.java  |   41 +
 .../apache/nifi/registry/util/FileUtils.java    |  426 +
 .../apache/nifi/registry/util/FormatUtils.java  |  261 +
 .../nifi/registry/util/PropertyValue.java       |   91 +
 .../registry/util/StandardPropertyValue.java    |   79 +
 .../nifi/registry/util/TestFileUtils.java       |   31 +
 .../nifi-registry-web-api/pom.xml               |  359 +
 .../registry/NiFiRegistryApiApplication.java    |   85 +
 .../registry/NiFiRegistryPropertiesFactory.java |   47 +
 .../web/NiFiRegistryResourceConfig.java         |   78 +
 .../registry/web/api/AccessPolicyResource.java  |  407 +
 .../nifi/registry/web/api/AccessResource.java   |  508 ++
 .../registry/web/api/ApplicationResource.java   |  194 +
 .../api/AuthorizableApplicationResource.java    |   81 +
 .../registry/web/api/BucketFlowResource.java    |  600 ++
 .../nifi/registry/web/api/BucketResource.java   |  293 +
 .../nifi/registry/web/api/ConfigResource.java   |  108 +
 .../nifi/registry/web/api/FlowResource.java     |  315 +
 .../registry/web/api/HttpStatusMessages.java    |   30 +
 .../nifi/registry/web/api/ItemResource.java     |  171 +
 .../nifi/registry/web/api/TenantResource.java   |  579 ++
 .../web/exception/UnauthorizedException.java    |   74 +
 .../nifi/registry/web/link/LinkService.java     |  110 +
 .../web/link/builder/BucketLinkBuilder.java     |   45 +
 .../registry/web/link/builder/LinkBuilder.java  |   30 +
 .../link/builder/VersionedFlowLinkBuilder.java  |   46 +
 .../VersionedFlowSnapshotLinkBuilder.java       |   47 +
 .../web/mapper/AccessDeniedExceptionMapper.java |   75 +
 .../mapper/AdministrationExceptionMapper.java   |   46 +
 ...ationCredentialsNotFoundExceptionMapper.java |   50 +
 .../AuthorizationAccessExceptionMapper.java     |   46 +
 .../web/mapper/BadRequestExceptionMapper.java   |   49 +
 .../ConstraintViolationExceptionMapper.java     |   68 +
 .../mapper/IllegalArgumentExceptionMapper.java  |   48 +
 .../web/mapper/IllegalStateExceptionMapper.java |   46 +
 .../InvalidAuthenticationExceptionMapper.java   |   47 +
 .../web/mapper/NiFiRegistryJsonProvider.java    |   36 +
 .../web/mapper/NotAllowedExceptionMapper.java   |   46 +
 .../web/mapper/NotFoundExceptionMapper.java     |   50 +
 .../mapper/ResourceNotFoundExceptionMapper.java |   47 +
 .../mapper/SerializationExceptionMapper.java    |   47 +
 .../registry/web/mapper/ThrowableMapper.java    |   42 +
 .../web/mapper/UnauthorizedExceptionMapper.java |   56 +
 .../NiFiRegistryMasterKeyProviderFactory.java   |   67 +
 .../security/NiFiRegistrySecurityConfig.java    |  210 +
 .../web/security/PermissionsService.java        |  100 +
 .../authentication/AnonymousIdentityFilter.java |   39 +
 .../AuthenticationRequestToken.java             |  107 +
 .../AuthenticationSuccessToken.java             |   55 +
 .../IdentityAuthenticationProvider.java         |  140 +
 .../security/authentication/IdentityFilter.java |   97 +
 .../InvalidAuthenticationException.java         |   35 +
 .../exception/UntrustedProxyException.java      |   31 +
 .../authentication/jwt/JwtIdentityProvider.java |   85 +
 .../security/authentication/jwt/JwtService.java |  212 +
 .../kerberos/KerberosIdentityProvider.java      |  111 +
 .../kerberos/KerberosSpnegoFactory.java         |   67 +
 .../KerberosSpnegoIdentityProvider.java         |  184 +
 .../KerberosTicketValidatorFactory.java         |   69 +
 .../kerberos/KerberosUserDetailsService.java    |   38 +
 .../x509/SubjectDnX509PrincipalExtractor.java   |   35 +
 .../x509/X509CertificateExtractor.java          |   55 +
 .../X509IdentityAuthenticationProvider.java     |  131 +
 .../x509/X509IdentityProvider.java              |  174 +
 .../HttpMethodAuthorizationRules.java           |   48 +
 .../ResourceAuthorizationFilter.java            |  218 +
 .../StandardHttpMethodAuthorizationRules.java   |   40 +
 .../src/main/resources/META-INF/LICENSE         |  352 +
 .../src/main/resources/META-INF/NOTICE          |  184 +
 ...try.security.authentication.IdentityProvider |   15 +
 .../src/main/resources/banner.txt               |    8 +
 .../src/main/resources/images/bgNifiLogo.png    |  Bin 0 -> 3894 bytes
 .../src/main/resources/images/nifi16.ico        |  Bin 0 -> 1150 bytes
 .../resources/swagger/security-definitions.json |   12 +
 .../src/main/resources/templates/endpoint.hbs   |   61 +
 .../src/main/resources/templates/example.hbs    |   18 +
 .../src/main/resources/templates/index.html.hbs |  514 ++
 .../src/main/resources/templates/operation.hbs  |  110 +
 .../src/main/resources/templates/type.hbs       |   57 +
 .../ResourceAuthorizationFilterSpec.groovy      |  170 +
 .../NiFiRegistryTestApiApplication.java         |   40 +
 .../registry/SecureLdapTestApiApplication.java  |   45 +
 .../apache/nifi/registry/web/TestRestAPI.java   |  170 +
 .../apache/nifi/registry/web/api/BucketsIT.java |  238 +
 .../apache/nifi/registry/web/api/FlowsIT.java   |  445 +
 .../registry/web/api/IntegrationTestBase.java   |  219 +
 .../registry/web/api/IntegrationTestUtils.java  |  120 +
 .../nifi/registry/web/api/SecureFileIT.java     |  172 +
 .../web/api/SecureITClientConfiguration.java    |   91 +
 .../nifi/registry/web/api/SecureKerberosIT.java |  216 +
 .../nifi/registry/web/api/SecureLdapIT.java     |  651 ++
 .../web/api/SecureNiFiRegistryClientIT.java     |  217 +
 .../nifi/registry/web/api/UnsecuredITBase.java  |   42 +
 .../web/api/UnsecuredNiFiRegistryClientIT.java  |  403 +
 .../nifi/registry/web/link/TestLinkService.java |  124 +
 .../application-ITSecureFile.properties         |   36 +
 .../application-ITSecureKerberos.properties     |   36 +
 .../application-ITSecureLdap.properties         |   48 +
 .../application-ITUnsecured.properties          |   21 +
 .../src/test/resources/application.properties   |   25 +
 .../src/test/resources/banner.txt               |    8 +
 .../src/test/resources/conf/providers.xml       |   25 +
 .../resources/conf/secure-file/authorizers.xml  |  143 +
 .../secure-file/nifi-registry-client.properties |   25 +
 .../conf/secure-file/nifi-registry.properties   |   30 +
 .../conf/secure-kerberos/authorizers.xml        |  101 +
 .../conf/secure-kerberos/identity-providers.xml |   31 +
 .../nifi-registry-client.properties             |   22 +
 .../secure-kerberos/nifi-registry.properties    |   36 +
 .../conf/secure-ldap/authorizers.protected.xml  |  221 +
 .../resources/conf/secure-ldap/authorizers.xml  |  242 +
 .../resources/conf/secure-ldap/bootstrap.conf   |   60 +
 .../identity-providers.protected.xml            |   89 +
 .../conf/secure-ldap/identity-providers.xml     |   88 +
 .../secure-ldap/nifi-registry-client.properties |   22 +
 .../conf/secure-ldap/nifi-registry.properties   |   32 +
 .../conf/secure-ldap/test-ldap-data.ldif        |  261 +
 .../conf/unsecured/nifi-registry.properties     |   25 +
 .../src/test/resources/db/BucketsIT.sql         |   26 +
 .../src/test/resources/db/FlowsIT.sql           |   50 +
 .../src/test/resources/db/clearDB.sql           |   19 +
 .../src/test/resources/keys/README.md           |   47 +
 .../src/test/resources/keys/client-ks.jks       |  Bin 0 -> 3048 bytes
 .../src/test/resources/keys/localhost-ks.jks    |  Bin 0 -> 3077 bytes
 .../src/test/resources/keys/localhost-ts.jks    |  Bin 0 -> 911 bytes
 .../nifi-registry-web-docs/pom.xml              |   68 +
 .../web/docs/DocumentationController.java       |   57 +
 .../src/main/resources/META-INF/LICENSE         |  223 +
 .../src/main/resources/META-INF/NOTICE          |   14 +
 .../main/webapp/WEB-INF/jsp/documentation.jsp   |   84 +
 .../WEB-INF/jsp/no-documentation-found.jsp      |   31 +
 .../src/main/webapp/WEB-INF/web.xml             |   33 +
 .../src/main/webapp/css/component-usage.css     |  183 +
 .../src/main/webapp/css/main.css                |  217 +
 .../src/main/webapp/images/bgBannerFoot.png     |  Bin 0 -> 189 bytes
 .../src/main/webapp/images/bgHeader.png         |  Bin 0 -> 1455 bytes
 .../src/main/webapp/images/bgTableHeader.png    |  Bin 0 -> 232 bytes
 .../src/main/webapp/js/application.js           |  400 +
 .../src/main/webapp/js/jquery.min.js            |    4 +
 nifi-registry-core/nifi-registry-web-ui/pom.xml |  493 ++
 .../src/main/frontend/Gruntfile.js              |   78 +
 .../src/main/frontend/karma-test-shim.js        |   96 +
 .../src/main/frontend/karma.conf.js             |  176 +
 .../src/main/frontend/package-lock.json         | 7817 ++++++++++++++++++
 .../src/main/frontend/package.json              |   81 +
 .../src/main/locale/messages.es.xlf             |  130 +
 .../src/main/resources/META-INF/LICENSE         | 1152 +++
 .../src/main/resources/META-INF/NOTICE          |   23 +
 .../resources/filters/registry-min.properties   |   19 +
 .../main/resources/filters/registry.properties  |   26 +
 .../src/main/webapp/WEB-INF/pages/index.jsp     |   35 +
 .../src/main/webapp/WEB-INF/web.xml             |   54 +
 .../nf-registry-administration.html             |   40 +
 .../nf-registry-administration.js               |   98 +
 .../nf-registry-administration.spec.js          |  146 +
 .../nf-registry-add-user-to-groups.html         |   81 +
 .../nf-registry-add-user-to-groups.js           |  254 +
 .../nf-registry-add-user-to-groups.spec.js      |  172 +
 .../dialogs/add-user/nf-registry-add-user.html  |   46 +
 .../dialogs/add-user/nf-registry-add-user.js    |  107 +
 .../add-user/nf-registry-add-user.spec.js       |   86 +
 .../nf-registry-add-users-to-group.html         |   80 +
 .../nf-registry-add-users-to-group.js           |  247 +
 .../nf-registry-add-users-to-group.spec.js      |  170 +
 .../nf-registry-create-new-group.html           |   46 +
 .../nf-registry-create-new-group.js             |  108 +
 .../nf-registry-create-new-group.spec.js        |   85 +
 .../users/nf-registry-users-administration.html |  178 +
 .../users/nf-registry-users-administration.js   |  150 +
 .../nf-registry-users-adminstration.spec.js     |  191 +
 .../manage-group/nf-registry-manage-group.html  |  204 +
 .../manage-group/nf-registry-manage-group.js    |  609 ++
 .../nf-registry-manage-group.spec.js            | 2517 ++++++
 .../manage-user/nf-registry-manage-user.html    |  211 +
 .../manage-user/nf-registry-manage-user.js      |  633 ++
 .../manage-user/nf-registry-manage-user.spec.js | 1877 +++++
 .../nf-registry-add-policy-to-bucket.html       |   95 +
 .../nf-registry-add-policy-to-bucket.js         |  413 +
 .../nf-registry-create-bucket.html              |   46 +
 .../create-bucket/nf-registry-create-bucket.js  |  106 +
 .../nf-registry-create-bucket.spec.js           |   81 +
 .../nf-registry-edit-bucket-policy.html         |   55 +
 .../nf-registry-edit-bucket-policy.js           |  352 +
 .../nf-registry-workflow-administration.html    |  123 +
 .../nf-registry-workflow-administration.js      |  104 +
 .../nf-registry-workflow-administration.spec.js |  169 +
 .../nf-registry-manage-bucket.html              |  129 +
 .../manage-bucket/nf-registry-manage-bucket.js  |  462 ++
 .../nf-registry-manage-bucket.spec.js           |  810 ++
 .../nf-registry-bucket-grid-list-viewer.js      |  123 +
 .../nf-registry-bucket-grid-list-viewer.spec.js |  242 +
 .../nf-registry-droplet-grid-list-viewer.js     |  123 +
 ...nf-registry-droplet-grid-list-viewer.spec.js |  291 +
 .../registry/nf-registry-grid-list-viewer.html  |  124 +
 .../registry/nf-registry-grid-list-viewer.js    |   98 +
 .../nf-registry-grid-list-viewer.spec.js        |  195 +
 .../explorer/nf-registry-explorer.html          |   18 +
 .../components/explorer/nf-registry-explorer.js |   70 +
 .../explorer/nf-registry-explorer.spec.js       |  119 +
 .../login/dialogs/nf-registry-user-login.html   |   45 +
 .../login/dialogs/nf-registry-user-login.js     |   75 +
 .../components/login/nf-registry-login.html     |   19 +
 .../components/login/nf-registry-login.js       |   64 +
 .../nf-registry-page-not-found.html             |   19 +
 .../nf-registry-page-not-found.js               |   75 +
 .../webapp/images/registry-background-logo.svg  |   17 +
 .../src/main/webapp/images/registry-favicon.png |  Bin 0 -> 388 bytes
 .../webapp/images/registry-logo-web-app.svg     |   17 +
 .../src/main/webapp/nf-registry-bootstrap.js    |   59 +
 .../src/main/webapp/nf-registry.animations.js   |  133 +
 .../src/main/webapp/nf-registry.e2e-spec.js     |   30 +
 .../src/main/webapp/nf-registry.html            |   94 +
 .../src/main/webapp/nf-registry.js              |  102 +
 .../src/main/webapp/nf-registry.module.js       |  116 +
 .../src/main/webapp/nf-registry.routes.js       |  108 +
 .../src/main/webapp/nf-registry.spec.js         |  105 +
 .../src/main/webapp/services/nf-registry.api.js |  786 ++
 .../webapp/services/nf-registry.api.spec.js     | 1374 +++
 .../services/nf-registry.auth-guard.service.js  |  372 +
 .../nf-registry.auth-guard.service.spec.js      |  629 ++
 .../main/webapp/services/nf-registry.service.js | 1175 +++
 .../webapp/services/nf-registry.service.spec.js | 1148 +++
 .../services/nf-registry.token.interceptor.js   |   53 +
 .../main/webapp/services/nf-storage.service.js  |  219 +
 .../src/main/webapp/systemjs-angular-loader.js  |   49 +
 .../src/main/webapp/systemjs.builder.config.js  |  176 +
 .../src/main/webapp/systemjs.spec.config.js     |  145 +
 .../src/main/webapp/theming/_helperClasses.scss |   82 +
 .../main/webapp/theming/_structureElements.scss |  160 +
 .../administration/_structureElements.scss      |   22 +
 .../users/_structureElements.scss               |   72 +
 .../workflow/_structureElements.scss            |   49 +
 .../explorer/grid-list/_structureElements.scss  |   56 +
 .../src/main/webapp/theming/nf-registry.scss    |   52 +
 nifi-registry-core/pom.xml                      |  146 +
 nifi-registry-data-model/pom.xml                |   41 -
 .../nifi/registry/RegistryConfiguration.java    |   78 -
 .../registry/authorization/AccessPolicy.java    |   72 -
 .../authorization/AccessPolicySummary.java      |   74 -
 .../registry/authorization/CurrentUser.java     |   55 -
 .../registry/authorization/Permissions.java     |  130 -
 .../nifi/registry/authorization/Resource.java   |   56 -
 .../authorization/ResourcePermissions.java      |  127 -
 .../nifi/registry/authorization/Tenant.java     |  117 -
 .../nifi/registry/authorization/User.java       |   58 -
 .../nifi/registry/authorization/UserGroup.java  |   70 -
 .../org/apache/nifi/registry/bucket/Bucket.java |  109 -
 .../apache/nifi/registry/bucket/BucketItem.java |  155 -
 .../nifi/registry/bucket/BucketItemType.java    |   27 -
 .../nifi/registry/diff/ComponentDifference.java |   77 -
 .../registry/diff/ComponentDifferenceGroup.java |   96 -
 .../registry/diff/VersionedFlowDifference.java  |   79 -
 .../org/apache/nifi/registry/field/Fields.java  |   41 -
 .../apache/nifi/registry/flow/BatchSize.java    |   76 -
 .../org/apache/nifi/registry/flow/Bundle.java   |   83 -
 .../nifi/registry/flow/ComponentType.java       |   49 -
 .../registry/flow/ConnectableComponent.java     |   95 -
 .../registry/flow/ConnectableComponentType.java |   27 -
 .../registry/flow/ControllerServiceAPI.java     |   65 -
 .../org/apache/nifi/registry/flow/PortType.java |   23 -
 .../org/apache/nifi/registry/flow/Position.java |   87 -
 .../flow/SiteToSiteTransportProtocol.java       |   23 -
 .../nifi/registry/flow/VersionedComponent.java  |  103 -
 .../flow/VersionedConfigurableComponent.java    |   34 -
 .../nifi/registry/flow/VersionedConnection.java |  177 -
 .../flow/VersionedControllerService.java        |  103 -
 .../flow/VersionedExtensionComponent.java       |   32 -
 .../nifi/registry/flow/VersionedFlow.java       |   56 -
 .../registry/flow/VersionedFlowCoordinates.java |  101 -
 .../registry/flow/VersionedFlowSnapshot.java    |  128 -
 .../flow/VersionedFlowSnapshotMetadata.java     |  130 -
 .../nifi/registry/flow/VersionedFunnel.java     |   25 -
 .../nifi/registry/flow/VersionedLabel.java      |   73 -
 .../nifi/registry/flow/VersionedPort.java       |   52 -
 .../registry/flow/VersionedProcessGroup.java    |  148 -
 .../nifi/registry/flow/VersionedProcessor.java  |  197 -
 .../flow/VersionedPropertyDescriptor.java       |   63 -
 .../registry/flow/VersionedRemoteGroupPort.java |  109 -
 .../flow/VersionedRemoteProcessGroup.java       |  161 -
 .../apache/nifi/registry/link/LinkAdapter.java  |   67 -
 .../nifi/registry/link/LinkableEntity.java      |   45 -
 .../apache/nifi/registry/params/SortOrder.java  |   47 -
 .../nifi/registry/params/SortParameter.java     |   85 -
 .../flow/TestVersionedRemoteProcessGroup.java   |  102 -
 nifi-registry-docker/dockerhub/.dockerignore    |   19 -
 nifi-registry-docker/dockerhub/DockerBuild.sh   |   36 -
 nifi-registry-docker/dockerhub/DockerImage.txt  |   16 -
 nifi-registry-docker/dockerhub/Dockerfile       |   56 -
 nifi-registry-docker/dockerhub/README.md        |  148 -
 nifi-registry-docker/dockerhub/sh/common.sh     |   28 -
 nifi-registry-docker/dockerhub/sh/secure.sh     |   56 -
 nifi-registry-docker/dockerhub/sh/start.sh      |   55 -
 .../dockerhub/sh/update_database.sh             |   24 -
 .../dockerhub/sh/update_flow_provider.sh        |   42 -
 .../dockerhub/sh/update_login_providers.sh      |   47 -
 nifi-registry-docker/pom.xml                    |   27 -
 nifi-registry-docs/LICENSE                      |  235 -
 nifi-registry-docs/NOTICE                       |    5 -
 nifi-registry-docs/pom.xml                      |  152 -
 .../src/main/asciidoc/administration-guide.adoc | 1212 ---
 .../src/main/asciidoc/asciidoc-mod.css          |  418 -
 .../src/main/asciidoc/getting-started.adoc      |  171 -
 .../main/asciidoc/images/ABCD_flow_changes.png  |  Bin 119728 -> 0 bytes
 .../images/ABCD_flow_in_test_bucket.png         |  Bin 65823 -> 0 bytes
 .../main/asciidoc/images/ABCD_flow_saved.png    |  Bin 156665 -> 0 bytes
 .../asciidoc/images/ABCD_process_group_menu.png |  Bin 185393 -> 0 bytes
 .../images/ABCD_save_flow_version_2.png         |  Bin 111920 -> 0 bytes
 .../src/main/asciidoc/images/ABCD_version_2.png |  Bin 157133 -> 0 bytes
 .../main/asciidoc/images/add_user_button.png    |  Bin 59897 -> 0 bytes
 .../main/asciidoc/images/add_user_dialog.png    |  Bin 17823 -> 0 bytes
 .../images/add_user_to_groups_dialog.png        |  Bin 29156 -> 0 bytes
 .../src/main/asciidoc/images/bucket_menu.png    |  Bin 54430 -> 0 bytes
 .../asciidoc/images/bucket_nav_name_edit.png    |  Bin 61106 -> 0 bytes
 .../asciidoc/images/buckets_filter_by_name.png  |  Bin 49859 -> 0 bytes
 .../asciidoc/images/buckets_sort_by_name.png    |  Bin 59859 -> 0 bytes
 .../asciidoc/images/changed_flow_options.png    |  Bin 212864 -> 0 bytes
 .../asciidoc/images/check_multiple_buckets.png  |  Bin 62281 -> 0 bytes
 .../asciidoc/images/check_multiple_users.png    |  Bin 60431 -> 0 bytes
 .../images/controller-settings-selection.png    |  Bin 142806 -> 0 bytes
 .../main/asciidoc/images/create_new_group.png   |  Bin 63792 -> 0 bytes
 .../asciidoc/images/create_new_group_dialog.png |  Bin 21564 -> 0 bytes
 .../asciidoc/images/delete_bucket_dialog.png    |  Bin 17736 -> 0 bytes
 .../asciidoc/images/delete_bucket_policy.png    |  Bin 69265 -> 0 bytes
 .../images/delete_bucket_policy_dialog.png      |  Bin 20881 -> 0 bytes
 .../asciidoc/images/delete_bucket_single.png    |  Bin 62781 -> 0 bytes
 .../asciidoc/images/delete_buckets_dialog.png   |  Bin 19357 -> 0 bytes
 .../asciidoc/images/delete_multiple_buckets.png |  Bin 65092 -> 0 bytes
 .../asciidoc/images/delete_multiple_users.png   |  Bin 64823 -> 0 bytes
 .../main/asciidoc/images/delete_user_dialog.png |  Bin 15769 -> 0 bytes
 .../main/asciidoc/images/delete_user_single.png |  Bin 61305 -> 0 bytes
 .../images/delete_users_groups_dialog.png       |  Bin 26343 -> 0 bytes
 .../main/asciidoc/images/drag_process_group.png |  Bin 163108 -> 0 bytes
 .../src/main/asciidoc/images/empty_registry.png |  Bin 34853 -> 0 bytes
 .../main/asciidoc/images/flow_change_log.png    |  Bin 77032 -> 0 bytes
 .../main/asciidoc/images/flow_delete_action.png |  Bin 69145 -> 0 bytes
 .../asciidoc/images/flow_delete_confirm.png     |  Bin 46657 -> 0 bytes
 .../src/main/asciidoc/images/flows_all.png      |  Bin 47974 -> 0 bytes
 .../asciidoc/images/flows_filter_by_name.png    |  Bin 37004 -> 0 bytes
 .../main/asciidoc/images/flows_sort_menu.png    |  Bin 52627 -> 0 bytes
 .../src/main/asciidoc/images/group_added.png    |  Bin 86646 -> 0 bytes
 .../src/main/asciidoc/images/iconDelete.png     |  Bin 695 -> 0 bytes
 .../src/main/asciidoc/images/iconHelp.png       |  Bin 970 -> 0 bytes
 .../asciidoc/images/iconLocallyModified.png     |  Bin 1247 -> 0 bytes
 .../src/main/asciidoc/images/iconManage.png     |  Bin 748 -> 0 bytes
 .../src/main/asciidoc/images/iconSettings.png   |  Bin 887 -> 0 bytes
 .../src/main/asciidoc/images/iconUpToDate.png   |  Bin 996 -> 0 bytes
 .../asciidoc/images/import_ABCD_version_2.png   |  Bin 113797 -> 0 bytes
 .../images/import_flow_from_registry.png        |  Bin 117875 -> 0 bytes
 .../src/main/asciidoc/images/local_registry.png |  Bin 70503 -> 0 bytes
 .../src/main/asciidoc/images/loginRegistry.png  |  Bin 15560 -> 0 bytes
 .../src/main/asciidoc/images/manage_bucket.png  |  Bin 63105 -> 0 bytes
 .../src/main/asciidoc/images/manage_user.png    |  Bin 62620 -> 0 bytes
 .../main/asciidoc/images/new_bucket_button.png  |  Bin 61133 -> 0 bytes
 .../main/asciidoc/images/new_bucket_dialog.png  |  Bin 19275 -> 0 bytes
 .../asciidoc/images/new_bucket_policy_added.png |  Bin 60618 -> 0 bytes
 .../images/new_bucket_policy_create.png         |  Bin 61821 -> 0 bytes
 .../new_bucket_policy_user_permission.png       |  Bin 28727 -> 0 bytes
 .../main/asciidoc/images/new_test_bucket.png    |  Bin 54131 -> 0 bytes
 .../images/nifi-registry-components.png         |  Bin 68544 -> 0 bytes
 .../images/nifi_user1_template.snagproj         |  Bin 9758309 -> 0 bytes
 .../asciidoc/images/nifi_user_template.snagproj |  Bin 11249836 -> 0 bytes
 .../asciidoc/images/remove_group_from_user.png  |  Bin 107323 -> 0 bytes
 .../asciidoc/images/remove_user_from_group.png  |  Bin 111803 -> 0 bytes
 .../asciidoc/images/save_ABCD_flow_dialog.png   |  Bin 106088 -> 0 bytes
 .../images/select_users_create_new_group.png    |  Bin 73927 -> 0 bytes
 .../select_users_create_new_group_dialog.png    |  Bin 21597 -> 0 bytes
 .../images/select_users_new_group_added.png     |  Bin 106978 -> 0 bytes
 .../src/main/asciidoc/images/test_bucket.png    |  Bin 52937 -> 0 bytes
 .../main/asciidoc/images/test_bucket_dialog.png |  Bin 71005 -> 0 bytes
 .../src/main/asciidoc/images/two_ABCD_flows.png |  Bin 174863 -> 0 bytes
 .../asciidoc/images/user_nav_add_to_group.png   |  Bin 92238 -> 0 bytes
 .../main/asciidoc/images/user_nav_name_edit.png |  Bin 77991 -> 0 bytes
 .../asciidoc/images/user_special_privileges.png |  Bin 115491 -> 0 bytes
 .../asciidoc/images/users_filter_by_name.png    |  Bin 50482 -> 0 bytes
 .../asciidoc/images/users_non_configurable.png  |  Bin 27133 -> 0 bytes
 .../main/asciidoc/images/users_sort_by_name.png |  Bin 59613 -> 0 bytes
 .../src/main/asciidoc/user-guide.adoc           |  365 -
 .../src/main/assembly/dependencies.xml          |   44 -
 .../nifi-registry-ranger-plugin/pom.xml         |    2 +-
 nifi-registry-flow-diff/pom.xml                 |   30 -
 .../registry/flow/diff/ComparableDataFlow.java  |   26 -
 .../ConciseEvolvingDifferenceDescriptor.java    |   85 -
 .../flow/diff/DifferenceDescriptor.java         |   36 -
 .../nifi/registry/flow/diff/DifferenceType.java |  255 -
 .../flow/diff/EvolvingDifferenceDescriptor.java |   61 -
 .../nifi/registry/flow/diff/FlowComparator.java |   35 -
 .../nifi/registry/flow/diff/FlowComparison.java |   28 -
 .../nifi/registry/flow/diff/FlowDifference.java |   38 -
 .../flow/diff/StandardComparableDataFlow.java   |   41 -
 .../flow/diff/StandardFlowComparator.java       |  404 -
 .../flow/diff/StandardFlowComparison.java       |   60 -
 .../flow/diff/StandardFlowDifference.java       |  119 -
 .../flow/diff/StaticDifferenceDescriptor.java   |   89 -
 nifi-registry-framework/pom.xml                 |  366 -
 .../db/CustomFlywayMigrationStrategy.java       |  157 -
 .../nifi/registry/db/DataSourceFactory.java     |   97 -
 .../nifi/registry/db/DatabaseKeyService.java    |  136 -
 .../registry/db/DatabaseMetadataService.java    |  441 -
 .../nifi/registry/db/entity/BucketEntity.java   |   83 -
 .../registry/db/entity/BucketItemEntity.java    |  123 -
 .../db/entity/BucketItemEntityType.java         |   42 -
 .../nifi/registry/db/entity/FlowEntity.java     |   42 -
 .../registry/db/entity/FlowSnapshotEntity.java  |   93 -
 .../nifi/registry/db/entity/KeyEntity.java      |   51 -
 .../db/mapper/BucketEntityRowMapper.java        |   39 -
 .../db/mapper/BucketItemEntityRowMapper.java    |   58 -
 .../registry/db/mapper/FlowEntityRowMapper.java |   43 -
 .../db/mapper/FlowSnapshotEntityRowMapper.java  |   39 -
 .../registry/db/mapper/KeyEntityRowMapper.java  |   37 -
 .../registry/db/migration/BucketEntityV1.java   |   86 -
 .../registry/db/migration/FlowEntityV1.java     |  106 -
 .../db/migration/FlowSnapshotEntityV1.java      |   96 -
 .../db/migration/LegacyDataSourceFactory.java   |   81 -
 .../db/migration/LegacyDatabaseService.java     |   77 -
 .../db/migration/LegacyEntityMapper.java        |   63 -
 .../nifi/registry/event/EventFactory.java       |   97 -
 .../nifi/registry/event/EventService.java       |  115 -
 .../nifi/registry/event/StandardEvent.java      |  124 -
 .../nifi/registry/event/StandardEventField.java |   49 -
 .../exception/AdministrationException.java      |   39 -
 .../exception/ResourceNotFoundException.java    |   32 -
 .../registry/extension/ExtensionCloseable.java  |   49 -
 .../registry/extension/ExtensionManager.java    |  217 -
 .../nifi/registry/provider/ProviderFactory.java |   46 -
 .../provider/ProviderFactoryException.java      |   38 -
 .../StandardProviderConfigurationContext.java   |   39 -
 .../provider/StandardProviderFactory.java       |  217 -
 .../flow/FileSystemFlowPersistenceProvider.java |  186 -
 .../flow/StandardFlowSnapshotContext.java       |  172 -
 .../nifi/registry/provider/flow/git/Bucket.java |   87 -
 .../nifi/registry/provider/flow/git/Flow.java   |  105 -
 .../provider/flow/git/GitFlowMetaData.java      |  426 -
 .../flow/git/GitFlowPersistenceProvider.java    |  258 -
 .../provider/hook/LoggingEventHookProvider.java |   59 -
 .../provider/hook/ScriptEventHookProvider.java  |  102 -
 .../authentication/IdentityProviderFactory.java |  291 -
 ...ardIdentityProviderConfigurationContext.java |   54 -
 .../AbstractPolicyBasedAuthorizer.java          |  824 --
 .../authorization/AuthorizableLookup.java       |   84 -
 .../AuthorizerCapabilityDetection.java          |   75 -
 .../authorization/AuthorizerFactory.java        |  915 --
 .../AuthorizerFactoryException.java             |   33 -
 .../CompositeConfigurableUserGroupProvider.java |  242 -
 .../authorization/CompositeUserAndGroups.java   |   79 -
 .../CompositeUserGroupProvider.java             |  207 -
 .../StandardAuthorizableLookup.java             |  220 -
 .../StandardAuthorizerConfigurationContext.java |   54 -
 ...StandardAuthorizerInitializationContext.java |   52 -
 .../StandardManagedAuthorizer.java              |  264 -
 .../authorization/UsersAndAccessPolicies.java   |   52 -
 .../file/AuthorizationsHolder.java              |  187 -
 .../file/FileAccessPolicyProvider.java          |  777 --
 .../authorization/file/FileAuthorizer.java      |  288 -
 .../file/FileUserGroupProvider.java             |  746 --
 .../authorization/file/IdentifierUtil.java      |   35 -
 .../authorization/file/UserGroupHolder.java     |  241 -
 .../authorization/resource/Authorizable.java    |  300 -
 .../resource/InheritingAuthorizable.java        |   85 -
 .../authorization/resource/ResourceFactory.java |  235 -
 .../authorization/resource/ResourceType.java    |   87 -
 .../security/authorization/user/NiFiUser.java   |   52 -
 .../authorization/user/NiFiUserDetails.java     |   91 -
 .../authorization/user/NiFiUserUtils.java       |   91 -
 .../authorization/user/StandardNiFiUser.java    |  189 -
 .../SensitivePropertyProviderConfiguration.java |   67 -
 .../apache/nifi/registry/security/key/Key.java  |   69 -
 .../nifi/registry/security/key/KeyService.java  |   46 -
 .../security/ldap/IdentityStrategy.java         |   22 -
 .../ldap/LdapAuthenticationStrategy.java        |   24 -
 .../security/ldap/LdapIdentityProvider.java     |  355 -
 .../security/ldap/LdapsSocketFactory.java       |  106 -
 .../security/ldap/ReferralStrategy.java         |   35 -
 .../ldap/tenants/LdapUserGroupProvider.java     |  815 --
 .../security/ldap/tenants/SearchScope.java      |   28 -
 .../security/ldap/tenants/TenantHolder.java     |  165 -
 .../nifi/registry/security/util/XmlUtils.java   |   44 -
 .../serialization/SerializationException.java   |   35 -
 .../nifi/registry/serialization/Serializer.java |   43 -
 .../VersionedProcessGroupSerializer.java        |  137 -
 .../serialization/VersionedSerializer.java      |   65 -
 .../jackson/JacksonSerializer.java              |  127 -
 .../JacksonVersionedProcessGroupSerializer.java |   33 -
 .../jackson/ObjectMapperProvider.java           |   43 -
 .../jackson/SerializationContainer.java         |   50 -
 .../serialization/jaxb/JAXBSerializer.java      |  127 -
 .../JAXBVersionedProcessGroupSerializer.java    |   30 -
 .../registry/service/AuthorizationService.java  |  811 --
 .../nifi/registry/service/DataModelMapper.java  |  168 -
 .../nifi/registry/service/MetadataService.java  |  232 -
 .../nifi/registry/service/QueryParameters.java  |  114 -
 .../nifi/registry/service/RegistryService.java  |  994 ---
 ...e.nifi.registry.flow.FlowPersistenceProvider |   16 -
 ....apache.nifi.registry.hook.EventHookProvider |   16 -
 ...try.security.authentication.IdentityProvider |   15 -
 ....security.authorization.AccessPolicyProvider |   15 -
 ...i.registry.security.authorization.Authorizer |   16 -
 ...try.security.authorization.UserGroupProvider |   18 -
 .../main/resources/db/migration/V2__Initial.sql |   60 -
 .../db/original/V1.2__IncreaseColumnSizes.sql   |   25 -
 .../V1.3__DropBucketItemNameUniqueness.sql      |   27 -
 .../main/resources/db/original/V1__Initial.sql  |   54 -
 .../src/main/xsd/authorizations.xsd             |   87 -
 .../src/main/xsd/authorizers.xsd                |   68 -
 .../src/main/xsd/identity-providers.xsd         |   50 -
 .../src/main/xsd/providers.xsd                  |   51 -
 .../src/main/xsd/tenants.xsd                    |   96 -
 .../authorization/AuthorizerFactorySpec.groovy  |  129 -
 .../service/AuthorizationServiceSpec.groovy     |  615 --
 .../nifi/registry/db/DatabaseBaseTest.java      |   33 -
 .../registry/db/DatabaseTestApplication.java    |   52 -
 .../registry/db/TestDatabaseKeyService.java     |   76 -
 .../db/TestDatabaseMetadataService.java         |  386 -
 .../db/migration/TestLegacyDatabaseService.java |  141 -
 .../db/migration/TestLegacyEntityMapper.java    |   79 -
 .../nifi/registry/event/TestEventFactory.java   |  169 -
 .../nifi/registry/event/TestEventService.java   |   97 -
 .../nifi/registry/event/TestStandardEvent.java  |   47 -
 .../provider/MockFlowPersistenceProvider.java   |   57 -
 .../provider/TestStandardProviderFactory.java   |   90 -
 .../TestFileSystemFlowPersistenceProvider.java  |  204 -
 .../flow/TestStandardFlowSnapshotContext.java   |   57 -
 .../git/TestGitFlowPersistenceProvider.java     |  290 -
 .../hook/TestScriptEventHookProvider.java       |   45 -
 .../ldap/tenants/LdapUserGroupProviderTest.java |  639 --
 .../TestVersionedProcessGroupSerializer.java    |  130 -
 ...TestJAXBVersionedProcessGroupSerializer.java |   72 -
 .../registry/service/TestRegistryService.java   | 1236 ---
 .../src/test/resources/application.properties   |   25 -
 .../db/migration/V999999.1__test-setup.sql      |   70 -
 .../src/test/resources/nifi-example.ldif        |  166 -
 .../provider/hook/bad-script-provider.xml       |   30 -
 .../provider/providers-class-not-found.xml      |   24 -
 .../test/resources/provider/providers-good.xml  |   24 -
 .../authorizers-bad-ap-provider-ids.xml         |   47 -
 .../security/authorizers-bad-authorizer-ids.xml |   46 -
 .../security/authorizers-bad-composite.xml      |   54 -
 .../authorizers-bad-configurable-composite.xml  |   54 -
 .../authorizers-bad-ug-provider-ids.xml         |   46 -
 .../authorizers-good-file-providers.xml         |   39 -
 .../serialization/json/no-version.snapshot      |    5 -
 .../json/non-integer-version.snapshot           |    6 -
 .../test/resources/serialization/ver1.snapshot  |  Bin 4421 -> 0 bytes
 .../test/resources/serialization/ver2.snapshot  |   97 -
 .../test/resources/serialization/ver3.snapshot  |    6 -
 nifi-registry-jetty/pom.xml                     |   66 -
 .../apache/nifi/registry/jetty/JettyServer.java |  489 --
 .../org/apache/nifi-registry/web/webdefault.xml |  556 --
 nifi-registry-properties/pom.xml                |   73 -
 .../AESSensitivePropertyProvider.java           |  265 -
 .../AESSensitivePropertyProviderFactory.java    |   54 -
 ...pleSensitivePropertyProtectionException.java |  129 -
 .../properties/NiFiRegistryProperties.java      |  305 -
 .../NiFiRegistryPropertiesLoader.java           |  148 -
 .../ProtectedNiFiRegistryProperties.java        |  528 --
 .../SensitivePropertyProtectionException.java   |   89 -
 .../properties/SensitivePropertyProvider.java   |   52 -
 .../SensitivePropertyProviderFactory.java       |   23 -
 .../properties/util/IdentityMapping.java        |   48 -
 .../properties/util/IdentityMappingUtil.java    |  145 -
 .../crypto/BootstrapFileCryptoKeyProvider.java  |   81 -
 .../security/crypto/CryptoKeyLoader.java        |   87 -
 .../security/crypto/CryptoKeyProvider.java      |   68 -
 .../crypto/MissingCryptoKeyException.java       |   47 -
 ...SSensitivePropertyProviderFactoryTest.groovy |   81 -
 .../AESSensitivePropertyProviderTest.groovy     |  471 --
 .../NiFiRegistryPropertiesGroovyTest.groovy     |  121 -
 ...iFiRegistryPropertiesLoaderGroovyTest.groovy |  264 -
 .../ProtectedNiFiPropertiesGroovyTest.groovy    |  739 --
 .../crypto/CryptoKeyLoaderGroovyTest.groovy     |  121 -
 .../src/test/resources/conf/bootstrap.conf      |   60 -
 .../bootstrap.unreadable_file_permissions.conf  |   22 -
 .../conf/bootstrap.with_missing_key.conf        |   60 -
 .../conf/bootstrap.with_missing_key_line.conf   |   60 -
 .../resources/conf/nifi-registry.properties     |   45 -
 ...ry.with_additional_sensitive_keys.properties |   55 -
 ...ive_props_fully_protected_aes_128.properties |   43 -
 ...sensitive_props_protected_aes_128.properties |   43 -
 ..._props_protected_aes_128_password.properties |   43 -
 ...sensitive_props_protected_aes_256.properties |   43 -
 ..._protected_aes_multiple_malformed.properties |   43 -
 ...ps_protected_aes_single_malformed.properties |   43 -
 ...sensitive_props_protected_unknown.properties |   43 -
 ....with_sensitive_props_unprotected.properties |   41 -
 ...tive_props_unprotected_extra_line.properties |   42 -
 nifi-registry-provider-api/pom.xml              |   28 -
 .../registry/flow/FlowPersistenceException.java |   31 -
 .../registry/flow/FlowPersistenceProvider.java  |   71 -
 .../nifi/registry/flow/FlowSnapshotContext.java |   64 -
 .../org/apache/nifi/registry/hook/Event.java    |   50 -
 .../apache/nifi/registry/hook/EventField.java   |   33 -
 .../nifi/registry/hook/EventFieldName.java      |   30 -
 .../nifi/registry/hook/EventHookException.java  |   31 -
 .../nifi/registry/hook/EventHookProvider.java   |   53 -
 .../apache/nifi/registry/hook/EventType.java    |   71 -
 .../WhitelistFilteringEventHookProvider.java    |   70 -
 .../apache/nifi/registry/provider/Provider.java |   32 -
 .../provider/ProviderConfigurationContext.java  |   36 -
 .../provider/ProviderCreationException.java     |   39 -
 nifi-registry-resources/pom.xml                 |   50 -
 .../src/main/assembly/dependencies.xml          |   36 -
 .../main/resources/bin/dump-nifi-registry.bat   |   49 -
 .../src/main/resources/bin/nifi-registry-env.sh |   28 -
 .../src/main/resources/bin/nifi-registry.sh     |  357 -
 .../main/resources/bin/run-nifi-registry.bat    |   50 -
 .../main/resources/bin/status-nifi-registry.bat |   49 -
 .../src/main/resources/conf/authorizers.xml     |  256 -
 .../src/main/resources/conf/bootstrap.conf      |   48 -
 .../main/resources/conf/identity-providers.xml  |  106 -
 .../src/main/resources/conf/logback.xml         |  124 -
 .../resources/conf/nifi-registry.properties     |   80 -
 .../src/main/resources/conf/providers.xml       |   54 -
 nifi-registry-runtime/pom.xml                   |   46 -
 .../apache/nifi/registry/BootstrapListener.java |  395 -
 .../org/apache/nifi/registry/NiFiRegistry.java  |  197 -
 .../nifi/registry/util/LimitingInputStream.java |  107 -
 nifi-registry-security-api/pom.xml              |   41 -
 .../authentication/AuthenticationRequest.java   |   82 -
 .../authentication/AuthenticationResponse.java  |   98 -
 .../BasicAuthIdentityProvider.java              |  100 -
 .../BearerAuthIdentityProvider.java             |   77 -
 .../authentication/IdentityProvider.java        |  157 -
 .../IdentityProviderConfigurationContext.java   |   50 -
 .../authentication/IdentityProviderLookup.java  |   23 -
 .../authentication/IdentityProviderUsage.java   |  135 -
 .../UsernamePasswordAuthenticationRequest.java  |   25 -
 .../annotation/IdentityProviderContext.java     |   31 -
 .../exception/IdentityAccessException.java      |   33 -
 .../exception/InvalidCredentialsException.java  |   33 -
 .../security/authorization/AccessPolicy.java    |  367 -
 .../authorization/AccessPolicyProvider.java     |   90 -
 ...cessPolicyProviderInitializationContext.java |   30 -
 .../AccessPolicyProviderLookup.java             |   31 -
 .../authorization/AuthorizationAuditor.java     |   30 -
 .../authorization/AuthorizationRequest.java     |  245 -
 .../authorization/AuthorizationResult.java      |  103 -
 .../security/authorization/Authorizer.java      |   63 -
 .../AuthorizerConfigurationContext.java         |   48 -
 .../AuthorizerInitializationContext.java        |   30 -
 .../authorization/AuthorizerLookup.java         |   31 -
 .../ConfigurableAccessPolicyProvider.java       |  108 -
 .../ConfigurableUserGroupProvider.java          |  163 -
 .../registry/security/authorization/Group.java  |  263 -
 .../authorization/ManagedAuthorizer.java        |   59 -
 .../security/authorization/RequestAction.java   |   56 -
 .../security/authorization/Resource.java        |   44 -
 .../registry/security/authorization/User.java   |  188 -
 .../security/authorization/UserAndGroups.java   |   55 -
 .../security/authorization/UserContextKeys.java |   26 -
 .../authorization/UserGroupProvider.java        |  108 -
 .../UserGroupProviderInitializationContext.java |   37 -
 .../authorization/UserGroupProviderLookup.java  |   31 -
 .../annotation/AuthorizerContext.java           |   35 -
 .../exception/AccessDeniedException.java        |   39 -
 .../exception/AuthorizationAccessException.java |   32 -
 .../UninheritableAuthorizationsException.java   |   28 -
 .../SecurityProviderCreationException.java      |   38 -
 .../SecurityProviderDestructionException.java   |   38 -
 nifi-registry-security-utils/pom.xml            |   42 -
 .../security/util/CertificateUtils.java         |  670 --
 .../registry/security/util/CryptoUtils.java     |   75 -
 .../registry/security/util/KeyStoreUtils.java   |   82 -
 .../registry/security/util/KeystoreType.java    |   25 -
 .../security/util/ProxiedEntitiesUtils.java     |  127 -
 .../security/util/SslContextFactory.java        |  249 -
 nifi-registry-utils/pom.xml                     |   26 -
 .../org/apache/nifi/registry/util/DataUnit.java |  245 -
 .../apache/nifi/registry/util/EscapeUtils.java  |   41 -
 .../apache/nifi/registry/util/FileUtils.java    |  426 -
 .../apache/nifi/registry/util/FormatUtils.java  |  261 -
 .../nifi/registry/util/PropertyValue.java       |   91 -
 .../registry/util/StandardPropertyValue.java    |   79 -
 .../nifi/registry/util/TestFileUtils.java       |   31 -
 nifi-registry-web-api/pom.xml                   |  359 -
 .../registry/NiFiRegistryApiApplication.java    |   85 -
 .../registry/NiFiRegistryPropertiesFactory.java |   47 -
 .../web/NiFiRegistryResourceConfig.java         |   78 -
 .../registry/web/api/AccessPolicyResource.java  |  407 -
 .../nifi/registry/web/api/AccessResource.java   |  508 --
 .../registry/web/api/ApplicationResource.java   |  194 -
 .../api/AuthorizableApplicationResource.java    |   81 -
 .../registry/web/api/BucketFlowResource.java    |  600 --
 .../nifi/registry/web/api/BucketResource.java   |  293 -
 .../nifi/registry/web/api/ConfigResource.java   |  108 -
 .../nifi/registry/web/api/FlowResource.java     |  315 -
 .../registry/web/api/HttpStatusMessages.java    |   30 -
 .../nifi/registry/web/api/ItemResource.java     |  171 -
 .../nifi/registry/web/api/TenantResource.java   |  579 --
 .../web/exception/UnauthorizedException.java    |   74 -
 .../nifi/registry/web/link/LinkService.java     |  110 -
 .../web/link/builder/BucketLinkBuilder.java     |   45 -
 .../registry/web/link/builder/LinkBuilder.java  |   30 -
 .../link/builder/VersionedFlowLinkBuilder.java  |   46 -
 .../VersionedFlowSnapshotLinkBuilder.java       |   47 -
 .../web/mapper/AccessDeniedExceptionMapper.java |   75 -
 .../mapper/AdministrationExceptionMapper.java   |   46 -
 ...ationCredentialsNotFoundExceptionMapper.java |   50 -
 .../AuthorizationAccessExceptionMapper.java     |   46 -
 .../web/mapper/BadRequestExceptionMapper.java   |   49 -
 .../ConstraintViolationExceptionMapper.java     |   68 -
 .../mapper/IllegalArgumentExceptionMapper.java  |   48 -
 .../web/mapper/IllegalStateExceptionMapper.java |   46 -
 .../InvalidAuthenticationExceptionMapper.java   |   47 -
 .../web/mapper/NiFiRegistryJsonProvider.java    |   36 -
 .../web/mapper/NotAllowedExceptionMapper.java   |   46 -
 .../web/mapper/NotFoundExceptionMapper.java     |   50 -
 .../mapper/ResourceNotFoundExceptionMapper.java |   47 -
 .../mapper/SerializationExceptionMapper.java    |   47 -
 .../registry/web/mapper/ThrowableMapper.java    |   42 -
 .../web/mapper/UnauthorizedExceptionMapper.java |   56 -
 .../NiFiRegistryMasterKeyProviderFactory.java   |   67 -
 .../security/NiFiRegistrySecurityConfig.java    |  210 -
 .../web/security/PermissionsService.java        |  100 -
 .../authentication/AnonymousIdentityFilter.java |   39 -
 .../AuthenticationRequestToken.java             |  107 -
 .../AuthenticationSuccessToken.java             |   55 -
 .../IdentityAuthenticationProvider.java         |  140 -
 .../security/authentication/IdentityFilter.java |   97 -
 .../InvalidAuthenticationException.java         |   35 -
 .../exception/UntrustedProxyException.java      |   31 -
 .../authentication/jwt/JwtIdentityProvider.java |   85 -
 .../security/authentication/jwt/JwtService.java |  212 -
 .../kerberos/KerberosIdentityProvider.java      |  111 -
 .../kerberos/KerberosSpnegoFactory.java         |   67 -
 .../KerberosSpnegoIdentityProvider.java         |  184 -
 .../KerberosTicketValidatorFactory.java         |   69 -
 .../kerberos/KerberosUserDetailsService.java    |   38 -
 .../x509/SubjectDnX509PrincipalExtractor.java   |   35 -
 .../x509/X509CertificateExtractor.java          |   55 -
 .../X509IdentityAuthenticationProvider.java     |  131 -
 .../x509/X509IdentityProvider.java              |  174 -
 .../HttpMethodAuthorizationRules.java           |   48 -
 .../ResourceAuthorizationFilter.java            |  218 -
 .../StandardHttpMethodAuthorizationRules.java   |   40 -
 .../src/main/resources/META-INF/LICENSE         |  352 -
 .../src/main/resources/META-INF/NOTICE          |  184 -
 ...try.security.authentication.IdentityProvider |   15 -
 .../src/main/resources/banner.txt               |    8 -
 .../src/main/resources/images/bgNifiLogo.png    |  Bin 3894 -> 0 bytes
 .../src/main/resources/images/nifi16.ico        |  Bin 1150 -> 0 bytes
 .../resources/swagger/security-definitions.json |   12 -
 .../src/main/resources/templates/endpoint.hbs   |   61 -
 .../src/main/resources/templates/example.hbs    |   18 -
 .../src/main/resources/templates/index.html.hbs |  514 --
 .../src/main/resources/templates/operation.hbs  |  110 -
 .../src/main/resources/templates/type.hbs       |   57 -
 .../ResourceAuthorizationFilterSpec.groovy      |  170 -
 .../NiFiRegistryTestApiApplication.java         |   40 -
 .../registry/SecureLdapTestApiApplication.java  |   45 -
 .../apache/nifi/registry/web/TestRestAPI.java   |  170 -
 .../apache/nifi/registry/web/api/BucketsIT.java |  238 -
 .../apache/nifi/registry/web/api/FlowsIT.java   |  445 -
 .../registry/web/api/IntegrationTestBase.java   |  219 -
 .../registry/web/api/IntegrationTestUtils.java  |  120 -
 .../nifi/registry/web/api/SecureFileIT.java     |  172 -
 .../web/api/SecureITClientConfiguration.java    |   91 -
 .../nifi/registry/web/api/SecureKerberosIT.java |  216 -
 .../nifi/registry/web/api/SecureLdapIT.java     |  651 --
 .../web/api/SecureNiFiRegistryClientIT.java     |  217 -
 .../nifi/registry/web/api/UnsecuredITBase.java  |   42 -
 .../web/api/UnsecuredNiFiRegistryClientIT.java  |  403 -
 .../nifi/registry/web/link/TestLinkService.java |  124 -
 .../application-ITSecureFile.properties         |   36 -
 .../application-ITSecureKerberos.properties     |   36 -
 .../application-ITSecureLdap.properties         |   48 -
 .../application-ITUnsecured.properties          |   21 -
 .../src/test/resources/application.properties   |   25 -
 .../src/test/resources/banner.txt               |    8 -
 .../src/test/resources/conf/providers.xml       |   25 -
 .../resources/conf/secure-file/authorizers.xml  |  143 -
 .../secure-file/nifi-registry-client.properties |   25 -
 .../conf/secure-file/nifi-registry.properties   |   30 -
 .../conf/secure-kerberos/authorizers.xml        |  101 -
 .../conf/secure-kerberos/identity-providers.xml |   31 -
 .../nifi-registry-client.properties             |   22 -
 .../secure-kerberos/nifi-registry.properties    |   36 -
 .../conf/secure-ldap/authorizers.protected.xml  |  221 -
 .../resources/conf/secure-ldap/authorizers.xml  |  242 -
 .../resources/conf/secure-ldap/bootstrap.conf   |   60 -
 .../identity-providers.protected.xml            |   89 -
 .../conf/secure-ldap/identity-providers.xml     |   88 -
 .../secure-ldap/nifi-registry-client.properties |   22 -
 .../conf/secure-ldap/nifi-registry.properties   |   32 -
 .../conf/secure-ldap/test-ldap-data.ldif        |  261 -
 .../conf/unsecured/nifi-registry.properties     |   25 -
 .../src/test/resources/db/BucketsIT.sql         |   26 -
 .../src/test/resources/db/FlowsIT.sql           |   50 -
 .../src/test/resources/db/clearDB.sql           |   19 -
 .../src/test/resources/keys/README.md           |   47 -
 .../src/test/resources/keys/client-ks.jks       |  Bin 3048 -> 0 bytes
 .../src/test/resources/keys/localhost-ks.jks    |  Bin 3077 -> 0 bytes
 .../src/test/resources/keys/localhost-ts.jks    |  Bin 911 -> 0 bytes
 nifi-registry-web-docs/pom.xml                  |   68 -
 .../web/docs/DocumentationController.java       |   57 -
 .../src/main/resources/META-INF/LICENSE         |  223 -
 .../src/main/resources/META-INF/NOTICE          |   14 -
 .../main/webapp/WEB-INF/jsp/documentation.jsp   |   84 -
 .../WEB-INF/jsp/no-documentation-found.jsp      |   31 -
 .../src/main/webapp/WEB-INF/web.xml             |   33 -
 .../src/main/webapp/css/component-usage.css     |  183 -
 .../src/main/webapp/css/main.css                |  217 -
 .../src/main/webapp/images/bgBannerFoot.png     |  Bin 189 -> 0 bytes
 .../src/main/webapp/images/bgHeader.png         |  Bin 1455 -> 0 bytes
 .../src/main/webapp/images/bgTableHeader.png    |  Bin 232 -> 0 bytes
 .../src/main/webapp/js/application.js           |  400 -
 .../src/main/webapp/js/jquery.min.js            |    4 -
 nifi-registry-web-ui/pom.xml                    |  493 --
 .../src/main/frontend/Gruntfile.js              |   78 -
 .../src/main/frontend/karma-test-shim.js        |   96 -
 .../src/main/frontend/karma.conf.js             |  176 -
 .../src/main/frontend/package-lock.json         | 7817 ------------------
 .../src/main/frontend/package.json              |   81 -
 .../src/main/locale/messages.es.xlf             |  130 -
 .../src/main/resources/META-INF/LICENSE         | 1152 ---
 .../src/main/resources/META-INF/NOTICE          |   23 -
 .../resources/filters/registry-min.properties   |   19 -
 .../main/resources/filters/registry.properties  |   26 -
 .../src/main/webapp/WEB-INF/pages/index.jsp     |   35 -
 .../src/main/webapp/WEB-INF/web.xml             |   54 -
 .../nf-registry-administration.html             |   40 -
 .../nf-registry-administration.js               |   98 -
 .../nf-registry-administration.spec.js          |  146 -
 .../nf-registry-add-user-to-groups.html         |   81 -
 .../nf-registry-add-user-to-groups.js           |  254 -
 .../nf-registry-add-user-to-groups.spec.js      |  172 -
 .../dialogs/add-user/nf-registry-add-user.html  |   46 -
 .../dialogs/add-user/nf-registry-add-user.js    |  107 -
 .../add-user/nf-registry-add-user.spec.js       |   86 -
 .../nf-registry-add-users-to-group.html         |   80 -
 .../nf-registry-add-users-to-group.js           |  247 -
 .../nf-registry-add-users-to-group.spec.js      |  170 -
 .../nf-registry-create-new-group.html           |   46 -
 .../nf-registry-create-new-group.js             |  108 -
 .../nf-registry-create-new-group.spec.js        |   85 -
 .../users/nf-registry-users-administration.html |  178 -
 .../users/nf-registry-users-administration.js   |  150 -
 .../nf-registry-users-adminstration.spec.js     |  191 -
 .../manage-group/nf-registry-manage-group.html  |  204 -
 .../manage-group/nf-registry-manage-group.js    |  609 --
 .../nf-registry-manage-group.spec.js            | 2517 ------
 .../manage-user/nf-registry-manage-user.html    |  211 -
 .../manage-user/nf-registry-manage-user.js      |  633 --
 .../manage-user/nf-registry-manage-user.spec.js | 1877 -----
 .../nf-registry-add-policy-to-bucket.html       |   95 -
 .../nf-registry-add-policy-to-bucket.js         |  413 -
 .../nf-registry-create-bucket.html              |   46 -
 .../create-bucket/nf-registry-create-bucket.js  |  106 -
 .../nf-registry-create-bucket.spec.js           |   81 -
 .../nf-registry-edit-bucket-policy.html         |   55 -
 .../nf-registry-edit-bucket-policy.js           |  352 -
 .../nf-registry-workflow-administration.html    |  123 -
 .../nf-registry-workflow-administration.js      |  104 -
 .../nf-registry-workflow-administration.spec.js |  169 -
 .../nf-registry-manage-bucket.html              |  129 -
 .../manage-bucket/nf-registry-manage-bucket.js  |  462 --
 .../nf-registry-manage-bucket.spec.js           |  810 --
 .../nf-registry-bucket-grid-list-viewer.js      |  123 -
 .../nf-registry-bucket-grid-list-viewer.spec.js |  242 -
 .../nf-registry-droplet-grid-list-viewer.js     |  123 -
 ...nf-registry-droplet-grid-list-viewer.spec.js |  291 -
 .../registry/nf-registry-grid-list-viewer.html  |  124 -
 .../registry/nf-registry-grid-list-viewer.js    |   98 -
 .../nf-registry-grid-list-viewer.spec.js        |  195 -
 .../explorer/nf-registry-explorer.html          |   18 -
 .../components/explorer/nf-registry-explorer.js |   70 -
 .../explorer/nf-registry-explorer.spec.js       |  119 -
 .../login/dialogs/nf-registry-user-login.html   |   45 -
 .../login/dialogs/nf-registry-user-login.js     |   75 -
 .../components/login/nf-registry-login.html     |   19 -
 .../components/login/nf-registry-login.js       |   64 -
 .../nf-registry-page-not-found.html             |   19 -
 .../nf-registry-page-not-found.js               |   75 -
 .../webapp/images/registry-background-logo.svg  |   17 -
 .../src/main/webapp/images/registry-favicon.png |  Bin 388 -> 0 bytes
 .../webapp/images/registry-logo-web-app.svg     |   17 -
 .../src/main/webapp/nf-registry-bootstrap.js    |   59 -
 .../src/main/webapp/nf-registry.animations.js   |  133 -
 .../src/main/webapp/nf-registry.e2e-spec.js     |   30 -
 .../src/main/webapp/nf-registry.html            |   94 -
 .../src/main/webapp/nf-registry.js              |  102 -
 .../src/main/webapp/nf-registry.module.js       |  116 -
 .../src/main/webapp/nf-registry.routes.js       |  108 -
 .../src/main/webapp/nf-registry.spec.js         |  105 -
 .../src/main/webapp/services/nf-registry.api.js |  786 --
 .../webapp/services/nf-registry.api.spec.js     | 1374 ---
 .../services/nf-registry.auth-guard.service.js  |  372 -
 .../nf-registry.auth-guard.service.spec.js      |  629 --
 .../main/webapp/services/nf-registry.service.js | 1175 ---
 .../webapp/services/nf-registry.service.spec.js | 1148 ---
 .../services/nf-registry.token.interceptor.js   |   53 -
 .../main/webapp/services/nf-storage.service.js  |  219 -
 .../src/main/webapp/systemjs-angular-loader.js  |   49 -
 .../src/main/webapp/systemjs.builder.config.js  |  176 -
 .../src/main/webapp/systemjs.spec.config.js     |  145 -
 .../src/main/webapp/theming/_helperClasses.scss |   82 -
 .../main/webapp/theming/_structureElements.scss |  160 -
 .../administration/_structureElements.scss      |   22 -
 .../users/_structureElements.scss               |   72 -
 .../workflow/_structureElements.scss            |   49 -
 .../explorer/grid-list/_structureElements.scss  |   56 -
 .../src/main/webapp/theming/nf-registry.scss    |   52 -
 pom.xml                                         |  116 +-
 1381 files changed, 94882 insertions(+), 94840 deletions(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/6f26290d/nifi-registry-bootstrap/pom.xml
----------------------------------------------------------------------
diff --git a/nifi-registry-bootstrap/pom.xml b/nifi-registry-bootstrap/pom.xml
deleted file mode 100644
index 31a377f..0000000
--- a/nifi-registry-bootstrap/pom.xml
+++ /dev/null
@@ -1,40 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<!-- 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. -->
-<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
-    <modelVersion>4.0.0</modelVersion>
-    <parent>
-        <groupId>org.apache.nifi.registry</groupId>
-        <artifactId>nifi-registry</artifactId>
-        <version>0.3.0-SNAPSHOT</version>
-    </parent>
-    
-    <artifactId>nifi-registry-bootstrap</artifactId>
-    <packaging>jar</packaging>
-    
-    <dependencies>
-        <dependency>
-            <groupId>org.apache.nifi.registry</groupId>
-            <artifactId>nifi-registry-utils</artifactId>
-            <version>0.3.0-SNAPSHOT</version>
-        </dependency>
-        <dependency>
-            <groupId>org.apache.commons</groupId>
-            <artifactId>commons-lang3</artifactId>
-        </dependency>
-        <dependency>
-            <groupId>net.java.dev.jna</groupId>
-            <artifactId>jna-platform</artifactId>
-            <version>4.4.0</version>
-        </dependency>
-    </dependencies>
-
-</project>


[45/51] [partial] nifi-registry git commit: NIFIREG-201 Refactoring project structure to better isolate extensions

Posted by kd...@apache.org.
http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/6f26290d/nifi-registry-core/nifi-registry-data-model/src/main/java/org/apache/nifi/registry/flow/VersionedConnection.java
----------------------------------------------------------------------
diff --git a/nifi-registry-core/nifi-registry-data-model/src/main/java/org/apache/nifi/registry/flow/VersionedConnection.java b/nifi-registry-core/nifi-registry-data-model/src/main/java/org/apache/nifi/registry/flow/VersionedConnection.java
new file mode 100644
index 0000000..52b7d70
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-data-model/src/main/java/org/apache/nifi/registry/flow/VersionedConnection.java
@@ -0,0 +1,177 @@
+/*
+ * 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.flow;
+
+import java.util.List;
+import java.util.Set;
+
+import io.swagger.annotations.ApiModelProperty;
+
+public class VersionedConnection extends VersionedComponent {
+    private ConnectableComponent source;
+    private ConnectableComponent destination;
+    private Integer labelIndex;
+    private Long zIndex;
+    private Set<String> selectedRelationships;
+
+    private Long backPressureObjectThreshold;
+    private String backPressureDataSizeThreshold;
+    private String flowFileExpiration;
+    private List<String> prioritizers;
+    private List<Position> bends;
+
+    private String loadBalanceStrategy;
+    private String partitioningAttribute;
+    private String loadBalanceCompression;
+
+
+    @ApiModelProperty("The source of the connection.")
+    public ConnectableComponent getSource() {
+        return source;
+    }
+
+    public void setSource(ConnectableComponent source) {
+        this.source = source;
+    }
+
+    @ApiModelProperty("The destination of the connection.")
+    public ConnectableComponent getDestination() {
+        return destination;
+    }
+
+    public void setDestination(ConnectableComponent destination) {
+        this.destination = destination;
+    }
+
+    @ApiModelProperty("The bend points on the connection.")
+    public List<Position> getBends() {
+        return bends;
+    }
+
+    public void setBends(List<Position> bends) {
+        this.bends = bends;
+    }
+
+    @ApiModelProperty("The index of the bend point where to place the connection label.")
+    public Integer getLabelIndex() {
+        return labelIndex;
+    }
+
+    public void setLabelIndex(Integer labelIndex) {
+        this.labelIndex = labelIndex;
+    }
+
+    @ApiModelProperty(
+            value = "The z index of the connection.",
+            name = "zIndex")  // Jackson maps this method name to JSON key "zIndex", but Swagger does not by default
+    public Long getzIndex() {
+        return zIndex;
+    }
+
+    public void setzIndex(Long zIndex) {
+        this.zIndex = zIndex;
+    }
+
+    @ApiModelProperty("The selected relationship that comprise the connection.")
+    public Set<String> getSelectedRelationships() {
+        return selectedRelationships;
+    }
+
+    public void setSelectedRelationships(Set<String> relationships) {
+        this.selectedRelationships = relationships;
+    }
+
+
+    @ApiModelProperty("The object count threshold for determining when back pressure is applied. Updating this value is a passive change in the sense that it won't impact whether existing files "
+        + "over the limit are affected but it does help feeder processors to stop pushing too much into this work queue.")
+    public Long getBackPressureObjectThreshold() {
+        return backPressureObjectThreshold;
+    }
+
+    public void setBackPressureObjectThreshold(Long backPressureObjectThreshold) {
+        this.backPressureObjectThreshold = backPressureObjectThreshold;
+    }
+
+
+    @ApiModelProperty("The object data size threshold for determining when back pressure is applied. Updating this value is a passive change in the sense that it won't impact whether existing "
+        + "files over the limit are affected but it does help feeder processors to stop pushing too much into this work queue.")
+    public String getBackPressureDataSizeThreshold() {
+        return backPressureDataSizeThreshold;
+    }
+
+    public void setBackPressureDataSizeThreshold(String backPressureDataSizeThreshold) {
+        this.backPressureDataSizeThreshold = backPressureDataSizeThreshold;
+    }
+
+
+    @ApiModelProperty("The amount of time a flow file may be in the flow before it will be automatically aged out of the flow. Once a flow file reaches this age it will be terminated from "
+        + "the flow the next time a processor attempts to start work on it.")
+    public String getFlowFileExpiration() {
+        return flowFileExpiration;
+    }
+
+    public void setFlowFileExpiration(String flowFileExpiration) {
+        this.flowFileExpiration = flowFileExpiration;
+    }
+
+
+    @ApiModelProperty("The comparators used to prioritize the queue.")
+    public List<String> getPrioritizers() {
+        return prioritizers;
+    }
+
+    public void setPrioritizers(List<String> prioritizers) {
+        this.prioritizers = prioritizers;
+    }
+
+    @ApiModelProperty(value = "The Strategy to use for load balancing data across the cluster, or null, if no Load Balance Strategy has been specified.",
+            allowableValues = "DO_NOT_LOAD_BALANCE, PARTITION_BY_ATTRIBUTE, ROUND_ROBIN, SINGLE_NODE")
+    public String getLoadBalanceStrategy() {
+        return loadBalanceStrategy;
+    }
+
+    public void setLoadBalanceStrategy(String loadBalanceStrategy) {
+        this.loadBalanceStrategy = loadBalanceStrategy;
+    }
+
+    @ApiModelProperty("The attribute to use for partitioning data as it is load balanced across the cluster. If the Load Balance Strategy is configured to use PARTITION_BY_ATTRIBUTE, the value " +
+            "returned by this method is the name of the FlowFile Attribute that will be used to determine which node in the cluster should receive a given FlowFile. If the Load Balance Strategy is " +
+            "unset or is set to any other value, the Partitioning Attribute has no effect.")
+    public String getPartitioningAttribute() {
+        return partitioningAttribute;
+    }
+
+    public void setPartitioningAttribute(final String partitioningAttribute) {
+        this.partitioningAttribute = partitioningAttribute;
+    }
+
+    @ApiModelProperty(value = "Whether or not compression should be used when transferring FlowFiles between nodes",
+            allowableValues = "DO_NOT_COMPRESS, COMPRESS_ATTRIBUTES_ONLY, COMPRESS_ATTRIBUTES_AND_CONTENT")
+    public String getLoadBalanceCompression() {
+        return loadBalanceCompression;
+    }
+
+    public void setLoadBalanceCompression(final String compression) {
+        this.loadBalanceCompression = compression;
+    }
+
+    @Override
+    public ComponentType getComponentType() {
+        return ComponentType.CONNECTION;
+    }
+}

http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/6f26290d/nifi-registry-core/nifi-registry-data-model/src/main/java/org/apache/nifi/registry/flow/VersionedControllerService.java
----------------------------------------------------------------------
diff --git a/nifi-registry-core/nifi-registry-data-model/src/main/java/org/apache/nifi/registry/flow/VersionedControllerService.java b/nifi-registry-core/nifi-registry-data-model/src/main/java/org/apache/nifi/registry/flow/VersionedControllerService.java
new file mode 100644
index 0000000..7b14ac2
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-data-model/src/main/java/org/apache/nifi/registry/flow/VersionedControllerService.java
@@ -0,0 +1,103 @@
+/*
+ * 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.flow;
+
+import java.util.List;
+import java.util.Map;
+
+import io.swagger.annotations.ApiModelProperty;
+
+public class VersionedControllerService extends VersionedComponent
+        implements VersionedConfigurableComponent, VersionedExtensionComponent {
+
+    private String type;
+    private Bundle bundle;
+    private List<ControllerServiceAPI> controllerServiceApis;
+
+    private Map<String, String> properties;
+    private Map<String, VersionedPropertyDescriptor> propertyDescriptors;
+    private String annotationData;
+
+
+    @Override
+    @ApiModelProperty(value = "The type of the controller service.")
+    public String getType() {
+        return type;
+    }
+
+    @Override
+    public void setType(String type) {
+        this.type = type;
+    }
+
+    @Override
+    @ApiModelProperty(value = "The details of the artifact that bundled this processor type.")
+    public Bundle getBundle() {
+        return bundle;
+    }
+
+    @Override
+    public void setBundle(Bundle bundle) {
+        this.bundle = bundle;
+    }
+
+    @ApiModelProperty(value = "Lists the APIs this Controller Service implements.")
+    public List<ControllerServiceAPI> getControllerServiceApis() {
+        return controllerServiceApis;
+    }
+
+    public void setControllerServiceApis(List<ControllerServiceAPI> controllerServiceApis) {
+        this.controllerServiceApis = controllerServiceApis;
+    }
+
+    @Override
+    @ApiModelProperty(value = "The properties of the controller service.")
+    public Map<String, String> getProperties() {
+        return properties;
+    }
+
+    @Override
+    public void setProperties(Map<String, String> properties) {
+        this.properties = properties;
+    }
+
+    @Override
+    @ApiModelProperty("The property descriptors for the processor.")
+    public Map<String, VersionedPropertyDescriptor> getPropertyDescriptors() {
+        return propertyDescriptors;
+    }
+
+    @Override
+    public void setPropertyDescriptors(Map<String, VersionedPropertyDescriptor> propertyDescriptors) {
+        this.propertyDescriptors = propertyDescriptors;
+    }
+
+    @ApiModelProperty(value = "The annotation for the controller service. This is how the custom UI relays configuration to the controller service.")
+    public String getAnnotationData() {
+        return annotationData;
+    }
+
+    public void setAnnotationData(String annotationData) {
+        this.annotationData = annotationData;
+    }
+
+    @Override
+    public ComponentType getComponentType() {
+        return ComponentType.CONTROLLER_SERVICE;
+    }
+}

http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/6f26290d/nifi-registry-core/nifi-registry-data-model/src/main/java/org/apache/nifi/registry/flow/VersionedExtensionComponent.java
----------------------------------------------------------------------
diff --git a/nifi-registry-core/nifi-registry-data-model/src/main/java/org/apache/nifi/registry/flow/VersionedExtensionComponent.java b/nifi-registry-core/nifi-registry-data-model/src/main/java/org/apache/nifi/registry/flow/VersionedExtensionComponent.java
new file mode 100644
index 0000000..e1d514c
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-data-model/src/main/java/org/apache/nifi/registry/flow/VersionedExtensionComponent.java
@@ -0,0 +1,32 @@
+/*
+ * 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.flow;
+
+/**
+ * A component that is an extension and has a type and bundle.
+ */
+public interface VersionedExtensionComponent {
+
+    Bundle getBundle();
+
+    void setBundle(Bundle bundle);
+
+    String getType();
+
+    void setType(String type);
+
+}

http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/6f26290d/nifi-registry-core/nifi-registry-data-model/src/main/java/org/apache/nifi/registry/flow/VersionedFlow.java
----------------------------------------------------------------------
diff --git a/nifi-registry-core/nifi-registry-data-model/src/main/java/org/apache/nifi/registry/flow/VersionedFlow.java b/nifi-registry-core/nifi-registry-data-model/src/main/java/org/apache/nifi/registry/flow/VersionedFlow.java
new file mode 100644
index 0000000..6ece46a
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-data-model/src/main/java/org/apache/nifi/registry/flow/VersionedFlow.java
@@ -0,0 +1,56 @@
+/*
+ * 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.flow;
+
+import io.swagger.annotations.ApiModel;
+import io.swagger.annotations.ApiModelProperty;
+import org.apache.nifi.registry.bucket.BucketItem;
+import org.apache.nifi.registry.bucket.BucketItemType;
+
+import javax.validation.constraints.Min;
+import javax.xml.bind.annotation.XmlRootElement;
+
+/**
+ * <p>
+ * Represents a versioned flow. A versioned flow is a named flow that is expected to change
+ * over time. This flow is saved to the registry with information such as its name, a description,
+ * and each version of the flow.
+ * </p>
+ *
+ * @see VersionedFlowSnapshot
+ */
+@XmlRootElement
+@ApiModel(value = "versionedFlow")
+public class VersionedFlow extends BucketItem {
+
+    @Min(0)
+    private long versionCount;
+
+    public VersionedFlow() {
+        super(BucketItemType.Flow);
+    }
+
+    @ApiModelProperty(value = "The number of versions of this flow.", readOnly = true)
+    public long getVersionCount() {
+        return versionCount;
+    }
+
+    public void setVersionCount(long versionCount) {
+        this.versionCount = versionCount;
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/6f26290d/nifi-registry-core/nifi-registry-data-model/src/main/java/org/apache/nifi/registry/flow/VersionedFlowCoordinates.java
----------------------------------------------------------------------
diff --git a/nifi-registry-core/nifi-registry-data-model/src/main/java/org/apache/nifi/registry/flow/VersionedFlowCoordinates.java b/nifi-registry-core/nifi-registry-data-model/src/main/java/org/apache/nifi/registry/flow/VersionedFlowCoordinates.java
new file mode 100644
index 0000000..8e39c5b
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-data-model/src/main/java/org/apache/nifi/registry/flow/VersionedFlowCoordinates.java
@@ -0,0 +1,101 @@
+/*
+ * 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.flow;
+
+import java.util.Objects;
+
+import io.swagger.annotations.ApiModelProperty;
+
+public class VersionedFlowCoordinates {
+    private String registryUrl;
+    private String bucketId;
+    private String flowId;
+    private int version;
+    private Boolean latest;
+
+    @ApiModelProperty("The URL of the Flow Registry that contains the flow")
+    public String getRegistryUrl() {
+        return registryUrl;
+    }
+
+    public void setRegistryUrl(String registryUrl) {
+        this.registryUrl = registryUrl;
+    }
+
+    @ApiModelProperty("The UUID of the bucket that the flow resides in")
+    public String getBucketId() {
+        return bucketId;
+    }
+
+    public void setBucketId(String bucketId) {
+        this.bucketId = bucketId;
+    }
+
+    @ApiModelProperty("The UUID of the flow")
+    public String getFlowId() {
+        return flowId;
+    }
+
+    public void setFlowId(String flowId) {
+        this.flowId = flowId;
+    }
+
+    @ApiModelProperty("The version of the flow")
+    public int getVersion() {
+        return version;
+    }
+
+    public void setVersion(int version) {
+        this.version = version;
+    }
+
+    @ApiModelProperty("Whether or not these coordinates point to the latest version of the flow")
+    public Boolean getLatest() {
+        return latest;
+    }
+
+    public void setLatest(Boolean latest) {
+        this.latest = latest;
+    }
+
+    @Override
+    public int hashCode() {
+        return Objects.hash(registryUrl, bucketId, flowId, version);
+    }
+
+    @Override
+    public boolean equals(final Object obj) {
+        if (obj == null) {
+            return false;
+        }
+        if (obj == this) {
+            return true;
+        }
+        if (!(obj instanceof VersionedFlowCoordinates)) {
+            return false;
+        }
+
+        final VersionedFlowCoordinates other = (VersionedFlowCoordinates) obj;
+        return Objects.equals(registryUrl, other.registryUrl) && Objects.equals(bucketId, other.bucketId) && Objects.equals(flowId, other.flowId) && Objects.equals(version, other.version);
+    }
+
+    @Override
+    public String toString() {
+        return "VersionedFlowCoordinates[bucketId=" + bucketId + ", flowId=" + flowId + ", version=" + version + ", registryUrl=" + registryUrl + "]";
+    }
+}

http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/6f26290d/nifi-registry-core/nifi-registry-data-model/src/main/java/org/apache/nifi/registry/flow/VersionedFlowSnapshot.java
----------------------------------------------------------------------
diff --git a/nifi-registry-core/nifi-registry-data-model/src/main/java/org/apache/nifi/registry/flow/VersionedFlowSnapshot.java b/nifi-registry-core/nifi-registry-data-model/src/main/java/org/apache/nifi/registry/flow/VersionedFlowSnapshot.java
new file mode 100644
index 0000000..bc82397
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-data-model/src/main/java/org/apache/nifi/registry/flow/VersionedFlowSnapshot.java
@@ -0,0 +1,128 @@
+/*
+ * 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.flow;
+
+import io.swagger.annotations.ApiModel;
+import io.swagger.annotations.ApiModelProperty;
+import org.apache.nifi.registry.bucket.Bucket;
+
+import javax.validation.Valid;
+import javax.validation.constraints.NotNull;
+import javax.xml.bind.annotation.XmlRootElement;
+import javax.xml.bind.annotation.XmlTransient;
+import java.util.Objects;
+
+/**
+ * <p>
+ * Represents a snapshot of a versioned flow. A versioned flow may change many times
+ * over the course of its life. Each of these versions that is saved to the registry
+ * is saved as a snapshot, representing information such as the name of the flow, the
+ * version of the flow, the timestamp when it was saved, the contents of the flow, etc.
+ * </p>
+ */
+@ApiModel(value = "versionedFlowSnapshot")
+@XmlRootElement
+public class VersionedFlowSnapshot {
+
+    @Valid
+    @NotNull
+    private VersionedFlowSnapshotMetadata snapshotMetadata;
+
+    @Valid
+    @NotNull
+    private VersionedProcessGroup flowContents;
+
+    // read-only, only populated from retrieval of a snapshot
+    private VersionedFlow flow;
+
+    // read-only, only populated from retrieval of a snapshot
+    private Bucket bucket;
+
+
+    @ApiModelProperty(value = "The metadata for this snapshot", required = true)
+    public VersionedFlowSnapshotMetadata getSnapshotMetadata() {
+        return snapshotMetadata;
+    }
+
+    public void setSnapshotMetadata(VersionedFlowSnapshotMetadata snapshotMetadata) {
+        this.snapshotMetadata = snapshotMetadata;
+    }
+
+    @ApiModelProperty(value = "The contents of the versioned flow", required = true)
+    public VersionedProcessGroup getFlowContents() {
+        return flowContents;
+    }
+
+    public void setFlowContents(VersionedProcessGroup flowContents) {
+        this.flowContents = flowContents;
+    }
+
+    @ApiModelProperty(value = "The flow this snapshot is for", readOnly = true)
+    public VersionedFlow getFlow() {
+        return flow;
+    }
+
+    public void setFlow(VersionedFlow flow) {
+        this.flow = flow;
+    }
+
+    @ApiModelProperty(value = "The bucket where the flow is located", readOnly = true)
+    public Bucket getBucket() {
+        return bucket;
+    }
+
+    public void setBucket(Bucket bucket) {
+        this.bucket = bucket;
+    }
+
+    /**
+     * This is a convenience method that will return true when flow is populated and when the flow's versionCount
+     * is equal to the version of this snapshot.
+     *
+     * @return true if flow is populated and if this snapshot is the latest version for the flow at the time of retrieval
+     */
+    @XmlTransient
+    public boolean isLatest() {
+        return flow != null && snapshotMetadata != null && flow.getVersionCount() == getSnapshotMetadata().getVersion();
+    }
+
+    @Override
+    public int hashCode() {
+        return Objects.hash(this.snapshotMetadata);
+    }
+
+    @Override
+    public boolean equals(final Object obj) {
+        if (obj == null) {
+            return false;
+        }
+        if (getClass() != obj.getClass()) {
+            return false;
+        }
+
+        final VersionedFlowSnapshot other = (VersionedFlowSnapshot) obj;
+        return Objects.equals(this.snapshotMetadata, other.snapshotMetadata);
+    }
+
+    @Override
+    public String toString() {
+        final String flowName = (flow == null ? "null" : flow.getName());
+        return "VersionedFlowSnapshot[flowId=" + snapshotMetadata.getFlowIdentifier() + ", flowName=" + flowName
+            + ", version=" + snapshotMetadata.getVersion() + ", comments=" + snapshotMetadata.getComments() + "]";
+    }
+}

http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/6f26290d/nifi-registry-core/nifi-registry-data-model/src/main/java/org/apache/nifi/registry/flow/VersionedFlowSnapshotMetadata.java
----------------------------------------------------------------------
diff --git a/nifi-registry-core/nifi-registry-data-model/src/main/java/org/apache/nifi/registry/flow/VersionedFlowSnapshotMetadata.java b/nifi-registry-core/nifi-registry-data-model/src/main/java/org/apache/nifi/registry/flow/VersionedFlowSnapshotMetadata.java
new file mode 100644
index 0000000..2007279
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-data-model/src/main/java/org/apache/nifi/registry/flow/VersionedFlowSnapshotMetadata.java
@@ -0,0 +1,130 @@
+/*
+ * 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.flow;
+
+import io.swagger.annotations.ApiModel;
+import io.swagger.annotations.ApiModelProperty;
+import org.apache.nifi.registry.link.LinkableEntity;
+
+import javax.validation.constraints.Min;
+import javax.validation.constraints.NotBlank;
+import java.util.Objects;
+
+/**
+ * The metadata information about a VersionedFlowSnapshot. This class implements Comparable in order
+ * to sort based on the snapshot version in ascending order.
+ */
+@ApiModel(value = "versionedFlowSnapshotMetadata")
+public class VersionedFlowSnapshotMetadata extends LinkableEntity implements Comparable<VersionedFlowSnapshotMetadata> {
+
+    @NotBlank
+    private String bucketIdentifier;
+
+    @NotBlank
+    private String flowIdentifier;
+
+    @Min(1)
+    private int version;
+
+    @Min(1)
+    private long timestamp;
+
+    @NotBlank
+    private String author;
+
+    private String comments;
+
+
+    @ApiModelProperty(value = "The identifier of the bucket this snapshot belongs to.", required = true)
+    public String getBucketIdentifier() {
+        return bucketIdentifier;
+    }
+
+    public void setBucketIdentifier(String bucketIdentifier) {
+        this.bucketIdentifier = bucketIdentifier;
+    }
+
+    @ApiModelProperty(value = "The identifier of the flow this snapshot belongs to.", required = true)
+    public String getFlowIdentifier() {
+        return flowIdentifier;
+    }
+
+    public void setFlowIdentifier(String flowIdentifier) {
+        this.flowIdentifier = flowIdentifier;
+    }
+
+    @ApiModelProperty(value = "The version of this snapshot of the flow.", required = true)
+    public int getVersion() {
+        return version;
+    }
+
+    public void setVersion(int version) {
+        this.version = version;
+    }
+
+    @ApiModelProperty(value = "The timestamp when the flow was saved, as milliseconds since epoch.", readOnly = true)
+    public long getTimestamp() {
+        return timestamp;
+    }
+
+    public void setTimestamp(long timestamp) {
+        this.timestamp = timestamp;
+    }
+
+    @ApiModelProperty(value = "The user that created this snapshot of the flow.", readOnly = true)
+    public String getAuthor() {
+        return author;
+    }
+
+    public void setAuthor(String author) {
+        this.author = author;
+    }
+
+    @ApiModelProperty("The comments provided by the user when creating the snapshot.")
+    public String getComments() {
+        return comments;
+    }
+
+    public void setComments(String comments) {
+        this.comments = comments;
+    }
+
+    @Override
+    public int compareTo(final VersionedFlowSnapshotMetadata o) {
+        return o == null ? -1 : Integer.compare(version, o.version);
+    }
+
+    @Override
+    public int hashCode() {
+        return Objects.hash(this.flowIdentifier, Integer.valueOf(this.version));
+    }
+
+    @Override
+    public boolean equals(final Object obj) {
+        if (obj == null) {
+            return false;
+        }
+        if (getClass() != obj.getClass()) {
+            return false;
+        }
+
+        final VersionedFlowSnapshotMetadata other = (VersionedFlowSnapshotMetadata) obj;
+
+        return Objects.equals(this.flowIdentifier, other.flowIdentifier)
+                && Objects.equals(this.version, other.version);
+    }
+}

http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/6f26290d/nifi-registry-core/nifi-registry-data-model/src/main/java/org/apache/nifi/registry/flow/VersionedFunnel.java
----------------------------------------------------------------------
diff --git a/nifi-registry-core/nifi-registry-data-model/src/main/java/org/apache/nifi/registry/flow/VersionedFunnel.java b/nifi-registry-core/nifi-registry-data-model/src/main/java/org/apache/nifi/registry/flow/VersionedFunnel.java
new file mode 100644
index 0000000..871dafc
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-data-model/src/main/java/org/apache/nifi/registry/flow/VersionedFunnel.java
@@ -0,0 +1,25 @@
+/*
+ * 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.flow;
+
+public class VersionedFunnel extends VersionedComponent {
+    @Override
+    public ComponentType getComponentType() {
+        return ComponentType.FUNNEL;
+    }
+}

http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/6f26290d/nifi-registry-core/nifi-registry-data-model/src/main/java/org/apache/nifi/registry/flow/VersionedLabel.java
----------------------------------------------------------------------
diff --git a/nifi-registry-core/nifi-registry-data-model/src/main/java/org/apache/nifi/registry/flow/VersionedLabel.java b/nifi-registry-core/nifi-registry-data-model/src/main/java/org/apache/nifi/registry/flow/VersionedLabel.java
new file mode 100644
index 0000000..f2f7887
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-data-model/src/main/java/org/apache/nifi/registry/flow/VersionedLabel.java
@@ -0,0 +1,73 @@
+/*
+ * 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.flow;
+
+import java.util.Map;
+
+import io.swagger.annotations.ApiModelProperty;
+
+public class VersionedLabel extends VersionedComponent {
+    private String label;
+
+    private Double width;
+    private Double height;
+
+    private Map<String, String> style;
+
+
+    @ApiModelProperty("The text that appears in the label.")
+    public String getLabel() {
+        return label;
+    }
+
+    public void setLabel(final String label) {
+        this.label = label;
+    }
+
+    @ApiModelProperty("The styles for this label (font-size : 12px, background-color : #eee, etc).")
+    public Map<String, String> getStyle() {
+        return style;
+    }
+
+    public void setStyle(final Map<String, String> style) {
+        this.style = style;
+    }
+
+    @ApiModelProperty("The height of the label in pixels when at a 1:1 scale.")
+    public Double getHeight() {
+        return height;
+    }
+
+    public void setHeight(Double height) {
+        this.height = height;
+    }
+
+    @ApiModelProperty("The width of the label in pixels when at a 1:1 scale.")
+    public Double getWidth() {
+        return width;
+    }
+
+    public void setWidth(Double width) {
+        this.width = width;
+    }
+
+    @Override
+    public ComponentType getComponentType() {
+        return ComponentType.LABEL;
+    }
+}

http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/6f26290d/nifi-registry-core/nifi-registry-data-model/src/main/java/org/apache/nifi/registry/flow/VersionedPort.java
----------------------------------------------------------------------
diff --git a/nifi-registry-core/nifi-registry-data-model/src/main/java/org/apache/nifi/registry/flow/VersionedPort.java b/nifi-registry-core/nifi-registry-data-model/src/main/java/org/apache/nifi/registry/flow/VersionedPort.java
new file mode 100644
index 0000000..f24e386
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-data-model/src/main/java/org/apache/nifi/registry/flow/VersionedPort.java
@@ -0,0 +1,52 @@
+/*
+ * 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.flow;
+
+import io.swagger.annotations.ApiModelProperty;
+
+public class VersionedPort extends VersionedComponent {
+    private PortType type;
+    private Integer concurrentlySchedulableTaskCount;
+
+    @ApiModelProperty("The number of tasks that should be concurrently scheduled for the port.")
+    public Integer getConcurrentlySchedulableTaskCount() {
+        return concurrentlySchedulableTaskCount;
+    }
+
+    public void setConcurrentlySchedulableTaskCount(Integer concurrentlySchedulableTaskCount) {
+        this.concurrentlySchedulableTaskCount = concurrentlySchedulableTaskCount;
+    }
+
+    @ApiModelProperty("The type of port.")
+    public PortType getType() {
+        return type;
+    }
+
+    public void setType(PortType type) {
+        this.type = type;
+    }
+
+    @Override
+    public ComponentType getComponentType() {
+        if (type == PortType.OUTPUT_PORT) {
+            return ComponentType.OUTPUT_PORT;
+        }
+
+        return ComponentType.INPUT_PORT;
+    }
+}

http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/6f26290d/nifi-registry-core/nifi-registry-data-model/src/main/java/org/apache/nifi/registry/flow/VersionedProcessGroup.java
----------------------------------------------------------------------
diff --git a/nifi-registry-core/nifi-registry-data-model/src/main/java/org/apache/nifi/registry/flow/VersionedProcessGroup.java b/nifi-registry-core/nifi-registry-data-model/src/main/java/org/apache/nifi/registry/flow/VersionedProcessGroup.java
new file mode 100644
index 0000000..2acd0a4
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-data-model/src/main/java/org/apache/nifi/registry/flow/VersionedProcessGroup.java
@@ -0,0 +1,148 @@
+/*
+ * 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.flow;
+
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Map;
+import java.util.Set;
+
+import javax.xml.bind.annotation.XmlRootElement;
+
+import io.swagger.annotations.ApiModelProperty;
+
+@XmlRootElement
+public class VersionedProcessGroup extends VersionedComponent {
+
+    private Set<VersionedProcessGroup> processGroups = new HashSet<>();
+    private Set<VersionedRemoteProcessGroup> remoteProcessGroups = new HashSet<>();
+    private Set<VersionedProcessor> processors = new HashSet<>();
+    private Set<VersionedPort> inputPorts = new HashSet<>();
+    private Set<VersionedPort> outputPorts = new HashSet<>();
+    private Set<VersionedConnection> connections = new HashSet<>();
+    private Set<VersionedLabel> labels = new HashSet<>();
+    private Set<VersionedFunnel> funnels = new HashSet<>();
+    private Set<VersionedControllerService> controllerServices = new HashSet<>();
+    private VersionedFlowCoordinates versionedFlowCoordinates = null;
+
+    private Map<String, String> variables = new HashMap<>();
+
+    @ApiModelProperty("The child Process Groups")
+    public Set<VersionedProcessGroup> getProcessGroups() {
+        return processGroups;
+    }
+
+    public void setProcessGroups(Set<VersionedProcessGroup> processGroups) {
+        this.processGroups = new HashSet<>(processGroups);
+    }
+
+    @ApiModelProperty("The Remote Process Groups")
+    public Set<VersionedRemoteProcessGroup> getRemoteProcessGroups() {
+        return remoteProcessGroups;
+    }
+
+    public void setRemoteProcessGroups(Set<VersionedRemoteProcessGroup> remoteProcessGroups) {
+        this.remoteProcessGroups = new HashSet<>(remoteProcessGroups);
+    }
+
+    @ApiModelProperty("The Processors")
+    public Set<VersionedProcessor> getProcessors() {
+        return processors;
+    }
+
+    public void setProcessors(Set<VersionedProcessor> processors) {
+        this.processors = new HashSet<>(processors);
+    }
+
+    @ApiModelProperty("The Input Ports")
+    public Set<VersionedPort> getInputPorts() {
+        return inputPorts;
+    }
+
+    public void setInputPorts(Set<VersionedPort> inputPorts) {
+        this.inputPorts = new HashSet<>(inputPorts);
+    }
+
+    @ApiModelProperty("The Output Ports")
+    public Set<VersionedPort> getOutputPorts() {
+        return outputPorts;
+    }
+
+    public void setOutputPorts(Set<VersionedPort> outputPorts) {
+        this.outputPorts = new HashSet<>(outputPorts);
+    }
+
+    @ApiModelProperty("The Connections")
+    public Set<VersionedConnection> getConnections() {
+        return connections;
+    }
+
+    public void setConnections(Set<VersionedConnection> connections) {
+        this.connections = new HashSet<>(connections);
+    }
+
+    @ApiModelProperty("The Labels")
+    public Set<VersionedLabel> getLabels() {
+        return labels;
+    }
+
+    public void setLabels(Set<VersionedLabel> labels) {
+        this.labels = new HashSet<>(labels);
+    }
+
+    @ApiModelProperty("The Funnels")
+    public Set<VersionedFunnel> getFunnels() {
+        return funnels;
+    }
+
+    public void setFunnels(Set<VersionedFunnel> funnels) {
+        this.funnels = new HashSet<>(funnels);
+    }
+
+    @ApiModelProperty("The Controller Services")
+    public Set<VersionedControllerService> getControllerServices() {
+        return controllerServices;
+    }
+
+    public void setControllerServices(Set<VersionedControllerService> controllerServices) {
+        this.controllerServices = new HashSet<>(controllerServices);
+    }
+
+    @Override
+    public ComponentType getComponentType() {
+        return ComponentType.PROCESS_GROUP;
+    }
+
+    public void setVariables(Map<String, String> variables) {
+        this.variables = variables;
+    }
+
+    @ApiModelProperty("The Variables in the Variable Registry for this Process Group (not including any ancestor or descendant Process Groups)")
+    public Map<String, String> getVariables() {
+        return variables;
+    }
+
+    public void setVersionedFlowCoordinates(VersionedFlowCoordinates flowCoordinates) {
+        this.versionedFlowCoordinates = flowCoordinates;
+    }
+
+    @ApiModelProperty("The coordinates where the remote flow is stored, or null if the Process Group is not directly under Version Control")
+    public VersionedFlowCoordinates getVersionedFlowCoordinates() {
+        return versionedFlowCoordinates;
+    }
+}

http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/6f26290d/nifi-registry-core/nifi-registry-data-model/src/main/java/org/apache/nifi/registry/flow/VersionedProcessor.java
----------------------------------------------------------------------
diff --git a/nifi-registry-core/nifi-registry-data-model/src/main/java/org/apache/nifi/registry/flow/VersionedProcessor.java b/nifi-registry-core/nifi-registry-data-model/src/main/java/org/apache/nifi/registry/flow/VersionedProcessor.java
new file mode 100644
index 0000000..aef6dcc
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-data-model/src/main/java/org/apache/nifi/registry/flow/VersionedProcessor.java
@@ -0,0 +1,197 @@
+/*
+ * 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.flow;
+
+import java.util.Map;
+import java.util.Set;
+
+import io.swagger.annotations.ApiModelProperty;
+
+public class VersionedProcessor extends VersionedComponent
+        implements VersionedConfigurableComponent, VersionedExtensionComponent {
+
+    private Bundle bundle;
+    private Map<String, String> style;
+
+    private String type;
+    private Map<String, String> properties;
+    private Map<String, VersionedPropertyDescriptor> propertyDescriptors;
+    private String annotationData;
+
+    private String schedulingPeriod;
+    private String schedulingStrategy;
+    private String executionNode;
+    private String penaltyDuration;
+    private String yieldDuration;
+    private String bulletinLevel;
+    private Long runDurationMillis;
+    private Integer concurrentlySchedulableTaskCount;
+    private Set<String> autoTerminatedRelationships;
+
+
+    @ApiModelProperty("The frequency with which to schedule the processor. The format of the value will depend on th value of schedulingStrategy.")
+    public String getSchedulingPeriod() {
+        return schedulingPeriod;
+    }
+
+    public void setSchedulingPeriod(String setSchedulingPeriod) {
+        this.schedulingPeriod = setSchedulingPeriod;
+    }
+
+    @ApiModelProperty("Indcates whether the prcessor should be scheduled to run in event or timer driven mode.")
+    public String getSchedulingStrategy() {
+        return schedulingStrategy;
+    }
+
+    public void setSchedulingStrategy(String schedulingStrategy) {
+        this.schedulingStrategy = schedulingStrategy;
+    }
+
+    @Override
+    @ApiModelProperty("The type of Processor")
+    public String getType() {
+        return type;
+    }
+
+    @Override
+    public void setType(final String type) {
+        this.type = type;
+    }
+
+    @ApiModelProperty("Indicates the node where the process will execute.")
+    public String getExecutionNode() {
+        return executionNode;
+    }
+
+    public void setExecutionNode(String executionNode) {
+        this.executionNode = executionNode;
+    }
+
+    @ApiModelProperty("The amout of time that is used when the process penalizes a flowfile.")
+    public String getPenaltyDuration() {
+        return penaltyDuration;
+    }
+
+    public void setPenaltyDuration(String penaltyDuration) {
+        this.penaltyDuration = penaltyDuration;
+    }
+
+    @ApiModelProperty("The amount of time that must elapse before this processor is scheduled again after yielding.")
+    public String getYieldDuration() {
+        return yieldDuration;
+    }
+
+    public void setYieldDuration(String yieldDuration) {
+        this.yieldDuration = yieldDuration;
+    }
+
+    @ApiModelProperty("The level at which the processor will report bulletins.")
+    public String getBulletinLevel() {
+        return bulletinLevel;
+    }
+
+    public void setBulletinLevel(String bulletinLevel) {
+        this.bulletinLevel = bulletinLevel;
+    }
+
+    @ApiModelProperty("The number of tasks that should be concurrently schedule for the processor. If the processor doesn't allow parallol processing then any positive input will be ignored.")
+    public Integer getConcurrentlySchedulableTaskCount() {
+        return concurrentlySchedulableTaskCount;
+    }
+
+    public void setConcurrentlySchedulableTaskCount(Integer concurrentlySchedulableTaskCount) {
+        this.concurrentlySchedulableTaskCount = concurrentlySchedulableTaskCount;
+    }
+
+    @Override
+    @ApiModelProperty("The properties for the processor. Properties whose value is not set will only contain the property name.")
+    public Map<String, String> getProperties() {
+        return properties;
+    }
+
+    @Override
+    public void setProperties(Map<String, String> properties) {
+        this.properties = properties;
+    }
+
+    @Override
+    @ApiModelProperty("The property descriptors for the processor.")
+    public Map<String, VersionedPropertyDescriptor> getPropertyDescriptors() {
+        return propertyDescriptors;
+    }
+
+    @Override
+    public void setPropertyDescriptors(Map<String, VersionedPropertyDescriptor> propertyDescriptors) {
+        this.propertyDescriptors = propertyDescriptors;
+    }
+
+    @ApiModelProperty("The annotation data for the processor used to relay configuration between a custom UI and the procesosr.")
+    public String getAnnotationData() {
+        return annotationData;
+    }
+
+    public void setAnnotationData(String annotationData) {
+        this.annotationData = annotationData;
+    }
+
+
+    @ApiModelProperty("The names of all relationships that cause a flow file to be terminated if the relationship is not connected elsewhere. This property differs "
+        + "from the 'isAutoTerminate' property of the RelationshipDTO in that the RelationshipDTO is meant to depict the current configuration, whereas this "
+        + "property can be set in a DTO when updating a Processor in order to change which Relationships should be auto-terminated.")
+    public Set<String> getAutoTerminatedRelationships() {
+        return autoTerminatedRelationships;
+    }
+
+    public void setAutoTerminatedRelationships(final Set<String> autoTerminatedRelationships) {
+        this.autoTerminatedRelationships = autoTerminatedRelationships;
+    }
+
+    @ApiModelProperty("The run duration for the processor in milliseconds.")
+    public Long getRunDurationMillis() {
+        return runDurationMillis;
+    }
+
+    public void setRunDurationMillis(Long runDurationMillis) {
+        this.runDurationMillis = runDurationMillis;
+    }
+
+    @Override
+    @ApiModelProperty("Information about the bundle from which the component came")
+    public Bundle getBundle() {
+        return bundle;
+    }
+
+    @Override
+    public void setBundle(Bundle bundle) {
+        this.bundle = bundle;
+    }
+
+    @ApiModelProperty("Stylistic data for rendering in a UI")
+    public Map<String, String> getStyle() {
+        return style;
+    }
+
+    public void setStyle(Map<String, String> style) {
+        this.style = style;
+    }
+
+    @Override
+    public ComponentType getComponentType() {
+        return ComponentType.PROCESSOR;
+    }
+}

http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/6f26290d/nifi-registry-core/nifi-registry-data-model/src/main/java/org/apache/nifi/registry/flow/VersionedPropertyDescriptor.java
----------------------------------------------------------------------
diff --git a/nifi-registry-core/nifi-registry-data-model/src/main/java/org/apache/nifi/registry/flow/VersionedPropertyDescriptor.java b/nifi-registry-core/nifi-registry-data-model/src/main/java/org/apache/nifi/registry/flow/VersionedPropertyDescriptor.java
new file mode 100644
index 0000000..2fa9463
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-data-model/src/main/java/org/apache/nifi/registry/flow/VersionedPropertyDescriptor.java
@@ -0,0 +1,63 @@
+/*
+ * 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.flow;
+
+import io.swagger.annotations.ApiModelProperty;
+
+public class VersionedPropertyDescriptor {
+    private String name;
+    private String displayName;
+    private boolean identifiesControllerService;
+    private boolean sensitive;
+
+    @ApiModelProperty("The name of the property")
+    public String getName() {
+        return name;
+    }
+
+    public void setName(String name) {
+        this.name = name;
+    }
+
+    @ApiModelProperty("The display name of the property")
+    public String getDisplayName() {
+        return displayName;
+    }
+
+    public void setDisplayName(String displayName) {
+        this.displayName = displayName;
+    }
+
+    @ApiModelProperty("Whether or not the property provides the identifier of a Controller Service")
+    public boolean getIdentifiesControllerService() {
+        return identifiesControllerService;
+    }
+
+    public void setIdentifiesControllerService(boolean identifiesControllerService) {
+        this.identifiesControllerService = identifiesControllerService;
+    }
+
+    @ApiModelProperty("Whether or not the property is considered sensitive")
+    public boolean isSensitive() {
+        return sensitive;
+    }
+
+    public void setSensitive(boolean sensitive) {
+        this.sensitive = sensitive;
+    }
+}

http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/6f26290d/nifi-registry-core/nifi-registry-data-model/src/main/java/org/apache/nifi/registry/flow/VersionedRemoteGroupPort.java
----------------------------------------------------------------------
diff --git a/nifi-registry-core/nifi-registry-data-model/src/main/java/org/apache/nifi/registry/flow/VersionedRemoteGroupPort.java b/nifi-registry-core/nifi-registry-data-model/src/main/java/org/apache/nifi/registry/flow/VersionedRemoteGroupPort.java
new file mode 100644
index 0000000..ca85ce4
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-data-model/src/main/java/org/apache/nifi/registry/flow/VersionedRemoteGroupPort.java
@@ -0,0 +1,109 @@
+/*
+ * 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.flow;
+
+import java.util.Objects;
+
+import io.swagger.annotations.ApiModelProperty;
+
+public class VersionedRemoteGroupPort extends VersionedComponent {
+    private String remoteGroupId;
+    private Integer concurrentlySchedulableTaskCount;
+    private Boolean useCompression;
+    private BatchSize batchSize;
+    private ComponentType componentType;
+    private String targetId;
+
+    @ApiModelProperty("The number of task that may transmit flowfiles to the target port concurrently.")
+    public Integer getConcurrentlySchedulableTaskCount() {
+        return concurrentlySchedulableTaskCount;
+    }
+
+    public void setConcurrentlySchedulableTaskCount(Integer concurrentlySchedulableTaskCount) {
+        this.concurrentlySchedulableTaskCount = concurrentlySchedulableTaskCount;
+    }
+
+    @ApiModelProperty("The id of the remote process group that the port resides in.")
+    public String getRemoteGroupId() {
+        return remoteGroupId;
+    }
+
+    public void setRemoteGroupId(String groupId) {
+        this.remoteGroupId = groupId;
+    }
+
+
+    @ApiModelProperty("Whether the flowfiles are compressed when sent to the target port.")
+    public Boolean isUseCompression() {
+        return useCompression;
+    }
+
+    public void setUseCompression(Boolean useCompression) {
+        this.useCompression = useCompression;
+    }
+
+    @ApiModelProperty("The batch settings for data transmission.")
+    public BatchSize getBatchSize() {
+        return batchSize;
+    }
+
+    public void setBatchSize(BatchSize batchSize) {
+        this.batchSize = batchSize;
+    }
+
+    @ApiModelProperty("The ID of the port on the target NiFi instance")
+    public String getTargetId() {
+        return targetId;
+    }
+
+    public void setTargetId(final String targetId) {
+        this.targetId = targetId;
+    }
+
+    @Override
+    public int hashCode() {
+        return 923847 + String.valueOf(getName()).hashCode();
+    }
+
+    @Override
+    public boolean equals(final Object obj) {
+        if (obj == null) {
+            return false;
+        }
+        if (!(obj instanceof VersionedRemoteGroupPort)) {
+            return false;
+        }
+
+        final VersionedRemoteGroupPort other = (VersionedRemoteGroupPort) obj;
+        return Objects.equals(getName(), other.getName());
+    }
+
+    @Override
+    public ComponentType getComponentType() {
+        return componentType;
+    }
+
+    @Override
+    public void setComponentType(final ComponentType componentType) {
+        if (componentType != ComponentType.REMOTE_INPUT_PORT && componentType != ComponentType.REMOTE_OUTPUT_PORT) {
+            throw new IllegalArgumentException();
+        }
+
+        this.componentType = componentType;
+    }
+}

http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/6f26290d/nifi-registry-core/nifi-registry-data-model/src/main/java/org/apache/nifi/registry/flow/VersionedRemoteProcessGroup.java
----------------------------------------------------------------------
diff --git a/nifi-registry-core/nifi-registry-data-model/src/main/java/org/apache/nifi/registry/flow/VersionedRemoteProcessGroup.java b/nifi-registry-core/nifi-registry-data-model/src/main/java/org/apache/nifi/registry/flow/VersionedRemoteProcessGroup.java
new file mode 100644
index 0000000..834afac
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-data-model/src/main/java/org/apache/nifi/registry/flow/VersionedRemoteProcessGroup.java
@@ -0,0 +1,161 @@
+/*
+ * 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.flow;
+
+import io.swagger.annotations.ApiModelProperty;
+import org.apache.commons.lang3.StringUtils;
+
+import java.util.Set;
+
+public class VersionedRemoteProcessGroup extends VersionedComponent {
+    private String targetUri;
+    private String targetUris;
+
+    private String communicationsTimeout;
+    private String yieldDuration;
+    private String transportProtocol;
+    private String localNetworkInterface;
+    private String proxyHost;
+    private Integer proxyPort;
+    private String proxyUser;
+
+    private Set<VersionedRemoteGroupPort> inputPorts;
+    private Set<VersionedRemoteGroupPort> outputPorts;
+
+
+    @Deprecated
+    @ApiModelProperty(
+            value = "[DEPRECATED] The target URI of the remote process group." +
+                    " If target uri is not set, but uris are set, then returns the first uri in the uris." +
+                    " If neither target uri nor uris are set, then returns null.",
+            notes = "This field is deprecated and will be removed in version 1.x of NiFi Registry." +
+                    " Please migrate to using targetUris only.")
+    public String getTargetUri() {
+
+        if (!StringUtils.isEmpty(targetUri)) {
+            return targetUri;
+        }
+        return !StringUtils.isEmpty(targetUris) ? targetUris.split(",", 2)[0] : null;
+
+    }
+
+    public void setTargetUri(final String targetUri) {
+        this.targetUri = targetUri;
+    }
+
+    @ApiModelProperty(
+            value = "The target URIs of the remote process group." +
+                    " If target uris is not set but target uri is set, then returns the single target uri." +
+                    " If neither target uris nor target uri is set, then returns null.")
+    public String getTargetUris() {
+
+        if (!StringUtils.isEmpty(targetUris)) {
+            return targetUris;
+        }
+        return !StringUtils.isEmpty(targetUri) ? targetUri : null;
+
+    }
+
+    public void setTargetUris(String targetUris) {
+        this.targetUris = targetUris;
+    }
+
+    @ApiModelProperty("The time period used for the timeout when communicating with the target.")
+    public String getCommunicationsTimeout() {
+        return communicationsTimeout;
+    }
+
+    public void setCommunicationsTimeout(String communicationsTimeout) {
+        this.communicationsTimeout = communicationsTimeout;
+    }
+
+    @ApiModelProperty("When yielding, this amount of time must elapse before the remote process group is scheduled again.")
+    public String getYieldDuration() {
+        return yieldDuration;
+    }
+
+    public void setYieldDuration(String yieldDuration) {
+        this.yieldDuration = yieldDuration;
+    }
+
+    @ApiModelProperty(value = "The Transport Protocol that is used for Site-to-Site communications", allowableValues = "RAW, HTTP")
+    public String getTransportProtocol() {
+        return transportProtocol;
+    }
+
+    public void setTransportProtocol(String transportProtocol) {
+        this.transportProtocol = transportProtocol;
+    }
+
+    @ApiModelProperty("A Set of Input Ports that can be connected to, in order to send data to the remote NiFi instance")
+    public Set<VersionedRemoteGroupPort> getInputPorts() {
+        return inputPorts;
+    }
+
+    public void setInputPorts(Set<VersionedRemoteGroupPort> inputPorts) {
+        this.inputPorts = inputPorts;
+    }
+
+    @ApiModelProperty("A Set of Output Ports that can be connected to, in order to pull data from the remote NiFi instance")
+    public Set<VersionedRemoteGroupPort> getOutputPorts() {
+        return outputPorts;
+    }
+
+    public void setOutputPorts(Set<VersionedRemoteGroupPort> outputPorts) {
+        this.outputPorts = outputPorts;
+    }
+
+
+    @ApiModelProperty("The local network interface to send/receive data. If not specified, any local address is used. If clustered, all nodes must have an interface with this identifier.")
+    public String getLocalNetworkInterface() {
+        return localNetworkInterface;
+    }
+
+    public void setLocalNetworkInterface(String localNetworkInterface) {
+        this.localNetworkInterface = localNetworkInterface;
+    }
+
+    public String getProxyHost() {
+        return proxyHost;
+    }
+
+    public void setProxyHost(String proxyHost) {
+        this.proxyHost = proxyHost;
+    }
+
+    public Integer getProxyPort() {
+        return proxyPort;
+    }
+
+    public void setProxyPort(Integer proxyPort) {
+        this.proxyPort = proxyPort;
+    }
+
+    public String getProxyUser() {
+        return proxyUser;
+    }
+
+    public void setProxyUser(String proxyUser) {
+        this.proxyUser = proxyUser;
+    }
+
+    @Override
+    public ComponentType getComponentType() {
+        return ComponentType.REMOTE_PROCESS_GROUP;
+    }
+}

http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/6f26290d/nifi-registry-core/nifi-registry-data-model/src/main/java/org/apache/nifi/registry/link/LinkAdapter.java
----------------------------------------------------------------------
diff --git a/nifi-registry-core/nifi-registry-data-model/src/main/java/org/apache/nifi/registry/link/LinkAdapter.java b/nifi-registry-core/nifi-registry-data-model/src/main/java/org/apache/nifi/registry/link/LinkAdapter.java
new file mode 100644
index 0000000..c3dae90
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-data-model/src/main/java/org/apache/nifi/registry/link/LinkAdapter.java
@@ -0,0 +1,67 @@
+/*
+ * 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.link;
+
+import javax.ws.rs.core.Link;
+import javax.xml.bind.annotation.adapters.XmlAdapter;
+import javax.xml.namespace.QName;
+import java.util.Map;
+
+/**
+ * This class is a modified version of Jersey's Link.JaxbAdapter that adds protection against nulls.
+ */
+public class LinkAdapter extends XmlAdapter<Link.JaxbLink, Link> {
+
+    /**
+     * Convert a {@link Link.JaxbLink} into a {@link Link}.
+     *
+     * @param v instance of type {@link Link.JaxbLink}.
+     * @return mapped instance of type {@link Link.JaxbLink}
+     */
+    @Override
+    public Link unmarshal(Link.JaxbLink v) {
+        if (v == null) {
+            return null;
+        }
+
+        Link.Builder lb = Link.fromUri(v.getUri());
+        for (Map.Entry<QName, Object> e : v.getParams().entrySet()) {
+            lb.param(e.getKey().getLocalPart(), e.getValue().toString());
+        }
+        return lb.build();
+    }
+
+    /**
+     * Convert a {@link Link} into a {@link Link.JaxbLink}.
+     *
+     * @param v instance of type {@link Link}.
+     * @return mapped instance of type {@link Link.JaxbLink}.
+     */
+    @Override
+    public Link.JaxbLink marshal(Link v) {
+        if (v == null) {
+           return null;
+        }
+
+        Link.JaxbLink jl = new Link.JaxbLink(v.getUri());
+        for (Map.Entry<String, String> e : v.getParams().entrySet()) {
+            final String name = e.getKey();
+            jl.getParams().put(new QName("", name), e.getValue());
+        }
+        return jl;
+    }
+}

http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/6f26290d/nifi-registry-core/nifi-registry-data-model/src/main/java/org/apache/nifi/registry/link/LinkableEntity.java
----------------------------------------------------------------------
diff --git a/nifi-registry-core/nifi-registry-data-model/src/main/java/org/apache/nifi/registry/link/LinkableEntity.java b/nifi-registry-core/nifi-registry-data-model/src/main/java/org/apache/nifi/registry/link/LinkableEntity.java
new file mode 100644
index 0000000..896e9d3
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-data-model/src/main/java/org/apache/nifi/registry/link/LinkableEntity.java
@@ -0,0 +1,45 @@
+/*
+ * 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.link;
+
+import io.swagger.annotations.ApiModel;
+import io.swagger.annotations.ApiModelProperty;
+
+import javax.ws.rs.core.Link;
+import javax.xml.bind.annotation.XmlElement;
+import javax.xml.bind.annotation.adapters.XmlJavaTypeAdapter;
+
+/**
+ * Base classes for domain objects that want to provide a hypermedia link.
+ */
+@ApiModel("linkableEntity")
+public abstract class LinkableEntity {
+
+    private Link link;
+
+    @XmlElement
+    @XmlJavaTypeAdapter(LinkAdapter.class)
+    @ApiModelProperty(value = "An WebLink to this entity.", readOnly = true)
+    public Link getLink() {
+        return link;
+    }
+
+    public void setLink(Link link) {
+        this.link = link;
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/6f26290d/nifi-registry-core/nifi-registry-data-model/src/main/java/org/apache/nifi/registry/params/SortOrder.java
----------------------------------------------------------------------
diff --git a/nifi-registry-core/nifi-registry-data-model/src/main/java/org/apache/nifi/registry/params/SortOrder.java b/nifi-registry-core/nifi-registry-data-model/src/main/java/org/apache/nifi/registry/params/SortOrder.java
new file mode 100644
index 0000000..8e571de
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-data-model/src/main/java/org/apache/nifi/registry/params/SortOrder.java
@@ -0,0 +1,47 @@
+/*
+ * 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.params;
+
+public enum SortOrder {
+
+    ASC("asc"),
+
+    DESC("desc");
+
+    private final String name;
+
+    SortOrder(final String name) {
+        this.name = name;
+    }
+
+    public String getName() {
+        return name;
+    }
+
+    public static SortOrder fromString(String order) {
+        if (ASC.getName().equals(order)) {
+            return  ASC;
+        }
+
+        if (DESC.getName().equals(order)) {
+            return DESC;
+        }
+
+        throw new IllegalArgumentException("Unknown Sort Order: " + order);
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/6f26290d/nifi-registry-core/nifi-registry-data-model/src/main/java/org/apache/nifi/registry/params/SortParameter.java
----------------------------------------------------------------------
diff --git a/nifi-registry-core/nifi-registry-data-model/src/main/java/org/apache/nifi/registry/params/SortParameter.java b/nifi-registry-core/nifi-registry-data-model/src/main/java/org/apache/nifi/registry/params/SortParameter.java
new file mode 100644
index 0000000..d4a1add
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-data-model/src/main/java/org/apache/nifi/registry/params/SortParameter.java
@@ -0,0 +1,85 @@
+/*
+ * 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.params;
+
+/**
+ * Sort parameter made up of a field and a sort order.
+ */
+public class SortParameter {
+
+    public static final String API_PARAM_DESCRIPTION =
+            "Apply client-defined sorting to the resulting list of resource objects. " +
+                    "The value of this parameter should be in the format \"field:order\". " +
+                    "Valid values for 'field' can be discovered via GET :resourceURI/fields. " +
+                    "Valid values for 'order' are 'ASC' (ascending order), 'DESC' (descending order).";
+
+    private final String fieldName;
+
+    private final SortOrder order;
+
+    public SortParameter(final String fieldName, final SortOrder order) {
+        this.fieldName = fieldName;
+        this.order = order;
+
+        if (this.fieldName == null) {
+            throw new IllegalStateException("Field Name cannot be null");
+        }
+
+        if (this.fieldName.trim().isEmpty()) {
+            throw new IllegalStateException("Field Name cannot be blank");
+        }
+
+        if (this.order == null) {
+            throw new IllegalStateException("Order cannot be null");
+        }
+    }
+
+    public String getFieldName() {
+        return fieldName;
+    }
+
+    public SortOrder getOrder() {
+        return order;
+    }
+
+    /**
+     * Parses a sorting expression of the form field:order.
+     *
+     * @param sortExpression the expression
+     * @return the Sort instance
+     */
+    public static SortParameter fromString(final String sortExpression) {
+        if (sortExpression == null) {
+            throw new IllegalArgumentException("Sort cannot be null");
+        }
+
+        final String[] sortParts = sortExpression.split("[:]");
+        if (sortParts.length != 2) {
+            throw new IllegalArgumentException("Sort must be in the form field:order");
+        }
+
+        final String fieldName = sortParts[0];
+        final SortOrder order = SortOrder.fromString(sortParts[1]);
+
+        return new SortParameter(fieldName, order);
+    }
+
+    @Override
+    public String toString() {
+        return fieldName + ":" + order.getName();
+    }
+}

http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/6f26290d/nifi-registry-core/nifi-registry-data-model/src/test/java/org/apache/nifi/registry/flow/TestVersionedRemoteProcessGroup.java
----------------------------------------------------------------------
diff --git a/nifi-registry-core/nifi-registry-data-model/src/test/java/org/apache/nifi/registry/flow/TestVersionedRemoteProcessGroup.java b/nifi-registry-core/nifi-registry-data-model/src/test/java/org/apache/nifi/registry/flow/TestVersionedRemoteProcessGroup.java
new file mode 100644
index 0000000..bbe6724
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-data-model/src/test/java/org/apache/nifi/registry/flow/TestVersionedRemoteProcessGroup.java
@@ -0,0 +1,102 @@
+/*
+ * 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.flow;
+
+import org.junit.Test;
+
+import static org.junit.Assert.assertEquals;
+
+public class TestVersionedRemoteProcessGroup {
+
+    @Test
+    public void testGetTargetUriAndGetTargetUris() {
+
+        VersionedRemoteProcessGroup vRPG = new VersionedRemoteProcessGroup();
+
+
+        /* targetUri is null, targetUris varies */
+
+        vRPG.setTargetUri(null);
+        vRPG.setTargetUris(null);
+        assertEquals(null, vRPG.getTargetUri());
+        assertEquals(null, vRPG.getTargetUris());
+
+        vRPG.setTargetUri(null);
+        vRPG.setTargetUris("");
+        assertEquals(null, vRPG.getTargetUri());
+        assertEquals(null, vRPG.getTargetUris());
+
+        vRPG.setTargetUri(null);
+        vRPG.setTargetUris("uri-2");
+        //assertEquals("uri-2", vRPG.getTargetUri());
+        assertEquals("uri-2", vRPG.getTargetUris());
+
+        vRPG.setTargetUri(null);
+        vRPG.setTargetUris("uri-2,uri-3");
+        assertEquals("uri-2", vRPG.getTargetUri());
+        assertEquals("uri-2,uri-3", vRPG.getTargetUris());
+
+
+        /* targetUri is empty, targetUris varies */
+
+        vRPG.setTargetUri("");
+        vRPG.setTargetUris(null);
+        assertEquals(null, vRPG.getTargetUri());
+        assertEquals(null, vRPG.getTargetUris());
+
+        vRPG.setTargetUri("");
+        vRPG.setTargetUris("");
+        assertEquals(null, vRPG.getTargetUri());
+        assertEquals(null, vRPG.getTargetUris());
+
+        vRPG.setTargetUri("");
+        vRPG.setTargetUris("uri-2");
+        assertEquals("uri-2", vRPG.getTargetUri());
+        assertEquals("uri-2", vRPG.getTargetUris());
+
+        vRPG.setTargetUri("");
+        vRPG.setTargetUris("uri-2,uri-3");
+        assertEquals("uri-2", vRPG.getTargetUri());
+        assertEquals("uri-2,uri-3", vRPG.getTargetUris());
+
+
+        /* targetUri is set, targetUris varies */
+
+        vRPG.setTargetUri("uri-1");
+        vRPG.setTargetUris(null);
+        assertEquals("uri-1", vRPG.getTargetUri());
+        assertEquals("uri-1", vRPG.getTargetUris());
+
+        vRPG.setTargetUri("uri-1");
+        vRPG.setTargetUris("");
+        assertEquals("uri-1", vRPG.getTargetUri());
+        assertEquals("uri-1", vRPG.getTargetUris());
+
+        vRPG.setTargetUri("uri-1");
+        vRPG.setTargetUris("uri-2");
+        assertEquals("uri-1", vRPG.getTargetUri());
+        assertEquals("uri-2", vRPG.getTargetUris());
+
+        vRPG.setTargetUri("uri-1");
+        vRPG.setTargetUris("uri-2,uri-3");
+        assertEquals("uri-1", vRPG.getTargetUri());
+        assertEquals("uri-2,uri-3", vRPG.getTargetUris());
+
+    }
+
+
+}

http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/6f26290d/nifi-registry-core/nifi-registry-docker/dockerhub/.dockerignore
----------------------------------------------------------------------
diff --git a/nifi-registry-core/nifi-registry-docker/dockerhub/.dockerignore b/nifi-registry-core/nifi-registry-docker/dockerhub/.dockerignore
new file mode 100644
index 0000000..30a2650
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-docker/dockerhub/.dockerignore
@@ -0,0 +1,19 @@
+# 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.
+
+# Place files you want to exclude from the docker build here similar to .gitignore https://docs.docker.com/engine/reference/builder/#dockerignore-file
+DockerBuild.sh
+DockerRun.sh
+DockerImage.txt
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/6f26290d/nifi-registry-core/nifi-registry-docker/dockerhub/DockerBuild.sh
----------------------------------------------------------------------
diff --git a/nifi-registry-core/nifi-registry-docker/dockerhub/DockerBuild.sh b/nifi-registry-core/nifi-registry-docker/dockerhub/DockerBuild.sh
new file mode 100755
index 0000000..c7e01e3
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-docker/dockerhub/DockerBuild.sh
@@ -0,0 +1,36 @@
+# 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.
+
+#!/bin/bash
+
+DOCKER_UID=1000
+if [ -n "$1" ]; then
+  DOCKER_UID="$1"
+fi
+
+DOCKER_GID=1000
+if [ -n "$2" ]; then
+  DOCKER_GID="$2"
+fi
+
+MIRROR=https://archive.apache.org/dist
+if [ -n "$3" ]; then
+  MIRROR="$3"
+fi
+
+DOCKER_IMAGE="$(egrep -v '(^#|^\s*$|^\s*\t*#)' DockerImage.txt)"
+NIFI_REGISTRY_IMAGE_VERSION="$(echo $DOCKER_IMAGE | cut -d : -f 2)"
+echo "Building NiFi-Registry Image: '$DOCKER_IMAGE' Version: NIFI_REGISTRY_IMAGE_VERSION Mirror: $MIRROR"
+docker build --build-arg UID="$DOCKER_UID" --build-arg GID="$DOCKER_GID" --build-arg NIFI_REGISTRY_VERSION="$NIFI_REGISTRY_IMAGE_VERSION" --build-arg MIRROR="$MIRROR" -t $DOCKER_IMAGE .

http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/6f26290d/nifi-registry-core/nifi-registry-docker/dockerhub/DockerImage.txt
----------------------------------------------------------------------
diff --git a/nifi-registry-core/nifi-registry-docker/dockerhub/DockerImage.txt b/nifi-registry-core/nifi-registry-docker/dockerhub/DockerImage.txt
new file mode 100644
index 0000000..07d6e8d
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-docker/dockerhub/DockerImage.txt
@@ -0,0 +1,16 @@
+# 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.
+
+apache/nifi-registry:0.3.0

http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/6f26290d/nifi-registry-core/nifi-registry-docker/dockerhub/Dockerfile
----------------------------------------------------------------------
diff --git a/nifi-registry-core/nifi-registry-docker/dockerhub/Dockerfile b/nifi-registry-core/nifi-registry-docker/dockerhub/Dockerfile
new file mode 100644
index 0000000..5d78ca0
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-docker/dockerhub/Dockerfile
@@ -0,0 +1,56 @@
+# 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.
+#
+
+FROM openjdk:8-jdk-slim
+LABEL maintainer="Apache NiFi <de...@nifi.apache.org>"
+LABEL site="https://nifi.apache.org"
+
+ARG UID=1000
+ARG GID=1000
+ARG NIFI_REGISTRY_VERSION=0.3.0
+ARG MIRROR=https://archive.apache.org/dist
+
+ENV NIFI_REGISTRY_BASE_DIR /opt/nifi-registry
+ENV NIFI_REGISTRY_HOME=${NIFI_REGISTRY_BASE_DIR}/nifi-registry-${NIFI_REGISTRY_VERSION} \
+    NIFI_REGISTRY_BINARY_URL=nifi/nifi-registry/nifi-registry-${NIFI_REGISTRY_VERSION}/nifi-registry-${NIFI_REGISTRY_VERSION}-bin.tar.gz
+
+ADD sh/ ${NIFI_REGISTRY_BASE_DIR}/scripts/
+
+# Setup NiFi-Registry user
+RUN groupadd -g ${GID} nifi || groupmod -n nifi `getent group ${GID} | cut -d: -f1` \
+    && useradd --shell /bin/bash -u ${UID} -g ${GID} -m nifi \
+    && chown -R nifi:nifi ${NIFI_REGISTRY_BASE_DIR} \
+    && apt-get update -y \
+    && apt-get install -y curl jq xmlstarlet
+
+USER nifi
+
+# Download, validate, and expand Apache NiFi-Registry binary.
+RUN curl -fSL ${MIRROR}/${NIFI_REGISTRY_BINARY_URL} -o ${NIFI_REGISTRY_BASE_DIR}/nifi-registry-${NIFI_REGISTRY_VERSION}-bin.tar.gz \
+    && echo "$(curl ${MIRROR}/${NIFI_REGISTRY_BINARY_URL}.sha256) *${NIFI_REGISTRY_BASE_DIR}/nifi-registry-${NIFI_REGISTRY_VERSION}-bin.tar.gz" | sha256sum -c - \
+    && tar -xvzf ${NIFI_REGISTRY_BASE_DIR}/nifi-registry-${NIFI_REGISTRY_VERSION}-bin.tar.gz -C ${NIFI_REGISTRY_BASE_DIR} \
+    && rm ${NIFI_REGISTRY_BASE_DIR}/nifi-registry-${NIFI_REGISTRY_VERSION}-bin.tar.gz \
+    && chown -R nifi:nifi ${NIFI_REGISTRY_HOME}
+
+# Web HTTP(s) ports
+EXPOSE 18080 18443
+
+WORKDIR ${NIFI_REGISTRY_HOME}
+
+# Apply configuration and start NiFi Registry
+CMD ${NIFI_REGISTRY_BASE_DIR}/scripts/start.sh


[37/51] [partial] nifi-registry git commit: NIFIREG-201 Refactoring project structure to better isolate extensions

Posted by kd...@apache.org.
http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/6f26290d/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/security/authorization/AbstractPolicyBasedAuthorizer.java
----------------------------------------------------------------------
diff --git a/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/security/authorization/AbstractPolicyBasedAuthorizer.java b/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/security/authorization/AbstractPolicyBasedAuthorizer.java
new file mode 100644
index 0000000..3e721de
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/security/authorization/AbstractPolicyBasedAuthorizer.java
@@ -0,0 +1,824 @@
+/*
+ * 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.security.authorization;
+
+import org.apache.nifi.registry.security.authorization.exception.AuthorizationAccessException;
+import org.apache.nifi.registry.security.authorization.exception.UninheritableAuthorizationsException;
+import org.apache.nifi.registry.security.exception.SecurityProviderCreationException;
+import org.apache.nifi.registry.security.exception.SecurityProviderDestructionException;
+import org.w3c.dom.Document;
+import org.w3c.dom.Element;
+import org.w3c.dom.Node;
+import org.w3c.dom.NodeList;
+import org.xml.sax.SAXException;
+
+import javax.xml.parsers.DocumentBuilder;
+import javax.xml.parsers.DocumentBuilderFactory;
+import javax.xml.parsers.ParserConfigurationException;
+import javax.xml.stream.XMLOutputFactory;
+import javax.xml.stream.XMLStreamException;
+import javax.xml.stream.XMLStreamWriter;
+import java.io.ByteArrayInputStream;
+import java.io.IOException;
+import java.io.StringWriter;
+import java.nio.charset.StandardCharsets;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.List;
+import java.util.Set;
+
+/**
+ * An Authorizer that provides management of users, groups, and policies.
+ */
+public abstract class AbstractPolicyBasedAuthorizer implements ManagedAuthorizer {
+
+    static final DocumentBuilderFactory DOCUMENT_BUILDER_FACTORY = DocumentBuilderFactory.newInstance();
+    static final XMLOutputFactory XML_OUTPUT_FACTORY = XMLOutputFactory.newInstance();
+
+    static final String USER_ELEMENT = "user";
+    static final String GROUP_USER_ELEMENT = "groupUser";
+    static final String GROUP_ELEMENT = "group";
+    static final String POLICY_ELEMENT = "policy";
+    static final String POLICY_USER_ELEMENT = "policyUser";
+    static final String POLICY_GROUP_ELEMENT = "policyGroup";
+    static final String IDENTIFIER_ATTR = "identifier";
+    static final String IDENTITY_ATTR = "identity";
+    static final String NAME_ATTR = "name";
+    static final String RESOURCE_ATTR = "resource";
+    static final String ACTIONS_ATTR = "actions";
+
+    @Override
+    public final void onConfigured(final AuthorizerConfigurationContext configurationContext) throws SecurityProviderCreationException {
+        doOnConfigured(configurationContext);
+    }
+
+    /**
+     * Allows sub-classes to take action when onConfigured is called.
+     *
+     * @param configurationContext the configuration context
+     * @throws SecurityProviderCreationException if an error occurs during onConfigured process
+     */
+    protected abstract void doOnConfigured(final AuthorizerConfigurationContext configurationContext) throws SecurityProviderCreationException;
+
+    @Override
+    public final AuthorizationResult authorize(AuthorizationRequest request) throws AuthorizationAccessException {
+        final UsersAndAccessPolicies usersAndAccessPolicies = getUsersAndAccessPolicies();
+        final String resourceIdentifier = request.getResource().getIdentifier();
+
+        final AccessPolicy policy = usersAndAccessPolicies.getAccessPolicy(resourceIdentifier, request.getAction());
+        if (policy == null) {
+            return AuthorizationResult.resourceNotFound();
+        }
+
+        final User user = usersAndAccessPolicies.getUser(request.getIdentity());
+        if (user == null) {
+            return AuthorizationResult.denied(String.format("Unknown user with identity '%s'.", request.getIdentity()));
+        }
+
+        final Set<Group> userGroups = usersAndAccessPolicies.getGroups(user.getIdentity());
+        if (policy.getUsers().contains(user.getIdentifier()) || containsGroup(userGroups, policy)) {
+            return AuthorizationResult.approved();
+        }
+
+        return AuthorizationResult.denied(request.getExplanationSupplier().get());
+    }
+
+    /**
+     * Determines if the policy contains one of the user's groups.
+     *
+     * @param userGroups the set of the user's groups
+     * @param policy the policy
+     * @return true if one of the Groups in userGroups is contained in the policy
+     */
+    private boolean containsGroup(final Set<Group> userGroups, final AccessPolicy policy) {
+        if (userGroups.isEmpty() || policy.getGroups().isEmpty()) {
+            return false;
+        }
+
+        for (Group userGroup : userGroups) {
+            if (policy.getGroups().contains(userGroup.getIdentifier())) {
+                return true;
+            }
+        }
+
+        return false;
+    }
+
+    /**
+     * Adds a new group.
+     *
+     * @param group the Group to add
+     * @return the added Group
+     * @throws AuthorizationAccessException if there was an unexpected error performing the operation
+     * @throws IllegalStateException if a group with the same name already exists
+     */
+    public final synchronized Group addGroup(Group group) throws AuthorizationAccessException {
+        return doAddGroup(group);
+    }
+
+    /**
+     * Adds a new group.
+     *
+     * @param group the Group to add
+     * @return the added Group
+     * @throws AuthorizationAccessException if there was an unexpected error performing the operation
+     */
+    public abstract Group doAddGroup(Group group) throws AuthorizationAccessException;
+
+    /**
+     * Retrieves a Group by id.
+     *
+     * @param identifier the identifier of the Group to retrieve
+     * @return the Group with the given identifier, or null if no matching group was found
+     * @throws AuthorizationAccessException if there was an unexpected error performing the operation
+     */
+    public abstract Group getGroup(String identifier) throws AuthorizationAccessException;
+
+    /**
+     * The group represented by the provided instance will be updated based on the provided instance.
+     *
+     * @param group an updated group instance
+     * @return the updated group instance, or null if no matching group was found
+     * @throws AuthorizationAccessException if there was an unexpected error performing the operation
+     * @throws IllegalStateException if there is already a group with the same name
+     */
+    public final synchronized Group updateGroup(Group group) throws AuthorizationAccessException {
+        return doUpdateGroup(group);
+    }
+
+    /**
+     * The group represented by the provided instance will be updated based on the provided instance.
+     *
+     * @param group an updated group instance
+     * @return the updated group instance, or null if no matching group was found
+     * @throws AuthorizationAccessException if there was an unexpected error performing the operation
+     */
+    public abstract Group doUpdateGroup(Group group) throws AuthorizationAccessException;
+
+    /**
+     * Deletes the given group.
+     *
+     * @param group the group to delete
+     * @return the deleted group, or null if no matching group was found
+     * @throws AuthorizationAccessException if there was an unexpected error performing the operation
+     */
+    public abstract Group deleteGroup(Group group) throws AuthorizationAccessException;
+
+    /**
+     * Deletes the group with the given identifier.
+     *
+     * @param groupIdentifier the id of the group to delete
+     * @return the deleted group, or null if no matching group was found
+     * @throws AuthorizationAccessException if there was an unexpected error performing the operation
+     */
+    public abstract Group deleteGroup(String groupIdentifier) throws AuthorizationAccessException;
+
+    /**
+     * Retrieves all groups.
+     *
+     * @return a list of groups
+     * @throws AuthorizationAccessException if there was an unexpected error performing the operation
+     */
+    public abstract Set<Group> getGroups() throws AuthorizationAccessException;
+
+
+    /**
+     * Adds the given user.
+     *
+     * @param user the user to add
+     * @return the user that was added
+     * @throws AuthorizationAccessException if there was an unexpected error performing the operation
+     * @throws IllegalStateException if there is already a user with the same identity
+     */
+    public final synchronized User addUser(User user) throws AuthorizationAccessException {
+        return doAddUser(user);
+    }
+
+    /**
+     * Adds the given user.
+     *
+     * @param user the user to add
+     * @return the user that was added
+     * @throws AuthorizationAccessException if there was an unexpected error performing the operation
+     */
+    public abstract User doAddUser(User user) throws AuthorizationAccessException;
+
+    /**
+     * Retrieves the user with the given identifier.
+     *
+     * @param identifier the id of the user to retrieve
+     * @return the user with the given id, or null if no matching user was found
+     * @throws AuthorizationAccessException if there was an unexpected error performing the operation
+     */
+    public abstract User getUser(String identifier) throws AuthorizationAccessException;
+
+    /**
+     * Retrieves the user with the given identity.
+     *
+     * @param identity the identity of the user to retrieve
+     * @return the user with the given identity, or null if no matching user was found
+     * @throws AuthorizationAccessException if there was an unexpected error performing the operation
+     */
+    public abstract User getUserByIdentity(String identity) throws AuthorizationAccessException;
+
+    /**
+     * The user represented by the provided instance will be updated based on the provided instance.
+     *
+     * @param user an updated user instance
+     * @return the updated user instance, or null if no matching user was found
+     * @throws AuthorizationAccessException if there was an unexpected error performing the operation
+     * @throws IllegalStateException if there is already a user with the same identity
+     */
+    public final synchronized User updateUser(final User user) throws AuthorizationAccessException {
+        return doUpdateUser(user);
+    }
+
+    /**
+     * The user represented by the provided instance will be updated based on the provided instance.
+     *
+     * @param user an updated user instance
+     * @return the updated user instance, or null if no matching user was found
+     * @throws AuthorizationAccessException if there was an unexpected error performing the operation
+     */
+    public abstract User doUpdateUser(User user) throws AuthorizationAccessException;
+
+    /**
+     * Deletes the given user.
+     *
+     * @param user the user to delete
+     * @return the user that was deleted, or null if no matching user was found
+     * @throws AuthorizationAccessException if there was an unexpected error performing the operation
+     */
+    public abstract User deleteUser(User user) throws AuthorizationAccessException;
+
+    /**
+     * Deletes the user with the given id.
+     *
+     * @param userIdentifier the identifier of the user to delete
+     * @return the user that was deleted, or null if no matching user was found
+     * @throws AuthorizationAccessException if there was an unexpected error performing the operation
+     */
+    public abstract User deleteUser(String userIdentifier) throws AuthorizationAccessException;
+
+    /**
+     * Retrieves all users.
+     *
+     * @return a list of users
+     * @throws AuthorizationAccessException if there was an unexpected error performing the operation
+     */
+    public abstract Set<User> getUsers() throws AuthorizationAccessException;
+
+    /**
+     * Adds the given policy ensuring that multiple policies can not be added for the same resource and action.
+     *
+     * @param accessPolicy the policy to add
+     * @return the policy that was added
+     * @throws AuthorizationAccessException if there was an unexpected error performing the operation
+     */
+    public final synchronized AccessPolicy addAccessPolicy(AccessPolicy accessPolicy) throws AuthorizationAccessException {
+        return doAddAccessPolicy(accessPolicy);
+    }
+
+    /**
+     * Adds the given policy.
+     *
+     * @param accessPolicy the policy to add
+     * @return the policy that was added
+     * @throws AuthorizationAccessException if there was an unexpected error performing the operation
+     */
+    protected abstract AccessPolicy doAddAccessPolicy(AccessPolicy accessPolicy) throws AuthorizationAccessException;
+
+    /**
+     * Retrieves the policy with the given identifier.
+     *
+     * @param identifier the id of the policy to retrieve
+     * @return the policy with the given id, or null if no matching policy exists
+     * @throws AuthorizationAccessException if there was an unexpected error performing the operation
+     */
+    public abstract AccessPolicy getAccessPolicy(String identifier) throws AuthorizationAccessException;
+
+    /**
+     * The policy represented by the provided instance will be updated based on the provided instance.
+     *
+     * @param accessPolicy an updated policy
+     * @return the updated policy, or null if no matching policy was found
+     * @throws AuthorizationAccessException if there was an unexpected error performing the operation
+     */
+    public abstract AccessPolicy updateAccessPolicy(AccessPolicy accessPolicy) throws AuthorizationAccessException;
+
+    /**
+     * Deletes the given policy.
+     *
+     * @param policy the policy to delete
+     * @return the deleted policy, or null if no matching policy was found
+     * @throws AuthorizationAccessException if there was an unexpected error performing the operation
+     */
+    public abstract AccessPolicy deleteAccessPolicy(AccessPolicy policy) throws AuthorizationAccessException;
+
+    /**
+     * Deletes the policy with the given id.
+     *
+     * @param policyIdentifier the id of the policy to delete
+     * @return the deleted policy, or null if no matching policy was found
+     * @throws AuthorizationAccessException if there was an unexpected error performing the operation
+     */
+    public abstract AccessPolicy deleteAccessPolicy(String policyIdentifier) throws AuthorizationAccessException;
+
+    /**
+     * Retrieves all access policies.
+     *
+     * @return a list of policies
+     * @throws AuthorizationAccessException if there was an unexpected error performing the operation
+     */
+    public abstract Set<AccessPolicy> getAccessPolicies() throws AuthorizationAccessException;
+
+    /**
+     * Returns the UserAccessPolicies instance.
+     *
+     * @return the UserAccessPolicies instance
+     * @throws AuthorizationAccessException if there was an unexpected error performing the operation
+     */
+    public abstract UsersAndAccessPolicies getUsersAndAccessPolicies() throws AuthorizationAccessException;
+
+    /**
+     * Returns whether the proposed fingerprint is inheritable.
+     *
+     * @param proposedFingerprint the proposed fingerprint
+     * @throws AuthorizationAccessException if there was an unexpected error performing the operation
+     * @throws UninheritableAuthorizationsException if the proposed fingerprint was uninheritable
+     */
+    @Override
+    public final void checkInheritability(String proposedFingerprint) throws AuthorizationAccessException, UninheritableAuthorizationsException {
+        try {
+            // ensure we understand the proposed fingerprint
+            parsePoliciesUsersAndGroups(proposedFingerprint);
+        } catch (final AuthorizationAccessException e) {
+            throw new UninheritableAuthorizationsException("Unable to parse proposed fingerprint: " + e);
+        }
+
+        final List<User> users = getSortedUsers();
+        final List<Group> groups = getSortedGroups();
+        final List<AccessPolicy> accessPolicies = getSortedAccessPolicies();
+
+        // ensure we're in a state to inherit
+        if (!users.isEmpty() || !groups.isEmpty() || !accessPolicies.isEmpty()) {
+            throw new UninheritableAuthorizationsException("Proposed fingerprint is not inheritable because the current Authorizations is not empty..");
+        }
+    }
+
+    /**
+     * Parses the fingerprint and adds any users, groups, and policies to the current Authorizer.
+     *
+     * @param fingerprint the fingerprint that was obtained from calling getFingerprint() on another Authorizer.
+     */
+    @Override
+    public final void inheritFingerprint(final String fingerprint) throws AuthorizationAccessException {
+        if (fingerprint == null || fingerprint.trim().isEmpty()) {
+            return;
+        }
+
+        final PoliciesUsersAndGroups policiesUsersAndGroups = parsePoliciesUsersAndGroups(fingerprint);
+        policiesUsersAndGroups.getUsers().forEach(user -> addUser(user));
+        policiesUsersAndGroups.getGroups().forEach(group -> addGroup(group));
+        policiesUsersAndGroups.getAccessPolicies().forEach(policy -> addAccessPolicy(policy));
+    }
+
+    private PoliciesUsersAndGroups parsePoliciesUsersAndGroups(final String fingerprint) {
+        final List<AccessPolicy> accessPolicies = new ArrayList<>();
+        final List<User> users = new ArrayList<>();
+        final List<Group> groups = new ArrayList<>();
+
+        final byte[] fingerprintBytes = fingerprint.getBytes(StandardCharsets.UTF_8);
+        try (final ByteArrayInputStream in = new ByteArrayInputStream(fingerprintBytes)) {
+            final DocumentBuilder docBuilder = DOCUMENT_BUILDER_FACTORY.newDocumentBuilder();
+            final Document document = docBuilder.parse(in);
+            final Element rootElement = document.getDocumentElement();
+
+            // parse all the users and add them to the current authorizer
+            NodeList userNodes = rootElement.getElementsByTagName(USER_ELEMENT);
+            for (int i=0; i < userNodes.getLength(); i++) {
+                Node userNode = userNodes.item(i);
+                users.add(parseUser((Element) userNode));
+            }
+
+            // parse all the groups and add them to the current authorizer
+            NodeList groupNodes = rootElement.getElementsByTagName(GROUP_ELEMENT);
+            for (int i=0; i < groupNodes.getLength(); i++) {
+                Node groupNode = groupNodes.item(i);
+                groups.add(parseGroup((Element) groupNode));
+            }
+
+            // parse all the policies and add them to the current authorizer
+            NodeList policyNodes = rootElement.getElementsByTagName(POLICY_ELEMENT);
+            for (int i=0; i < policyNodes.getLength(); i++) {
+                Node policyNode = policyNodes.item(i);
+                accessPolicies.add(parsePolicy((Element) policyNode));
+            }
+        } catch (SAXException | ParserConfigurationException | IOException e) {
+            throw new AuthorizationAccessException("Unable to parse fingerprint", e);
+        }
+
+        return new PoliciesUsersAndGroups(accessPolicies, users, groups);
+    }
+
+    private User parseUser(final Element element) {
+        final User.Builder builder = new User.Builder()
+                .identifier(element.getAttribute(IDENTIFIER_ATTR))
+                .identity(element.getAttribute(IDENTITY_ATTR));
+
+        return builder.build();
+    }
+
+    private Group parseGroup(final Element element) {
+        final Group.Builder builder = new Group.Builder()
+                .identifier(element.getAttribute(IDENTIFIER_ATTR))
+                .name(element.getAttribute(NAME_ATTR));
+
+        NodeList groupUsers = element.getElementsByTagName(GROUP_USER_ELEMENT);
+        for (int i=0; i < groupUsers.getLength(); i++) {
+            Element groupUserNode = (Element) groupUsers.item(i);
+            builder.addUser(groupUserNode.getAttribute(IDENTIFIER_ATTR));
+        }
+
+        return builder.build();
+    }
+
+    private AccessPolicy parsePolicy(final Element element) {
+        final AccessPolicy.Builder builder = new AccessPolicy.Builder()
+                .identifier(element.getAttribute(IDENTIFIER_ATTR))
+                .resource(element.getAttribute(RESOURCE_ATTR));
+
+        final String actions = element.getAttribute(ACTIONS_ATTR);
+        if (actions.equals(RequestAction.READ.name())) {
+            builder.action(RequestAction.READ);
+        } else if (actions.equals(RequestAction.WRITE.name())) {
+            builder.action(RequestAction.WRITE);
+        } else if (actions.equals(RequestAction.DELETE.name())) {
+            builder.action(RequestAction.DELETE);
+        } else {
+            throw new IllegalStateException("Unknown Policy Action: " + actions);
+        }
+
+        NodeList policyUsers = element.getElementsByTagName(POLICY_USER_ELEMENT);
+        for (int i=0; i < policyUsers.getLength(); i++) {
+            Element policyUserNode = (Element) policyUsers.item(i);
+            builder.addUser(policyUserNode.getAttribute(IDENTIFIER_ATTR));
+        }
+
+        NodeList policyGroups = element.getElementsByTagName(POLICY_GROUP_ELEMENT);
+        for (int i=0; i < policyGroups.getLength(); i++) {
+            Element policyGroupNode = (Element) policyGroups.item(i);
+            builder.addGroup(policyGroupNode.getAttribute(IDENTIFIER_ATTR));
+        }
+
+        return builder.build();
+    }
+
+    @Override
+    public final AccessPolicyProvider getAccessPolicyProvider() {
+        return new ConfigurableAccessPolicyProvider() {
+            @Override
+            public Set<AccessPolicy> getAccessPolicies() throws AuthorizationAccessException {
+                return AbstractPolicyBasedAuthorizer.this.getAccessPolicies();
+            }
+
+            @Override
+            public AccessPolicy getAccessPolicy(String identifier) throws AuthorizationAccessException {
+                return AbstractPolicyBasedAuthorizer.this.getAccessPolicy(identifier);
+            }
+
+            @Override
+            public AccessPolicy addAccessPolicy(AccessPolicy accessPolicy) throws AuthorizationAccessException {
+                return AbstractPolicyBasedAuthorizer.this.addAccessPolicy(accessPolicy);
+            }
+
+            @Override
+            public AccessPolicy updateAccessPolicy(AccessPolicy accessPolicy) throws AuthorizationAccessException {
+                return AbstractPolicyBasedAuthorizer.this.updateAccessPolicy(accessPolicy);
+            }
+
+            @Override
+            public AccessPolicy deleteAccessPolicy(AccessPolicy accessPolicy) throws AuthorizationAccessException {
+                return AbstractPolicyBasedAuthorizer.this.deleteAccessPolicy(accessPolicy);
+            }
+
+            @Override
+            public AccessPolicy deleteAccessPolicy(String accessPolicyIdentifier) throws AuthorizationAccessException {
+                return AbstractPolicyBasedAuthorizer.this.deleteAccessPolicy(accessPolicyIdentifier);
+            }
+
+            @Override
+            public AccessPolicy getAccessPolicy(String resourceIdentifier, RequestAction action) throws AuthorizationAccessException {
+                final UsersAndAccessPolicies usersAndAccessPolicies = AbstractPolicyBasedAuthorizer.this.getUsersAndAccessPolicies();
+                return usersAndAccessPolicies.getAccessPolicy(resourceIdentifier, action);
+            }
+
+            @Override
+            public String getFingerprint() throws AuthorizationAccessException {
+                // fingerprint is managed by the encapsulating class
+                throw new UnsupportedOperationException();
+            }
+
+            @Override
+            public void inheritFingerprint(String fingerprint) throws AuthorizationAccessException {
+                // fingerprint is managed by the encapsulating class
+                throw new UnsupportedOperationException();
+            }
+
+            @Override
+            public void checkInheritability(String proposedFingerprint) throws AuthorizationAccessException, UninheritableAuthorizationsException {
+                // fingerprint is managed by the encapsulating class
+                throw new UnsupportedOperationException();
+            }
+
+            @Override
+            public UserGroupProvider getUserGroupProvider() {
+                return new ConfigurableUserGroupProvider() {
+                    @Override
+                    public User addUser(User user) throws AuthorizationAccessException {
+                        return AbstractPolicyBasedAuthorizer.this.addUser(user);
+                    }
+
+                    @Override
+                    public User updateUser(User user) throws AuthorizationAccessException {
+                        return AbstractPolicyBasedAuthorizer.this.updateUser(user);
+                    }
+
+                    @Override
+                    public User deleteUser(User user) throws AuthorizationAccessException {
+                        return AbstractPolicyBasedAuthorizer.this.deleteUser(user);
+                    }
+
+                    @Override
+                    public User deleteUser(String userIdentifier) throws AuthorizationAccessException {
+                        return AbstractPolicyBasedAuthorizer.this.deleteUser(userIdentifier);
+                    }
+
+                    @Override
+                    public Group addGroup(Group group) throws AuthorizationAccessException {
+                        return AbstractPolicyBasedAuthorizer.this.addGroup(group);
+                    }
+
+                    @Override
+                    public Group updateGroup(Group group) throws AuthorizationAccessException {
+                        return AbstractPolicyBasedAuthorizer.this.updateGroup(group);
+                    }
+
+                    @Override
+                    public Group deleteGroup(Group group) throws AuthorizationAccessException {
+                        return AbstractPolicyBasedAuthorizer.this.deleteGroup(group);
+                    }
+
+                    @Override
+                    public Group deleteGroup(String groupIdentifier) throws AuthorizationAccessException {
+                        return AbstractPolicyBasedAuthorizer.this.deleteGroup(groupIdentifier);
+                    }
+
+                    @Override
+                    public Set<User> getUsers() throws AuthorizationAccessException {
+                        return AbstractPolicyBasedAuthorizer.this.getUsers();
+                    }
+
+                    @Override
+                    public User getUser(String identifier) throws AuthorizationAccessException {
+                        return AbstractPolicyBasedAuthorizer.this.getUser(identifier);
+                    }
+
+                    @Override
+                    public User getUserByIdentity(String identity) throws AuthorizationAccessException {
+                        return AbstractPolicyBasedAuthorizer.this.getUserByIdentity(identity);
+                    }
+
+                    @Override
+                    public Set<Group> getGroups() throws AuthorizationAccessException {
+                        return AbstractPolicyBasedAuthorizer.this.getGroups();
+                    }
+
+                    @Override
+                    public Group getGroup(String identifier) throws AuthorizationAccessException {
+                        return AbstractPolicyBasedAuthorizer.this.getGroup(identifier);
+                    }
+
+                    @Override
+                    public UserAndGroups getUserAndGroups(String identity) throws AuthorizationAccessException {
+                        final UsersAndAccessPolicies usersAndAccessPolicies = AbstractPolicyBasedAuthorizer.this.getUsersAndAccessPolicies();
+                        final User user = usersAndAccessPolicies.getUser(identity);
+                        final Set<Group> groups = usersAndAccessPolicies.getGroups(identity);
+
+                        return new UserAndGroups() {
+                            @Override
+                            public User getUser() {
+                                return user;
+                            }
+
+                            @Override
+                            public Set<Group> getGroups() {
+                                return groups;
+                            }
+                        };
+                    }
+
+                    @Override
+                    public String getFingerprint() throws AuthorizationAccessException {
+                        // fingerprint is managed by the encapsulating class
+                        throw new UnsupportedOperationException();
+                    }
+
+                    @Override
+                    public void inheritFingerprint(String fingerprint) throws AuthorizationAccessException {
+                        // fingerprint is managed by the encapsulating class
+                        throw new UnsupportedOperationException();
+                    }
+
+                    @Override
+                    public void checkInheritability(String proposedFingerprint) throws AuthorizationAccessException, UninheritableAuthorizationsException {
+                        // fingerprint is managed by the encapsulating class
+                        throw new UnsupportedOperationException();
+                    }
+
+                    @Override
+                    public void initialize(UserGroupProviderInitializationContext initializationContext) throws SecurityProviderCreationException {
+                    }
+
+                    @Override
+                    public void onConfigured(AuthorizerConfigurationContext configurationContext) throws SecurityProviderCreationException {
+                    }
+
+                    @Override
+                    public void preDestruction() throws SecurityProviderDestructionException {
+                    }
+                };
+            }
+
+            @Override
+            public void initialize(AccessPolicyProviderInitializationContext initializationContext) throws SecurityProviderCreationException {
+            }
+
+            @Override
+            public void onConfigured(AuthorizerConfigurationContext configurationContext) throws SecurityProviderCreationException {
+            }
+
+            @Override
+            public void preDestruction() throws SecurityProviderDestructionException {
+            }
+        };
+    }
+
+    /**
+     * Returns a fingerprint representing the authorizations managed by this authorizer. The fingerprint will be
+     * used for comparison to determine if two policy-based authorizers represent a compatible set of users,
+     * groups, and policies.
+     *
+     * @return the fingerprint for this Authorizer
+     */
+    @Override
+    public final String getFingerprint() throws AuthorizationAccessException {
+        final List<User> users = getSortedUsers();
+        final List<Group> groups = getSortedGroups();
+        final List<AccessPolicy> policies = getSortedAccessPolicies();
+
+        XMLStreamWriter writer = null;
+        final StringWriter out = new StringWriter();
+        try {
+            writer = XML_OUTPUT_FACTORY.createXMLStreamWriter(out);
+            writer.writeStartDocument();
+            writer.writeStartElement("authorizations");
+
+            for (User user : users) {
+                writeUser(writer, user);
+            }
+            for (Group group : groups) {
+                writeGroup(writer, group);
+            }
+            for (AccessPolicy policy : policies) {
+                writePolicy(writer, policy);
+            }
+
+            writer.writeEndElement();
+            writer.writeEndDocument();
+            writer.flush();
+        } catch (XMLStreamException e) {
+            throw new AuthorizationAccessException("Unable to generate fingerprint", e);
+        } finally {
+            if (writer != null) {
+                try {
+                    writer.close();
+                } catch (XMLStreamException e) {
+                    // nothing to do here
+                }
+            }
+        }
+
+        return out.toString();
+    }
+
+    private void writeUser(final XMLStreamWriter writer, final User user) throws XMLStreamException {
+        writer.writeStartElement(USER_ELEMENT);
+        writer.writeAttribute(IDENTIFIER_ATTR, user.getIdentifier());
+        writer.writeAttribute(IDENTITY_ATTR, user.getIdentity());
+        writer.writeEndElement();
+    }
+
+    private void writeGroup(final XMLStreamWriter writer, final Group group) throws XMLStreamException {
+        List<String> users = new ArrayList<>(group.getUsers());
+        Collections.sort(users);
+
+        writer.writeStartElement(GROUP_ELEMENT);
+        writer.writeAttribute(IDENTIFIER_ATTR, group.getIdentifier());
+        writer.writeAttribute(NAME_ATTR, group.getName());
+
+        for (String user : users) {
+            writer.writeStartElement(GROUP_USER_ELEMENT);
+            writer.writeAttribute(IDENTIFIER_ATTR, user);
+            writer.writeEndElement();
+        }
+
+        writer.writeEndElement();
+    }
+
+    private void writePolicy(final XMLStreamWriter writer, final AccessPolicy policy) throws XMLStreamException {
+        // sort the users for the policy
+        List<String> policyUsers = new ArrayList<>(policy.getUsers());
+        Collections.sort(policyUsers);
+
+        // sort the groups for this policy
+        List<String> policyGroups = new ArrayList<>(policy.getGroups());
+        Collections.sort(policyGroups);
+
+        writer.writeStartElement(POLICY_ELEMENT);
+        writer.writeAttribute(IDENTIFIER_ATTR, policy.getIdentifier());
+        writer.writeAttribute(RESOURCE_ATTR, policy.getResource());
+        writer.writeAttribute(ACTIONS_ATTR, policy.getAction().name());
+
+        for (String policyUser : policyUsers) {
+            writer.writeStartElement(POLICY_USER_ELEMENT);
+            writer.writeAttribute(IDENTIFIER_ATTR, policyUser);
+            writer.writeEndElement();
+        }
+
+        for (String policyGroup : policyGroups) {
+            writer.writeStartElement(POLICY_GROUP_ELEMENT);
+            writer.writeAttribute(IDENTIFIER_ATTR, policyGroup);
+            writer.writeEndElement();
+        }
+
+        writer.writeEndElement();
+    }
+
+    private List<AccessPolicy> getSortedAccessPolicies() {
+        final List<AccessPolicy> policies = new ArrayList<>(getAccessPolicies());
+        Collections.sort(policies, Comparator.comparing(AccessPolicy::getIdentifier));
+        return policies;
+    }
+
+    private List<Group> getSortedGroups() {
+        final List<Group> groups = new ArrayList<>(getGroups());
+        Collections.sort(groups, Comparator.comparing(Group::getIdentifier));
+        return groups;
+    }
+
+    private List<User> getSortedUsers() {
+        final List<User> users = new ArrayList<>(getUsers());
+        Collections.sort(users, Comparator.comparing(User::getIdentifier));
+        return users;
+    }
+
+    private static class PoliciesUsersAndGroups {
+        final List<AccessPolicy> accessPolicies;
+        final List<User> users;
+        final List<Group> groups;
+
+        public PoliciesUsersAndGroups(List<AccessPolicy> accessPolicies, List<User> users, List<Group> groups) {
+            this.accessPolicies = accessPolicies;
+            this.users = users;
+            this.groups = groups;
+        }
+
+        public List<AccessPolicy> getAccessPolicies() {
+            return accessPolicies;
+        }
+
+        public List<User> getUsers() {
+            return users;
+        }
+
+        public List<Group> getGroups() {
+            return groups;
+        }
+    }
+}

http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/6f26290d/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/security/authorization/AuthorizableLookup.java
----------------------------------------------------------------------
diff --git a/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/security/authorization/AuthorizableLookup.java b/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/security/authorization/AuthorizableLookup.java
new file mode 100644
index 0000000..49db37a
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/security/authorization/AuthorizableLookup.java
@@ -0,0 +1,84 @@
+/*
+ * 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.security.authorization;
+
+import org.apache.nifi.registry.security.authorization.resource.Authorizable;
+
+public interface AuthorizableLookup {
+
+    /**
+     * Get the authorizable for /actuator.
+     *
+     * @return authorizable
+     */
+    Authorizable getActuatorAuthorizable();
+
+    /**
+     * Get the authorizable for /swagger.
+     *
+     * @return authorizable
+     */
+    Authorizable getSwaggerAuthorizable();
+
+    /**
+     * Get the authorizable for /proxy.
+     *
+     * @return authorizable
+     */
+    Authorizable getProxyAuthorizable();
+
+    /**
+     * Get the authorizable for all tenants.
+     *
+     * Get the {@link Authorizable} that represents the resource of users and user groups.
+     * @return authorizable
+     */
+    Authorizable getTenantsAuthorizable();
+
+    /**
+     * Get the authorizable for all access policies.
+     *
+     * @return authorizable
+     */
+    Authorizable getPoliciesAuthorizable();
+
+    /**
+     * Get the authorizable for all Buckets.
+     *
+     * @return authorizable
+     */
+    Authorizable getBucketsAuthorizable();
+
+    /**
+     * Get the authorizable for the Bucket with the bucket id.
+     *
+     * @param bucketIdentifier bucket id
+     * @return authorizable
+     */
+    Authorizable getBucketAuthorizable(String bucketIdentifier);
+
+    /**
+     * Get the authorizable of the specified resource.
+     * If the resource is authorized by its base/top-level
+     * resource type, the authorizable for the base type will be returned.
+     *
+     * @param resource resource
+     * @return authorizable
+     */
+    Authorizable getAuthorizableByResource(final String resource);
+
+}

http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/6f26290d/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/security/authorization/AuthorizerCapabilityDetection.java
----------------------------------------------------------------------
diff --git a/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/security/authorization/AuthorizerCapabilityDetection.java b/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/security/authorization/AuthorizerCapabilityDetection.java
new file mode 100644
index 0000000..0652583
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/security/authorization/AuthorizerCapabilityDetection.java
@@ -0,0 +1,75 @@
+/*
+ * 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.security.authorization;
+
+public final class AuthorizerCapabilityDetection {
+
+    public static boolean isManagedAuthorizer(final Authorizer authorizer) {
+        return authorizer instanceof ManagedAuthorizer;
+    }
+
+    public static boolean isConfigurableAccessPolicyProvider(final Authorizer authorizer) {
+        if (!isManagedAuthorizer(authorizer)) {
+            return false;
+        }
+
+        final ManagedAuthorizer managedAuthorizer = (ManagedAuthorizer) authorizer;
+        return managedAuthorizer.getAccessPolicyProvider() instanceof ConfigurableAccessPolicyProvider;
+    }
+
+    public static boolean isConfigurableUserGroupProvider(final Authorizer authorizer) {
+        if (!isManagedAuthorizer(authorizer)) {
+            return false;
+        }
+
+        final ManagedAuthorizer managedAuthorizer = (ManagedAuthorizer) authorizer;
+        final AccessPolicyProvider accessPolicyProvider = managedAuthorizer.getAccessPolicyProvider();
+        return accessPolicyProvider.getUserGroupProvider() instanceof ConfigurableUserGroupProvider;
+    }
+
+    public static boolean isUserConfigurable(final Authorizer authorizer, final User user) {
+        if (!isConfigurableUserGroupProvider(authorizer)) {
+            return false;
+        }
+
+        final ManagedAuthorizer managedAuthorizer = (ManagedAuthorizer) authorizer;
+        final ConfigurableUserGroupProvider configurableUserGroupProvider = (ConfigurableUserGroupProvider) managedAuthorizer.getAccessPolicyProvider().getUserGroupProvider();
+        return configurableUserGroupProvider.isConfigurable(user);
+    }
+
+    public static boolean isGroupConfigurable(final Authorizer authorizer, final Group group) {
+        if (!isConfigurableUserGroupProvider(authorizer)) {
+            return false;
+        }
+
+        final ManagedAuthorizer managedAuthorizer = (ManagedAuthorizer) authorizer;
+        final ConfigurableUserGroupProvider configurableUserGroupProvider = (ConfigurableUserGroupProvider) managedAuthorizer.getAccessPolicyProvider().getUserGroupProvider();
+        return configurableUserGroupProvider.isConfigurable(group);
+    }
+
+    public static boolean isAccessPolicyConfigurable(final Authorizer authorizer, final AccessPolicy accessPolicy) {
+        if (!isConfigurableAccessPolicyProvider(authorizer)) {
+            return false;
+        }
+
+        final ManagedAuthorizer managedAuthorizer = (ManagedAuthorizer) authorizer;
+        final ConfigurableAccessPolicyProvider configurableAccessPolicyProvider = (ConfigurableAccessPolicyProvider) managedAuthorizer.getAccessPolicyProvider();
+        return configurableAccessPolicyProvider.isConfigurable(accessPolicy);
+    }
+
+    private AuthorizerCapabilityDetection() {}
+}


[15/51] [partial] nifi-registry git commit: NIFIREG-201 Refactoring project structure to better isolate extensions

Posted by kd...@apache.org.
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/mapper/IllegalStateExceptionMapper.java
----------------------------------------------------------------------
diff --git a/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/mapper/IllegalStateExceptionMapper.java b/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/mapper/IllegalStateExceptionMapper.java
new file mode 100644
index 0000000..11fc09d
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/mapper/IllegalStateExceptionMapper.java
@@ -0,0 +1,46 @@
+/*
+ * 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.mapper;
+
+import org.apache.commons.lang3.StringUtils;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.stereotype.Component;
+
+import javax.ws.rs.core.Response;
+import javax.ws.rs.ext.ExceptionMapper;
+import javax.ws.rs.ext.Provider;
+
+@Component
+@Provider
+public class IllegalStateExceptionMapper implements ExceptionMapper<IllegalStateException> {
+
+    private static final Logger logger = LoggerFactory.getLogger(IllegalStateExceptionMapper.class);
+
+    @Override
+    public Response toResponse(IllegalStateException exception) {
+        // log the error
+        logger.info(String.format("%s. Returning %s response.", exception, Response.Status.CONFLICT));
+
+        if (logger.isDebugEnabled()) {
+            logger.debug(StringUtils.EMPTY, exception);
+        }
+
+        return Response.status(Response.Status.CONFLICT).entity(exception.getMessage()).type("text/plain").build();
+    }
+
+}

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/mapper/InvalidAuthenticationExceptionMapper.java
----------------------------------------------------------------------
diff --git a/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/mapper/InvalidAuthenticationExceptionMapper.java b/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/mapper/InvalidAuthenticationExceptionMapper.java
new file mode 100644
index 0000000..15bb227
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/mapper/InvalidAuthenticationExceptionMapper.java
@@ -0,0 +1,47 @@
+/*
+ * 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.mapper;
+
+import org.apache.commons.lang3.StringUtils;
+import org.apache.nifi.registry.web.security.authentication.exception.InvalidAuthenticationException;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.stereotype.Component;
+
+import javax.ws.rs.core.Response;
+import javax.ws.rs.ext.ExceptionMapper;
+import javax.ws.rs.ext.Provider;
+
+/**
+ * Maps access denied exceptions into a client response.
+ */
+@Component
+@Provider
+public class InvalidAuthenticationExceptionMapper implements ExceptionMapper<InvalidAuthenticationException> {
+
+    private static final Logger logger = LoggerFactory.getLogger(InvalidAuthenticationExceptionMapper.class);
+
+    @Override
+    public Response toResponse(InvalidAuthenticationException exception) {
+        if (logger.isDebugEnabled()) {
+            logger.debug(StringUtils.EMPTY, exception);
+        }
+
+        return Response.status(Response.Status.UNAUTHORIZED).entity(exception.getMessage()).type("text/plain").build();
+    }
+
+}

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/mapper/NiFiRegistryJsonProvider.java
----------------------------------------------------------------------
diff --git a/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/mapper/NiFiRegistryJsonProvider.java b/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/mapper/NiFiRegistryJsonProvider.java
new file mode 100644
index 0000000..10a044a
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/mapper/NiFiRegistryJsonProvider.java
@@ -0,0 +1,36 @@
+/*
+ * 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.mapper;
+
+import org.apache.nifi.registry.serialization.jackson.ObjectMapperProvider;
+import org.glassfish.jersey.jackson.internal.jackson.jaxrs.json.JacksonJaxbJsonProvider;
+import org.springframework.stereotype.Component;
+
+import javax.ws.rs.Produces;
+import javax.ws.rs.core.MediaType;
+import javax.ws.rs.ext.Provider;
+
+@Component
+@Provider
+@Produces(MediaType.APPLICATION_JSON)
+public class NiFiRegistryJsonProvider extends JacksonJaxbJsonProvider {
+
+    public NiFiRegistryJsonProvider() {
+        super();
+        setMapper(ObjectMapperProvider.getMapper());
+    }
+}

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/mapper/NotAllowedExceptionMapper.java
----------------------------------------------------------------------
diff --git a/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/mapper/NotAllowedExceptionMapper.java b/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/mapper/NotAllowedExceptionMapper.java
new file mode 100644
index 0000000..9237735
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/mapper/NotAllowedExceptionMapper.java
@@ -0,0 +1,46 @@
+/*
+ * 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.mapper;
+
+import org.apache.commons.lang3.StringUtils;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.stereotype.Component;
+
+import javax.ws.rs.NotAllowedException;
+import javax.ws.rs.core.Response;
+import javax.ws.rs.core.Response.Status;
+import javax.ws.rs.ext.ExceptionMapper;
+import javax.ws.rs.ext.Provider;
+
+/**
+ * Maps exceptions into client responses.
+ */
+@Component
+@Provider
+public class NotAllowedExceptionMapper implements ExceptionMapper<NotAllowedException> {
+
+    private static final Logger logger = LoggerFactory.getLogger(NotAllowedExceptionMapper.class);
+
+    @Override
+    public Response toResponse(NotAllowedException exception) {
+        logger.info(String.format("%s. Returning %s response.", exception, Status.METHOD_NOT_ALLOWED));
+        logger.debug(StringUtils.EMPTY, exception);
+        return Response.status(Status.METHOD_NOT_ALLOWED).entity(exception.getMessage()).type("text/plain").build();
+    }
+
+}

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/mapper/NotFoundExceptionMapper.java
----------------------------------------------------------------------
diff --git a/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/mapper/NotFoundExceptionMapper.java b/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/mapper/NotFoundExceptionMapper.java
new file mode 100644
index 0000000..8abd4c0
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/mapper/NotFoundExceptionMapper.java
@@ -0,0 +1,50 @@
+/*
+ * 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.mapper;
+
+import javax.ws.rs.NotFoundException;
+import javax.ws.rs.core.Response;
+import javax.ws.rs.ext.ExceptionMapper;
+import javax.ws.rs.ext.Provider;
+
+import org.apache.commons.lang3.StringUtils;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.stereotype.Component;
+
+/**
+ * Maps not found exceptions into client responses.
+ */
+@Component
+@Provider
+public class NotFoundExceptionMapper implements ExceptionMapper<NotFoundException> {
+
+    private static final Logger logger = LoggerFactory.getLogger(NotFoundExceptionMapper.class);
+
+    @Override
+    public Response toResponse(NotFoundException exception) {
+        // log the error
+        logger.info(String.format("%s. Returning %s response.", exception, Response.Status.NOT_FOUND));
+
+        if (logger.isDebugEnabled()) {
+            logger.debug(StringUtils.EMPTY, exception);
+        }
+
+        return Response.status(Response.Status.NOT_FOUND).entity(exception.getMessage()).type("text/plain").build();
+    }
+
+}

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/mapper/ResourceNotFoundExceptionMapper.java
----------------------------------------------------------------------
diff --git a/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/mapper/ResourceNotFoundExceptionMapper.java b/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/mapper/ResourceNotFoundExceptionMapper.java
new file mode 100644
index 0000000..a71452d
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/mapper/ResourceNotFoundExceptionMapper.java
@@ -0,0 +1,47 @@
+/*
+ * 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.mapper;
+
+import org.apache.commons.lang3.StringUtils;
+import org.apache.nifi.registry.exception.ResourceNotFoundException;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.stereotype.Component;
+
+import javax.ws.rs.core.Response;
+import javax.ws.rs.ext.ExceptionMapper;
+import javax.ws.rs.ext.Provider;
+
+@Component
+@Provider
+public class ResourceNotFoundExceptionMapper implements ExceptionMapper<ResourceNotFoundException> {
+
+    private static final Logger logger = LoggerFactory.getLogger(ResourceNotFoundExceptionMapper.class);
+
+    @Override
+    public Response toResponse(ResourceNotFoundException exception) {
+        // log the error
+        logger.info(String.format("%s. Returning %s response.", exception, Response.Status.NOT_FOUND));
+
+        if (logger.isDebugEnabled()) {
+            logger.debug(StringUtils.EMPTY, exception);
+        }
+
+        return Response.status(Response.Status.NOT_FOUND).entity(exception.getMessage()).type("text/plain").build();
+    }
+
+}

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/mapper/SerializationExceptionMapper.java
----------------------------------------------------------------------
diff --git a/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/mapper/SerializationExceptionMapper.java b/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/mapper/SerializationExceptionMapper.java
new file mode 100644
index 0000000..8f53141
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/mapper/SerializationExceptionMapper.java
@@ -0,0 +1,47 @@
+/*
+ * 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.mapper;
+
+import org.apache.commons.lang3.StringUtils;
+import org.apache.nifi.registry.serialization.SerializationException;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.stereotype.Component;
+
+import javax.ws.rs.core.Response;
+import javax.ws.rs.ext.ExceptionMapper;
+import javax.ws.rs.ext.Provider;
+
+@Component
+@Provider
+public class SerializationExceptionMapper implements ExceptionMapper<SerializationException> {
+
+    private static final Logger logger = LoggerFactory.getLogger(SerializationExceptionMapper.class);
+
+    @Override
+    public Response toResponse(SerializationException exception) {
+        // log the error
+        logger.info(String.format("%s. Returning %s response.", exception, Response.Status.INTERNAL_SERVER_ERROR));
+
+        if (logger.isDebugEnabled()) {
+            logger.debug(StringUtils.EMPTY, exception);
+        }
+
+        return Response.status(Response.Status.INTERNAL_SERVER_ERROR).entity(exception.getMessage()).type("text/plain").build();
+    }
+
+}

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/mapper/ThrowableMapper.java
----------------------------------------------------------------------
diff --git a/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/mapper/ThrowableMapper.java b/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/mapper/ThrowableMapper.java
new file mode 100644
index 0000000..4b434ed
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/mapper/ThrowableMapper.java
@@ -0,0 +1,42 @@
+/*
+ * 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.mapper;
+
+import javax.ws.rs.core.Response;
+import javax.ws.rs.ext.ExceptionMapper;
+import javax.ws.rs.ext.Provider;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.stereotype.Component;
+
+/**
+ * Maps unknown node exceptions into client responses.
+ */
+@Component
+@Provider
+public class ThrowableMapper implements ExceptionMapper<Throwable> {
+
+    private static final Logger logger = LoggerFactory.getLogger(ThrowableMapper.class);
+
+    @Override
+    public Response toResponse(Throwable exception) {
+        // log the error
+        logger.error(String.format("An unexpected error has occurred: %s. Returning %s response.", exception, Response.Status.INTERNAL_SERVER_ERROR), exception);
+        return Response.status(Response.Status.INTERNAL_SERVER_ERROR).entity("An unexpected error has occurred. Please check the logs for additional details.").type("text/plain").build();
+    }
+
+}

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/mapper/UnauthorizedExceptionMapper.java
----------------------------------------------------------------------
diff --git a/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/mapper/UnauthorizedExceptionMapper.java b/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/mapper/UnauthorizedExceptionMapper.java
new file mode 100644
index 0000000..1c67e94
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/mapper/UnauthorizedExceptionMapper.java
@@ -0,0 +1,56 @@
+/*
+ * 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.mapper;
+
+import org.apache.commons.lang3.StringUtils;
+import org.apache.nifi.registry.web.exception.UnauthorizedException;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.stereotype.Component;
+
+import javax.ws.rs.core.Response;
+import javax.ws.rs.ext.ExceptionMapper;
+import javax.ws.rs.ext.Provider;
+
+/**
+ * Maps Unauthorized exceptions into client responses that set the WWW-Authenticate header
+ * with a list of challenges (i.e., acceptable auth scheme types).
+ */
+@Component
+@Provider
+public class UnauthorizedExceptionMapper implements ExceptionMapper<UnauthorizedException> {
+
+    private static final Logger logger = LoggerFactory.getLogger(UnauthorizedExceptionMapper.class);
+
+    private static final String AUTHENTICATION_CHALLENGE_HEADER_NAME = "WWW-Authenticate";
+
+    @Override
+    public Response toResponse(UnauthorizedException exception) {
+
+        logger.info("{}. Returning {} response.", exception, Response.Status.UNAUTHORIZED);
+        logger.debug(StringUtils.EMPTY, exception);
+
+        final Response.ResponseBuilder response = Response.status(Response.Status.UNAUTHORIZED);
+        if (exception.getWwwAuthenticateChallenge() != null) {
+            response.header(AUTHENTICATION_CHALLENGE_HEADER_NAME, exception.getWwwAuthenticateChallenge());
+        }
+        response.entity(exception.getMessage()).type("text/plain");
+        return response.build();
+
+    }
+
+}

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/security/NiFiRegistryMasterKeyProviderFactory.java
----------------------------------------------------------------------
diff --git a/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/security/NiFiRegistryMasterKeyProviderFactory.java b/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/security/NiFiRegistryMasterKeyProviderFactory.java
new file mode 100644
index 0000000..8026cca
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/security/NiFiRegistryMasterKeyProviderFactory.java
@@ -0,0 +1,67 @@
+/*
+ * 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.security;
+
+import org.apache.nifi.registry.NiFiRegistryApiApplication;
+import org.apache.nifi.registry.security.crypto.CryptoKeyProvider;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.web.context.ServletContextAware;
+
+import javax.servlet.ServletContext;
+
+@Configuration
+public class NiFiRegistryMasterKeyProviderFactory implements ServletContextAware {
+
+    private static final Logger logger = LoggerFactory.getLogger(NiFiRegistryMasterKeyProviderFactory.class);
+
+    private CryptoKeyProvider masterKeyProvider = null;
+
+    @Bean
+    public CryptoKeyProvider getNiFiRegistryMasterKeyProvider() {
+        return masterKeyProvider;
+    }
+
+    @Override
+    public void setServletContext(ServletContext servletContext) {
+        Object rawKeyProviderObject = servletContext.getAttribute(NiFiRegistryApiApplication.NIFI_REGISTRY_MASTER_KEY_ATTRIBUTE);
+
+        if (rawKeyProviderObject == null) {
+            logger.warn("Value of {} was null. " +
+                    "{} bean will not be available in Application Context, so any attempt to load protected property values may fail.",
+                    NiFiRegistryApiApplication.NIFI_REGISTRY_MASTER_KEY_ATTRIBUTE,
+                    CryptoKeyProvider.class.getSimpleName());
+            return;
+        }
+
+        if (!(rawKeyProviderObject instanceof CryptoKeyProvider)) {
+            logger.warn("Expected value of {} to be of type {}, but instead got {}. " +
+                    "{} bean will NOT be available in Application Context, so any attempt to load protected property values may fail.",
+                    NiFiRegistryApiApplication.NIFI_REGISTRY_MASTER_KEY_ATTRIBUTE,
+                    CryptoKeyProvider.class.getName(),
+                    rawKeyProviderObject.getClass().getName(),
+                    CryptoKeyProvider.class.getSimpleName());
+            return;
+        }
+
+        logger.info("Updating Application Context with {} bean for obtaining NiFi Registry master key.", CryptoKeyProvider.class.getSimpleName());
+        masterKeyProvider = (CryptoKeyProvider) rawKeyProviderObject;
+    }
+
+}

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/security/NiFiRegistrySecurityConfig.java
----------------------------------------------------------------------
diff --git a/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/security/NiFiRegistrySecurityConfig.java b/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/security/NiFiRegistrySecurityConfig.java
new file mode 100644
index 0000000..a2a6ea9
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/security/NiFiRegistrySecurityConfig.java
@@ -0,0 +1,210 @@
+/*
+ * 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.security;
+
+import org.apache.nifi.registry.properties.NiFiRegistryProperties;
+import org.apache.nifi.registry.security.authorization.resource.ResourceType;
+import org.apache.nifi.registry.service.AuthorizationService;
+import org.apache.nifi.registry.web.security.authentication.AnonymousIdentityFilter;
+import org.apache.nifi.registry.web.security.authentication.IdentityAuthenticationProvider;
+import org.apache.nifi.registry.web.security.authentication.IdentityFilter;
+import org.apache.nifi.registry.web.security.authentication.exception.UntrustedProxyException;
+import org.apache.nifi.registry.web.security.authentication.jwt.JwtIdentityProvider;
+import org.apache.nifi.registry.web.security.authentication.x509.X509IdentityAuthenticationProvider;
+import org.apache.nifi.registry.web.security.authentication.x509.X509IdentityProvider;
+import org.apache.nifi.registry.web.security.authorization.ResourceAuthorizationFilter;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
+import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
+import org.springframework.security.config.annotation.web.builders.HttpSecurity;
+import org.springframework.security.config.annotation.web.builders.WebSecurity;
+import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
+import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
+import org.springframework.security.config.http.SessionCreationPolicy;
+import org.springframework.security.core.AuthenticationException;
+import org.springframework.security.web.AuthenticationEntryPoint;
+import org.springframework.security.web.access.intercept.FilterSecurityInterceptor;
+import org.springframework.security.web.authentication.AnonymousAuthenticationFilter;
+
+import javax.servlet.ServletException;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+import java.io.IOException;
+
+/**
+ * NiFi Registry Web Api Spring security
+ */
+@Configuration
+@EnableWebSecurity
+@EnableGlobalMethodSecurity(prePostEnabled = true)
+public class NiFiRegistrySecurityConfig extends WebSecurityConfigurerAdapter {
+
+    private static final Logger logger = LoggerFactory.getLogger(NiFiRegistrySecurityConfig.class);
+
+    @Autowired
+    private NiFiRegistryProperties properties;
+
+    @Autowired
+    private AuthorizationService authorizationService;
+
+    private AnonymousIdentityFilter anonymousAuthenticationFilter = new AnonymousIdentityFilter();
+
+    @Autowired
+    private X509IdentityProvider x509IdentityProvider;
+    private IdentityFilter x509AuthenticationFilter;
+    private IdentityAuthenticationProvider x509AuthenticationProvider;
+
+    @Autowired
+    private JwtIdentityProvider jwtIdentityProvider;
+    private IdentityFilter jwtAuthenticationFilter;
+    private IdentityAuthenticationProvider jwtAuthenticationProvider;
+
+    private ResourceAuthorizationFilter resourceAuthorizationFilter;
+
+    public NiFiRegistrySecurityConfig() {
+        super(true); // disable defaults
+    }
+
+    @Override
+    public void configure(WebSecurity webSecurity) throws Exception {
+        // allow any client to access the endpoint for logging in to generate an access token
+        webSecurity.ignoring().antMatchers( "/access/token/**");
+    }
+
+    @Override
+    protected void configure(HttpSecurity http) throws Exception {
+        http
+                .rememberMe().disable()
+                .authorizeRequests()
+                    .anyRequest().fullyAuthenticated()
+                    .and()
+                .exceptionHandling()
+                    .authenticationEntryPoint(http401AuthenticationEntryPoint())
+                    .and()
+                .sessionManagement()
+                    .sessionCreationPolicy(SessionCreationPolicy.STATELESS);
+
+        // x509
+        http.addFilterBefore(x509AuthenticationFilter(), AnonymousAuthenticationFilter.class);
+
+        // jwt
+        http.addFilterBefore(jwtAuthenticationFilter(), AnonymousAuthenticationFilter.class);
+
+        // otp
+        // todo, if needed one-time password auth filter goes here
+
+        if (properties.getSslPort() == null) {
+            // If we are running an unsecured NiFi Registry server, add an
+            // anonymous authentication filter that will populate the
+            // authenticated, anonymous user if no other user identity
+            // is detected earlier in the Spring filter chain.
+            http.anonymous().authenticationFilter(anonymousAuthenticationFilter);
+        }
+
+        // After Spring Security filter chain is complete (so authentication is done),
+        // but before the Jersey application endpoints get the request,
+        // insert the ResourceAuthorizationFilter to do its authorization checks
+        http.addFilterAfter(resourceAuthorizationFilter(), FilterSecurityInterceptor.class);
+
+    }
+
+    @Override
+    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
+        auth
+                .authenticationProvider(x509AuthenticationProvider())
+                .authenticationProvider(jwtAuthenticationProvider());
+    }
+
+    private IdentityFilter x509AuthenticationFilter() throws Exception {
+        if (x509AuthenticationFilter == null) {
+            x509AuthenticationFilter = new IdentityFilter(x509IdentityProvider);
+        }
+        return x509AuthenticationFilter;
+    }
+
+    private IdentityAuthenticationProvider x509AuthenticationProvider() {
+        if (x509AuthenticationProvider == null) {
+            x509AuthenticationProvider = new X509IdentityAuthenticationProvider(properties, authorizationService.getAuthorizer(), x509IdentityProvider);
+        }
+        return x509AuthenticationProvider;
+    }
+
+    private IdentityFilter jwtAuthenticationFilter() throws Exception {
+        if (jwtAuthenticationFilter == null) {
+            jwtAuthenticationFilter = new IdentityFilter(jwtIdentityProvider);
+        }
+        return jwtAuthenticationFilter;
+    }
+
+    private IdentityAuthenticationProvider jwtAuthenticationProvider() {
+        if (jwtAuthenticationProvider == null) {
+            jwtAuthenticationProvider = new IdentityAuthenticationProvider(properties, authorizationService.getAuthorizer(), jwtIdentityProvider);
+        }
+        return jwtAuthenticationProvider;
+    }
+
+    private ResourceAuthorizationFilter resourceAuthorizationFilter() {
+        if (resourceAuthorizationFilter == null) {
+            resourceAuthorizationFilter = ResourceAuthorizationFilter.builder()
+                    .setAuthorizationService(authorizationService)
+                    .addResourceType(ResourceType.Actuator)
+                    .addResourceType(ResourceType.Swagger)
+                    .build();
+        }
+        return resourceAuthorizationFilter;
+    }
+
+    private AuthenticationEntryPoint http401AuthenticationEntryPoint() {
+        // This gets used for both secured and unsecured configurations. It will be called by Spring Security if a request makes it through the filter chain without being authenticated.
+        // For unsecured, this should never be reached because the custom AnonymousAuthenticationFilter should always populate a fully-authenticated anonymous user
+        // For secured, this will cause attempt to access any API endpoint (except those explicitly ignored) without providing credentials to return a 401 Unauthorized challenge
+        return new AuthenticationEntryPoint() {
+            @Override
+            public void commence(HttpServletRequest request,
+                                 HttpServletResponse response,
+                                 AuthenticationException authenticationException)
+                    throws IOException, ServletException {
+
+                final int status;
+
+                // See X509IdentityAuthenticationProvider.buildAuthenticatedToken(...)
+                if (authenticationException instanceof UntrustedProxyException) {
+                    // return a 403 response
+                    status = HttpServletResponse.SC_FORBIDDEN;
+                    logger.info("Identity in proxy chain not trusted to act as a proxy: {} Returning 403 response.", authenticationException.toString());
+
+                } else {
+                    // return a 401 response
+                    status = HttpServletResponse.SC_UNAUTHORIZED;
+                    logger.info("Client could not be authenticated due to: {} Returning 401 response.", authenticationException.toString());
+                }
+
+                logger.debug("", authenticationException);
+
+                if (!response.isCommitted()) {
+                    response.setStatus(status);
+                    response.setContentType("text/plain");
+                    response.getWriter().println(String.format("%s Contact the system administrator.", authenticationException.getLocalizedMessage()));
+                }
+            }
+        };
+    }
+
+}

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/security/PermissionsService.java
----------------------------------------------------------------------
diff --git a/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/security/PermissionsService.java b/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/security/PermissionsService.java
new file mode 100644
index 0000000..1e00ee1
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/security/PermissionsService.java
@@ -0,0 +1,100 @@
+/*
+ * 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.security;
+
+import org.apache.nifi.registry.bucket.Bucket;
+import org.apache.nifi.registry.bucket.BucketItem;
+import org.apache.nifi.registry.authorization.Permissions;
+import org.apache.nifi.registry.security.authorization.AuthorizableLookup;
+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.Service;
+
+/**
+ * This is a class that Resource classes can utilized to populate fields
+ * on model objects returned by the {@link org.apache.nifi.registry.service.RegistryService}
+ * before returning them to a client.
+ *
+ * The fields cannot be populated by the RegistryService because they require
+ * the {@link AuthorizationService}, which RegistryService does not depend on.
+ */
+@Service
+public class PermissionsService {
+
+    private AuthorizationService authorizationService;
+    private AuthorizableLookup authorizableLookup;
+
+    @Autowired
+    public PermissionsService(AuthorizationService authorizationService, AuthorizableLookup authorizableLookup) {
+        this.authorizationService = authorizationService;
+        this.authorizableLookup = authorizableLookup;
+    }
+
+    public void populateBucketPermissions(final Iterable<Bucket> buckets) {
+        Permissions topLevelBucketPermissions = authorizationService.getPermissionsForResource(authorizableLookup.getBucketsAuthorizable());
+        buckets.forEach(b -> populateBucketPermissions(b, topLevelBucketPermissions));
+    }
+
+    public void populateBucketPermissions(final Bucket bucket) {
+        populateBucketPermissions(bucket, null);
+    }
+
+    public void populateItemPermissions(final Iterable<? extends BucketItem> bucketItems) {
+        Permissions topLevelBucketPermissions = authorizationService.getPermissionsForResource(authorizableLookup.getBucketsAuthorizable());
+        bucketItems.forEach(i -> populateItemPermissions(i, topLevelBucketPermissions));
+    }
+
+    public void populateItemPermissions(final BucketItem bucketItem) {
+        populateItemPermissions(bucketItem, null);
+    }
+
+    private void populateBucketPermissions(final Bucket bucket, final Permissions knownPermissions) {
+
+        if (bucket == null) {
+            return;
+        }
+
+        Permissions bucketPermissions = createPermissionsForBucketId(bucket.getIdentifier(), knownPermissions);
+        bucket.setPermissions(bucketPermissions);
+
+    }
+
+    private void populateItemPermissions(final BucketItem bucketItem, final Permissions knownPermissions) {
+
+        if (bucketItem == null) {
+            return;
+        }
+
+        Permissions bucketItemPermissions = createPermissionsForBucketId(bucketItem.getBucketIdentifier(), knownPermissions);
+        bucketItem.setPermissions(bucketItemPermissions);
+
+    }
+
+    private Permissions createPermissionsForBucketId(String bucketId, final Permissions knownPermissions) {
+
+        Authorizable bucketResource = authorizableLookup.getBucketAuthorizable(bucketId);
+
+        Permissions permissions = knownPermissions == null
+                ? authorizationService.getPermissionsForResource(bucketResource)
+                : authorizationService.getPermissionsForResource(bucketResource, knownPermissions);
+
+        return permissions;
+
+    }
+
+}

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/security/authentication/AnonymousIdentityFilter.java
----------------------------------------------------------------------
diff --git a/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/security/authentication/AnonymousIdentityFilter.java b/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/security/authentication/AnonymousIdentityFilter.java
new file mode 100644
index 0000000..f879f0d
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/security/authentication/AnonymousIdentityFilter.java
@@ -0,0 +1,39 @@
+/*
+ * 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.security.authentication;
+
+import org.apache.nifi.registry.security.authorization.user.NiFiUserDetails;
+import org.apache.nifi.registry.security.authorization.user.StandardNiFiUser;
+import org.springframework.security.core.Authentication;
+import org.springframework.security.web.authentication.AnonymousAuthenticationFilter;
+
+import javax.servlet.http.HttpServletRequest;
+
+public class AnonymousIdentityFilter extends AnonymousAuthenticationFilter {
+
+    private static final String ANONYMOUS_KEY = "anonymousNifiKey";
+
+    public AnonymousIdentityFilter() {
+        super(ANONYMOUS_KEY);
+    }
+
+    @Override
+    protected Authentication createAuthentication(HttpServletRequest request) {
+        return new AuthenticationSuccessToken(new NiFiUserDetails(StandardNiFiUser.ANONYMOUS));
+    }
+
+}

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/security/authentication/AuthenticationRequestToken.java
----------------------------------------------------------------------
diff --git a/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/security/authentication/AuthenticationRequestToken.java b/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/security/authentication/AuthenticationRequestToken.java
new file mode 100644
index 0000000..a5a5ec3
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/security/authentication/AuthenticationRequestToken.java
@@ -0,0 +1,107 @@
+/*
+ * 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.security.authentication;
+
+import org.apache.nifi.registry.security.authentication.AuthenticationRequest;
+import org.springframework.security.core.Authentication;
+import org.springframework.security.core.GrantedAuthority;
+
+import java.security.Principal;
+import java.util.Collection;
+
+/**
+ * Wraps an AuthenticationRequest in a Token that implements the Spring Security Authentication interface.
+ */
+public class AuthenticationRequestToken implements Authentication {
+
+    private final AuthenticationRequest authenticationRequest;
+    private final Class<?> authenticationRequestOrigin;
+    private final String clientAddress;
+
+    public AuthenticationRequestToken(AuthenticationRequest authenticationRequest, Class<?> authenticationRequestOrigin, String clientAddress) {
+        this.authenticationRequest = authenticationRequest;
+        this.authenticationRequestOrigin = authenticationRequestOrigin;
+        this.clientAddress = clientAddress;
+    }
+
+    @Override
+    public Collection<? extends GrantedAuthority> getAuthorities() {
+        return null;
+    }
+
+    @Override
+    public Object getCredentials() {
+        return authenticationRequest.getCredentials();
+    }
+
+    @Override
+    public Object getDetails() {
+        return authenticationRequest.getDetails();
+    }
+
+    @Override
+    public Object getPrincipal() {
+        return new Principal() {
+            @Override
+            public String getName() {
+                return authenticationRequest.getUsername();
+            }
+        };
+    }
+
+    @Override
+    public boolean isAuthenticated() {
+        return false;
+    }
+
+    @Override
+    public void setAuthenticated(boolean b) throws IllegalArgumentException {
+        throw new IllegalArgumentException("AuthenticationRequestWrapper cannot be trusted. It is only to be used for storing an identity claim.");
+    }
+
+    @Override
+    public String getName() {
+        return authenticationRequest.getUsername();
+    }
+
+    @Override
+    public int hashCode() {
+        return authenticationRequest.hashCode();
+    }
+
+    @Override
+    public boolean equals(Object obj) {
+        return authenticationRequest.equals(obj);
+    }
+
+    @Override
+    public String toString() {
+        return authenticationRequest.toString();
+    }
+
+    public AuthenticationRequest getAuthenticationRequest() {
+        return authenticationRequest;
+    }
+
+    public Class<?> getAuthenticationRequestOrigin() {
+        return authenticationRequestOrigin;
+    }
+
+    public String getClientAddress() {
+        return clientAddress;
+    }
+}

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/security/authentication/AuthenticationSuccessToken.java
----------------------------------------------------------------------
diff --git a/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/security/authentication/AuthenticationSuccessToken.java b/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/security/authentication/AuthenticationSuccessToken.java
new file mode 100644
index 0000000..ea6f1e9
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/security/authentication/AuthenticationSuccessToken.java
@@ -0,0 +1,55 @@
+/*
+ * 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.security.authentication;
+
+import org.springframework.security.authentication.AbstractAuthenticationToken;
+import org.springframework.security.core.userdetails.UserDetails;
+
+/**
+ * An authentication token that represents an Authenticated and Authorized user of the NiFi Apis. The authorities are based off the specified UserDetails.
+ */
+public class AuthenticationSuccessToken extends AbstractAuthenticationToken {
+
+    private final UserDetails nifiUserDetails;
+
+    public AuthenticationSuccessToken(final UserDetails nifiUserDetails) {
+        super(nifiUserDetails.getAuthorities());
+        super.setAuthenticated(true);
+        setDetails(nifiUserDetails);
+        this.nifiUserDetails = nifiUserDetails;
+    }
+
+    @Override
+    public Object getCredentials() {
+        return nifiUserDetails.getPassword();
+    }
+
+    @Override
+    public Object getPrincipal() {
+        return nifiUserDetails;
+    }
+
+    @Override
+    public final void setAuthenticated(boolean authenticated) {
+        throw new IllegalArgumentException("Cannot change the authenticated state.");
+    }
+
+    @Override
+    public String toString() {
+        return nifiUserDetails.getUsername();
+    }
+}

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/security/authentication/IdentityAuthenticationProvider.java
----------------------------------------------------------------------
diff --git a/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/security/authentication/IdentityAuthenticationProvider.java b/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/security/authentication/IdentityAuthenticationProvider.java
new file mode 100644
index 0000000..ff6a218
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/security/authentication/IdentityAuthenticationProvider.java
@@ -0,0 +1,140 @@
+/*
+ * 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.security.authentication;
+
+import org.apache.nifi.registry.properties.NiFiRegistryProperties;
+import org.apache.nifi.registry.properties.util.IdentityMapping;
+import org.apache.nifi.registry.properties.util.IdentityMappingUtil;
+import org.apache.nifi.registry.security.authentication.AuthenticationRequest;
+import org.apache.nifi.registry.security.authentication.AuthenticationResponse;
+import org.apache.nifi.registry.security.authentication.IdentityProvider;
+import org.apache.nifi.registry.security.authentication.exception.InvalidCredentialsException;
+import org.apache.nifi.registry.security.authorization.Authorizer;
+import org.apache.nifi.registry.security.authorization.Group;
+import org.apache.nifi.registry.security.authorization.ManagedAuthorizer;
+import org.apache.nifi.registry.security.authorization.UserAndGroups;
+import org.apache.nifi.registry.security.authorization.UserGroupProvider;
+import org.apache.nifi.registry.security.authorization.user.NiFiUserDetails;
+import org.apache.nifi.registry.security.authorization.user.StandardNiFiUser;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.security.authentication.AuthenticationProvider;
+import org.springframework.security.authentication.BadCredentialsException;
+import org.springframework.security.core.Authentication;
+import org.springframework.security.core.AuthenticationException;
+
+import java.util.Collections;
+import java.util.List;
+import java.util.Set;
+import java.util.stream.Collectors;
+
+public class IdentityAuthenticationProvider implements AuthenticationProvider {
+
+    private static final Logger LOGGER = LoggerFactory.getLogger(IdentityAuthenticationProvider.class);
+
+    protected NiFiRegistryProperties properties;
+    protected Authorizer authorizer;
+    protected final IdentityProvider identityProvider;
+    private List<IdentityMapping> mappings;
+
+    public IdentityAuthenticationProvider(
+            NiFiRegistryProperties properties,
+            Authorizer authorizer,
+            IdentityProvider identityProvider) {
+        this.properties = properties;
+        this.authorizer = authorizer;
+        this.identityProvider = identityProvider;
+        this.mappings = Collections.unmodifiableList(IdentityMappingUtil.getIdentityMappings(properties));
+    }
+
+    @Override
+    public Authentication authenticate(Authentication authentication) throws AuthenticationException {
+
+        // Determine if this AuthenticationProvider's identityProvider should be able to support this AuthenticationRequest
+        boolean tokenOriginatedFromThisIdentityProvider = checkTokenOriginatedFromThisIdentityProvider(authentication);
+
+        if (!tokenOriginatedFromThisIdentityProvider) {
+            // Returning null indicates to The Spring Security AuthenticationManager that this AuthenticationProvider
+            // cannot authenticate this token and another provider should be tried.
+            return null;
+        }
+
+        AuthenticationRequestToken authenticationRequestToken = ((AuthenticationRequestToken)authentication);
+        AuthenticationRequest authenticationRequest = authenticationRequestToken.getAuthenticationRequest();
+
+        try {
+            AuthenticationResponse authenticationResponse = identityProvider.authenticate(authenticationRequest);
+            if (authenticationResponse == null) {
+                return null;
+            }
+            return buildAuthenticatedToken(authenticationRequestToken, authenticationResponse);
+        } catch (InvalidCredentialsException e) {
+            throw new BadCredentialsException("Identity Provider authentication failed.", e);
+        }
+
+    }
+
+    @Override
+    public boolean supports(Class<?> authenticationClazz) {
+        // is authenticationClazz a subclass of AuthenticationRequestWrapper?
+        return AuthenticationRequestToken.class.isAssignableFrom(authenticationClazz);
+    }
+
+    protected AuthenticationSuccessToken buildAuthenticatedToken(
+            AuthenticationRequestToken requestToken,
+            AuthenticationResponse response) {
+
+        final String mappedIdentity = mapIdentity(response.getIdentity());
+
+        return new AuthenticationSuccessToken(new NiFiUserDetails(
+                new StandardNiFiUser.Builder()
+                        .identity(mappedIdentity)
+                        .groups(getUserGroups(mappedIdentity))
+                        .clientAddress(requestToken.getClientAddress())
+                        .build()));
+    }
+
+    protected boolean checkTokenOriginatedFromThisIdentityProvider(Authentication authentication) {
+        return (authentication instanceof AuthenticationRequestToken
+                && identityProvider.getClass().equals(((AuthenticationRequestToken) authentication).getAuthenticationRequestOrigin()));
+    }
+
+    protected String mapIdentity(final String identity) {
+        return IdentityMappingUtil.mapIdentity(identity, mappings);
+    }
+
+    protected Set<String> getUserGroups(final String identity) {
+        return getUserGroups(authorizer, identity);
+    }
+
+    private static Set<String> getUserGroups(final Authorizer authorizer, final String userIdentity) {
+        if (authorizer instanceof ManagedAuthorizer) {
+            final ManagedAuthorizer managedAuthorizer = (ManagedAuthorizer) authorizer;
+            final UserGroupProvider userGroupProvider = managedAuthorizer.getAccessPolicyProvider().getUserGroupProvider();
+            final UserAndGroups userAndGroups = userGroupProvider.getUserAndGroups(userIdentity);
+            final Set<Group> userGroups = userAndGroups.getGroups();
+
+            if (userGroups == null || userGroups.isEmpty()) {
+                return Collections.emptySet();
+            } else {
+                return userAndGroups.getGroups().stream().map(Group::getName).collect(Collectors.toSet());
+            }
+        } else {
+            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/security/authentication/IdentityFilter.java
----------------------------------------------------------------------
diff --git a/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/security/authentication/IdentityFilter.java b/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/security/authentication/IdentityFilter.java
new file mode 100644
index 0000000..cd5e2bf
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/security/authentication/IdentityFilter.java
@@ -0,0 +1,97 @@
+/*
+ * 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.security.authentication;
+
+import org.apache.nifi.registry.security.authentication.AuthenticationRequest;
+import org.apache.nifi.registry.security.authentication.IdentityProvider;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.security.core.Authentication;
+import org.springframework.security.core.context.SecurityContextHolder;
+import org.springframework.web.filter.GenericFilterBean;
+
+import javax.servlet.FilterChain;
+import javax.servlet.ServletException;
+import javax.servlet.ServletRequest;
+import javax.servlet.ServletResponse;
+import javax.servlet.http.HttpServletRequest;
+import java.io.IOException;
+
+/**
+ * A class that will extract an identity / credentials claim from an HttpServlet Request using an injected IdentityProvider.
+ *
+ * This class is designed to be used in collaboration with an {@link IdentityAuthenticationProvider}. The identity/credentials will be
+ * extracted by this filter and later validated by the {@link IdentityAuthenticationProvider} in the default SecurityInterceptorFilter.
+ */
+public class IdentityFilter extends GenericFilterBean {
+
+    private static final Logger logger = LoggerFactory.getLogger(IdentityFilter.class);
+
+    private final IdentityProvider identityProvider;
+
+    public IdentityFilter(IdentityProvider identityProvider) {
+        this.identityProvider = identityProvider;
+    }
+
+    @Override
+    public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
+
+        // Only require authentication from an identity provider if the NiFi registry is running securely.
+        if (!servletRequest.isSecure()) {
+            // Otherwise, requests will be "authenticated" by the AnonymousIdentityFilter
+            filterChain.doFilter(servletRequest, servletResponse);
+            return;
+        }
+
+        if (identityProvider == null) {
+            logger.warn("Identity Filter configured with NULL identity provider. Credentials will not be extracted.");
+            filterChain.doFilter(servletRequest, servletResponse);
+            return;
+        }
+
+        if (credentialsAlreadyPresent()) {
+            logger.debug("Credentials already extracted for [{}], skipping credentials extraction filter using {}",
+                    SecurityContextHolder.getContext().getAuthentication().getPrincipal().toString(),
+                    identityProvider.getClass().getSimpleName());
+            filterChain.doFilter(servletRequest, servletResponse);
+            return;
+        }
+
+        logger.debug("Attempting to extract user credentials using {}", identityProvider.getClass().getSimpleName());
+
+        try {
+            AuthenticationRequest authenticationRequest = identityProvider.extractCredentials((HttpServletRequest)servletRequest);
+            if (authenticationRequest != null) {
+                Authentication authentication = new AuthenticationRequestToken(authenticationRequest, identityProvider.getClass(), servletRequest.getRemoteAddr());
+                logger.debug("Adding credentials claim to SecurityContext to be authenticated. Credentials extracted by {}: {}",
+                        identityProvider.getClass().getSimpleName(),
+                        authenticationRequest);
+                SecurityContextHolder.getContext().setAuthentication(authentication);
+                // This filter's job, which is merely to search for and extract an identity claim, is done.
+                // The actual authentication of the identity claim will be handled by a corresponding IdentityAuthenticationProvider
+            }
+        } catch (Exception e) {
+            logger.debug("Exception occurred while extracting credentials:", e);
+        }
+
+        filterChain.doFilter(servletRequest, servletResponse);
+    }
+
+    private boolean credentialsAlreadyPresent() {
+        return SecurityContextHolder.getContext().getAuthentication() != 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/security/authentication/exception/InvalidAuthenticationException.java
----------------------------------------------------------------------
diff --git a/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/security/authentication/exception/InvalidAuthenticationException.java b/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/security/authentication/exception/InvalidAuthenticationException.java
new file mode 100644
index 0000000..016e9cb
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/security/authentication/exception/InvalidAuthenticationException.java
@@ -0,0 +1,35 @@
+/*
+ * 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.security.authentication.exception;
+
+import org.springframework.security.core.AuthenticationException;
+
+/**
+ * Thrown if the authentication of a given request is invalid. For instance,
+ * an expired certificate or token.
+ */
+public class InvalidAuthenticationException extends AuthenticationException {
+
+    public InvalidAuthenticationException(String msg) {
+        super(msg);
+    }
+
+    public InvalidAuthenticationException(String msg, Throwable t) {
+        super(msg, t);
+    }
+
+}

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/security/authentication/exception/UntrustedProxyException.java
----------------------------------------------------------------------
diff --git a/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/security/authentication/exception/UntrustedProxyException.java b/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/security/authentication/exception/UntrustedProxyException.java
new file mode 100644
index 0000000..82570a3
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/security/authentication/exception/UntrustedProxyException.java
@@ -0,0 +1,31 @@
+/*
+ * 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.security.authentication.exception;
+
+import org.springframework.security.core.AuthenticationException;
+
+public class UntrustedProxyException extends AuthenticationException {
+
+    public UntrustedProxyException(String msg) {
+        super(msg);
+    }
+
+    public UntrustedProxyException(String msg, Throwable t) {
+        super(msg, t);
+    }
+
+}

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/security/authentication/jwt/JwtIdentityProvider.java
----------------------------------------------------------------------
diff --git a/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/security/authentication/jwt/JwtIdentityProvider.java b/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/security/authentication/jwt/JwtIdentityProvider.java
new file mode 100644
index 0000000..d3f12c9
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/security/authentication/jwt/JwtIdentityProvider.java
@@ -0,0 +1,85 @@
+/*
+ * 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.security.authentication.jwt;
+
+import io.jsonwebtoken.JwtException;
+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.BearerAuthIdentityProvider;
+import org.apache.nifi.registry.security.authentication.IdentityProvider;
+import org.apache.nifi.registry.security.authentication.IdentityProviderConfigurationContext;
+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.Authorizer;
+import org.apache.nifi.registry.security.exception.SecurityProviderCreationException;
+import org.apache.nifi.registry.security.exception.SecurityProviderDestructionException;
+import org.apache.nifi.registry.web.security.authentication.exception.InvalidAuthenticationException;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Component;
+
+import java.util.concurrent.TimeUnit;
+
+@Component
+public class JwtIdentityProvider extends BearerAuthIdentityProvider implements IdentityProvider {
+
+    private static final Logger logger = LoggerFactory.getLogger(JwtIdentityProvider.class);
+
+    private static final String issuer = JwtIdentityProvider.class.getSimpleName();
+
+    private static final long expiration = TimeUnit.MILLISECONDS.convert(12, TimeUnit.HOURS);
+
+    private final JwtService jwtService;
+
+    @Autowired
+    public JwtIdentityProvider(JwtService jwtService, NiFiRegistryProperties nifiProperties, Authorizer authorizer) {
+        this.jwtService = jwtService;
+    }
+
+    @Override
+    public AuthenticationResponse authenticate(AuthenticationRequest authenticationRequest) throws InvalidCredentialsException, IdentityAccessException {
+
+        if (authenticationRequest == null) {
+            logger.info("Cannot authenticate null authenticationRequest, returning null.");
+            return null;
+        }
+
+        final Object credentials = authenticationRequest.getCredentials();
+        String jwtAuthToken = credentials != null && credentials instanceof String ? (String) credentials : null;
+
+        if (credentials == null) {
+            logger.info("JWT not found in authenticationRequest credentials, returning null.");
+            return null;
+        }
+
+        try {
+            final String jwtPrincipal = jwtService.getAuthenticationFromToken(jwtAuthToken);
+            return new AuthenticationResponse(jwtPrincipal, jwtPrincipal, expiration, issuer);
+        } catch (JwtException e) {
+            throw new InvalidAuthenticationException(e.getMessage(), e);
+        }
+    }
+
+    @Override
+    public void onConfigured(IdentityProviderConfigurationContext configurationContext) throws SecurityProviderCreationException {}
+
+    @Override
+    public void preDestruction() throws SecurityProviderDestructionException {}
+
+}

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/security/authentication/jwt/JwtService.java
----------------------------------------------------------------------
diff --git a/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/security/authentication/jwt/JwtService.java b/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/security/authentication/jwt/JwtService.java
new file mode 100644
index 0000000..d47b301
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/security/authentication/jwt/JwtService.java
@@ -0,0 +1,212 @@
+/*
+ * 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.security.authentication.jwt;
+
+import io.jsonwebtoken.Claims;
+import io.jsonwebtoken.ExpiredJwtException;
+import io.jsonwebtoken.Jws;
+import io.jsonwebtoken.JwsHeader;
+import io.jsonwebtoken.JwtException;
+import io.jsonwebtoken.Jwts;
+import io.jsonwebtoken.MalformedJwtException;
+import io.jsonwebtoken.SignatureAlgorithm;
+import io.jsonwebtoken.SignatureException;
+import io.jsonwebtoken.SigningKeyResolverAdapter;
+import io.jsonwebtoken.UnsupportedJwtException;
+import org.apache.commons.lang3.StringUtils;
+import org.apache.nifi.registry.security.authentication.AuthenticationResponse;
+import org.apache.nifi.registry.security.key.Key;
+import org.apache.nifi.registry.security.key.KeyService;
+import org.slf4j.LoggerFactory;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+
+import java.nio.charset.StandardCharsets;
+import java.text.SimpleDateFormat;
+import java.util.Calendar;
+import java.util.concurrent.TimeUnit;
+
+// TODO, look into replacing this JwtService service with Apache Licensed JJWT library
+@Service
+public class JwtService {
+
+    private static final org.slf4j.Logger logger = LoggerFactory.getLogger(JwtService.class);
+
+    private static final SignatureAlgorithm SIGNATURE_ALGORITHM = SignatureAlgorithm.HS256;
+    private static final String KEY_ID_CLAIM = "kid";
+    private static final String USERNAME_CLAIM = "preferred_username";
+
+    private final KeyService keyService;
+
+    @Autowired
+    public JwtService(final KeyService keyService) {
+        this.keyService = keyService;
+    }
+
+    public String getAuthenticationFromToken(final String base64EncodedToken) throws JwtException {
+        // The library representations of the JWT should be kept internal to this service.
+        try {
+            final Jws<Claims> jws = parseTokenFromBase64EncodedString(base64EncodedToken);
+
+            if (jws == null) {
+                throw new JwtException("Unable to parse token");
+            }
+
+            // Additional validation that subject is present
+            if (StringUtils.isEmpty(jws.getBody().getSubject())) {
+                throw new JwtException("No subject available in token");
+            }
+
+            // TODO: Validate issuer against active IdentityProvider?
+            if (StringUtils.isEmpty(jws.getBody().getIssuer())) {
+                throw new JwtException("No issuer available in token");
+            }
+            return jws.getBody().getSubject();
+        } catch (JwtException e) {
+            logger.debug("The Base64 encoded JWT: " + base64EncodedToken);
+            final String errorMessage = "There was an error validating the JWT";
+            logger.error(errorMessage, e);
+            throw e;
+        }
+    }
+
+    private Jws<Claims> parseTokenFromBase64EncodedString(final String base64EncodedToken) throws JwtException {
+        try {
+            return Jwts.parser().setSigningKeyResolver(new SigningKeyResolverAdapter() {
+                @Override
+                public byte[] resolveSigningKeyBytes(JwsHeader header, Claims claims) {
+                    final String identity = claims.getSubject();
+
+                    // Get the key based on the key id in the claims
+                    final String keyId = claims.get(KEY_ID_CLAIM, String.class);
+                    final Key key = keyService.getKey(keyId);
+
+                    // Ensure we were able to find a key that was previously issued by this key service for this user
+                    if (key == null || key.getKey() == null) {
+                        throw new UnsupportedJwtException("Unable to determine signing key for " + identity + " [kid: " + keyId + "]");
+                    }
+
+                    return key.getKey().getBytes(StandardCharsets.UTF_8);
+                }
+            }).parseClaimsJws(base64EncodedToken);
+        } catch (final MalformedJwtException | UnsupportedJwtException | SignatureException | ExpiredJwtException | IllegalArgumentException e) {
+            // TODO: Exercise all exceptions to ensure none leak key material to logs
+            final String errorMessage = "Unable to validate the access token.";
+            throw new JwtException(errorMessage, e);
+        }
+    }
+
+    /**
+     * Generates a signed JWT token from the provided IdentityProvider AuthenticationResponse
+     *
+     * @param authenticationResponse an instance issued by an IdentityProvider after identity claim has been verified as authentic
+     * @return a signed JWT containing the user identity and the identity provider, Base64-encoded
+     * @throws JwtException if there is a problem generating the signed token
+     */
+    public String generateSignedToken(final AuthenticationResponse authenticationResponse) throws JwtException {
+        if (authenticationResponse == null) {
+            throw new IllegalArgumentException("Cannot generate a JWT for a null authenticationResponse");
+        }
+
+        return generateSignedToken(
+                authenticationResponse.getIdentity(),
+                authenticationResponse.getUsername(),
+                authenticationResponse.getIssuer(),
+                authenticationResponse.getIssuer(),
+                authenticationResponse.getExpiration());
+    }
+
+    public String generateSignedToken(String identity, String preferredUsername, String issuer, String audience, long expirationMillis) throws JwtException {
+
+        if (identity == null || StringUtils.isEmpty(identity)) {
+            String errorMessage = "Cannot generate a JWT for a token with an empty identity";
+            errorMessage = issuer != null ? errorMessage + " issued by " + issuer + "." : ".";
+            logger.error(errorMessage);
+            throw new IllegalArgumentException(errorMessage);
+        }
+
+        // Compute expiration
+        final Calendar now = Calendar.getInstance();
+        long expirationMillisRelativeToNow = validateTokenExpiration(expirationMillis, identity);
+        long expirationMillisSinceEpoch = now.getTimeInMillis() + expirationMillisRelativeToNow;
+        final Calendar expiration = new Calendar.Builder().setInstant(expirationMillisSinceEpoch).build();
+
+        try {
+            // Get/create the key for this user
+            final Key key = keyService.getOrCreateKey(identity);
+            final byte[] keyBytes = key.getKey().getBytes(StandardCharsets.UTF_8);
+
+            //logger.trace("Generating JWT for " + describe(authenticationResponse));
+
+            // TODO: Implement "jti" claim with nonce to prevent replay attacks and allow blacklisting of revoked tokens
+            // Build the token
+            return Jwts.builder().setSubject(identity)
+                    .setIssuer(issuer)
+                    .setAudience(audience)
+                    .claim(USERNAME_CLAIM, preferredUsername)
+                    .claim(KEY_ID_CLAIM, key.getId())
+                    .setIssuedAt(now.getTime())
+                    .setExpiration(expiration.getTime())
+                    .signWith(SIGNATURE_ALGORITHM, keyBytes).compact();
+        } catch (NullPointerException e) {
+            final String errorMessage = "Could not retrieve the signing key for JWT for " + identity;
+            logger.error(errorMessage, e);
+            throw new JwtException(errorMessage, e);
+        }
+
+    }
+
+    private static long validateTokenExpiration(long proposedTokenExpiration, String identity) {
+        final long maxExpiration = TimeUnit.MILLISECONDS.convert(12, TimeUnit.HOURS);
+        final long minExpiration = TimeUnit.MILLISECONDS.convert(1, TimeUnit.MINUTES);
+
+        if (proposedTokenExpiration > maxExpiration) {
+            logger.warn(String.format("Max token expiration exceeded. Setting expiration to %s from %s for %s", maxExpiration,
+                    proposedTokenExpiration, identity));
+            proposedTokenExpiration = maxExpiration;
+        } else if (proposedTokenExpiration < minExpiration) {
+            logger.warn(String.format("Min token expiration not met. Setting expiration to %s from %s for %s", minExpiration,
+                    proposedTokenExpiration, identity));
+            proposedTokenExpiration = minExpiration;
+        }
+
+        return proposedTokenExpiration;
+    }
+
+    private static String describe(AuthenticationResponse authenticationResponse) {
+        Calendar expirationTime = Calendar.getInstance();
+        expirationTime.setTimeInMillis(authenticationResponse.getExpiration());
+        long remainingTime = expirationTime.getTimeInMillis() - Calendar.getInstance().getTimeInMillis();
+
+        SimpleDateFormat dateFormat = new SimpleDateFormat("dd-MM-yyyy HH:mm:ss.SSS");
+        dateFormat.setTimeZone(expirationTime.getTimeZone());
+        String expirationTimeString = dateFormat.format(expirationTime.getTime());
+
+        return new StringBuilder("LoginAuthenticationToken for ")
+                .append(authenticationResponse.getUsername())
+                .append(" issued by ")
+                .append(authenticationResponse.getIssuer())
+                .append(" expiring at ")
+                .append(expirationTimeString)
+                .append(" [")
+                .append(authenticationResponse.getExpiration())
+                .append(" ms, ")
+                .append(remainingTime)
+                .append(" ms remaining]")
+                .toString();
+    }
+}


[36/51] [partial] nifi-registry git commit: NIFIREG-201 Refactoring project structure to better isolate extensions

Posted by kd...@apache.org.
http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/6f26290d/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/security/authorization/AuthorizerFactory.java
----------------------------------------------------------------------
diff --git a/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/security/authorization/AuthorizerFactory.java b/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/security/authorization/AuthorizerFactory.java
new file mode 100644
index 0000000..43862fe
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/security/authorization/AuthorizerFactory.java
@@ -0,0 +1,915 @@
+/*
+ * 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.security.authorization;
+
+import org.apache.commons.lang3.StringUtils;
+import org.apache.nifi.registry.extension.ExtensionCloseable;
+import org.apache.nifi.registry.extension.ExtensionManager;
+import org.apache.nifi.registry.properties.NiFiRegistryProperties;
+import org.apache.nifi.registry.properties.SensitivePropertyProtectionException;
+import org.apache.nifi.registry.properties.SensitivePropertyProvider;
+import org.apache.nifi.registry.provider.StandardProviderFactory;
+import org.apache.nifi.registry.security.authorization.annotation.AuthorizerContext;
+import org.apache.nifi.registry.security.authorization.exception.AuthorizationAccessException;
+import org.apache.nifi.registry.security.authorization.exception.UninheritableAuthorizationsException;
+import org.apache.nifi.registry.security.authorization.generated.Authorizers;
+import org.apache.nifi.registry.security.authorization.generated.Prop;
+import org.apache.nifi.registry.security.exception.SecurityProviderCreationException;
+import org.apache.nifi.registry.security.exception.SecurityProviderDestructionException;
+import org.apache.nifi.registry.security.util.XmlUtils;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.beans.factory.DisposableBean;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.lang.Nullable;
+import org.xml.sax.SAXException;
+
+import javax.xml.XMLConstants;
+import javax.xml.bind.JAXBContext;
+import javax.xml.bind.JAXBElement;
+import javax.xml.bind.JAXBException;
+import javax.xml.bind.Unmarshaller;
+import javax.xml.stream.XMLStreamException;
+import javax.xml.transform.stream.StreamSource;
+import javax.xml.validation.Schema;
+import javax.xml.validation.SchemaFactory;
+import java.io.File;
+import java.lang.reflect.Constructor;
+import java.lang.reflect.Field;
+import java.lang.reflect.InvocationTargetException;
+import java.lang.reflect.Method;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+/**
+ * Creates and configures Authorizers and their providers based on the configuration (authorizers.xml).
+ *
+ * This implementation of AuthorizerFactory in NiFi Registry is based on a combination of
+ * NiFi's AuthorizerFactory and AuthorizerFactoryBean.
+ */
+@Configuration("authorizerFactory")
+public class AuthorizerFactory implements UserGroupProviderLookup, AccessPolicyProviderLookup, AuthorizerLookup, DisposableBean {
+
+    private static final Logger logger = LoggerFactory.getLogger(StandardProviderFactory.class);
+
+    private static final String AUTHORIZERS_XSD = "/authorizers.xsd";
+    private static final String JAXB_GENERATED_PATH = "org.apache.nifi.registry.security.authorization.generated";
+    private static final JAXBContext JAXB_CONTEXT = initializeJaxbContext();
+
+    /**
+     * Load the JAXBContext.
+     */
+    private static JAXBContext initializeJaxbContext() {
+        try {
+            return JAXBContext.newInstance(JAXB_GENERATED_PATH, AuthorizerFactory.class.getClassLoader());
+        } catch (JAXBException e) {
+            throw new RuntimeException("Unable to create JAXBContext.", e);
+        }
+    }
+
+    private final NiFiRegistryProperties properties;
+    private final ExtensionManager extensionManager;
+    private final SensitivePropertyProvider sensitivePropertyProvider;
+
+    private Authorizer authorizer;
+    private final Map<String, UserGroupProvider> userGroupProviders = new HashMap<>();
+    private final Map<String, AccessPolicyProvider> accessPolicyProviders = new HashMap<>();
+    private final Map<String, Authorizer> authorizers = new HashMap<>();
+
+    @Autowired
+    public AuthorizerFactory(
+            final NiFiRegistryProperties properties,
+            final ExtensionManager extensionManager,
+            @Nullable final SensitivePropertyProvider sensitivePropertyProvider) {
+
+        this.properties = properties;
+        this.extensionManager = extensionManager;
+        this.sensitivePropertyProvider = sensitivePropertyProvider;
+
+        if (this.properties == null) {
+            throw new IllegalStateException("NiFiRegistryProperties cannot be null");
+        }
+
+        if (this.extensionManager == null) {
+            throw new IllegalStateException("ExtensionManager cannot be null");
+        }
+    }
+
+    /***** UserGroupProviderLookup *****/
+
+    @Override
+    public UserGroupProvider getUserGroupProvider(String identifier) {
+        return userGroupProviders.get(identifier);
+    }
+
+    /***** AccessPolicyProviderLookup *****/
+
+    @Override
+    public AccessPolicyProvider getAccessPolicyProvider(String identifier) {
+        return accessPolicyProviders.get(identifier);
+    }
+
+
+    /***** AuthorizerLookup *****/
+
+    @Override
+    public Authorizer getAuthorizer(String identifier) {
+        return authorizers.get(identifier);
+    }
+
+    /***** AuthorizerFactory / DisposableBean *****/
+
+    @Bean
+    public Authorizer getAuthorizer() throws AuthorizerFactoryException {
+        if (authorizer == null) {
+            if (properties.getSslPort() == null) {
+                // use a default authorizer... only allowable when running not securely
+                authorizer = createDefaultAuthorizer();
+            } else {
+                // look up the authorizer to use
+                final String authorizerIdentifier = properties.getProperty(NiFiRegistryProperties.SECURITY_AUTHORIZER);
+
+                // ensure the authorizer class name was specified
+                if (StringUtils.isBlank(authorizerIdentifier)) {
+                    throw new AuthorizerFactoryException("When running securely, the authorizer identifier must be specified in the nifi-registry.properties file.");
+                } else {
+
+                    try {
+                        final Authorizers authorizerConfiguration = loadAuthorizersConfiguration();
+
+                        // create each user group provider
+                        for (final org.apache.nifi.registry.security.authorization.generated.UserGroupProvider userGroupProvider : authorizerConfiguration.getUserGroupProvider()) {
+                            if (userGroupProviders.containsKey(userGroupProvider.getIdentifier())) {
+                                throw new AuthorizerFactoryException("Duplicate User Group Provider identifier in Authorizers configuration: " + userGroupProvider.getIdentifier());
+                            }
+                            userGroupProviders.put(userGroupProvider.getIdentifier(), createUserGroupProvider(userGroupProvider.getIdentifier(), userGroupProvider.getClazz()));
+                        }
+
+                        // configure each user group provider
+                        for (final org.apache.nifi.registry.security.authorization.generated.UserGroupProvider provider : authorizerConfiguration.getUserGroupProvider()) {
+                            final UserGroupProvider instance = userGroupProviders.get(provider.getIdentifier());
+                            instance.onConfigured(loadAuthorizerConfiguration(provider.getIdentifier(), provider.getProperty()));
+                        }
+
+                        // create each access policy provider
+                        for (final org.apache.nifi.registry.security.authorization.generated.AccessPolicyProvider accessPolicyProvider : authorizerConfiguration.getAccessPolicyProvider()) {
+                            if (accessPolicyProviders.containsKey(accessPolicyProvider.getIdentifier())) {
+                                throw new AuthorizerFactoryException("Duplicate Access Policy Provider identifier in Authorizers configuration: " + accessPolicyProvider.getIdentifier());
+                            }
+                            accessPolicyProviders.put(accessPolicyProvider.getIdentifier(), createAccessPolicyProvider(accessPolicyProvider.getIdentifier(), accessPolicyProvider.getClazz()));
+                        }
+
+                        // configure each access policy provider
+                        for (final org.apache.nifi.registry.security.authorization.generated.AccessPolicyProvider provider : authorizerConfiguration.getAccessPolicyProvider()) {
+                            final AccessPolicyProvider instance = accessPolicyProviders.get(provider.getIdentifier());
+                            instance.onConfigured(loadAuthorizerConfiguration(provider.getIdentifier(), provider.getProperty()));
+                        }
+
+                        // create each authorizer
+                        for (final org.apache.nifi.registry.security.authorization.generated.Authorizer authorizer : authorizerConfiguration.getAuthorizer()) {
+                            if (authorizers.containsKey(authorizer.getIdentifier())) {
+                                throw new AuthorizerFactoryException("Duplicate Authorizer identifier in Authorizers configuration: " + authorizer.getIdentifier());
+                            }
+                            authorizers.put(authorizer.getIdentifier(), createAuthorizer(authorizer.getIdentifier(), authorizer.getClazz(), authorizer.getClasspath()));
+                        }
+
+                        // configure each authorizer
+                        for (final org.apache.nifi.registry.security.authorization.generated.Authorizer provider : authorizerConfiguration.getAuthorizer()) {
+                            final Authorizer instance = authorizers.get(provider.getIdentifier());
+                            final Class authorizerClass = instance instanceof WrappedAuthorizer
+                                    ? ((WrappedAuthorizer) instance).getBaseAuthorizer().getClass()
+                                    : instance.getClass();
+                            try (ExtensionCloseable extCloseable = ExtensionCloseable.withComponentClassLoader(extensionManager, authorizerClass)) {
+                                instance.onConfigured(loadAuthorizerConfiguration(provider.getIdentifier(), provider.getProperty()));
+                            }
+                        }
+
+                        // get the authorizer instance
+                        authorizer = getAuthorizer(authorizerIdentifier);
+
+                        // ensure it was found
+                        if (authorizer == null) {
+                            throw new AuthorizerFactoryException(String.format("The specified authorizer '%s' could not be found.", authorizerIdentifier));
+                        }
+                    } catch (AuthorizerFactoryException e) {
+                        throw e;
+                    } catch (Exception e) {
+                        throw new AuthorizerFactoryException("Failed to construct Authorizer.", e);
+                    }
+                }
+            }
+        }
+        return authorizer;
+    }
+
+    @Override
+    public void destroy() throws Exception {
+        if (authorizers != null) {
+            authorizers.forEach((key, value) -> value.preDestruction());
+        }
+
+        if (accessPolicyProviders != null) {
+            accessPolicyProviders.forEach((key, value) -> value.preDestruction());
+        }
+
+        if (userGroupProviders != null) {
+            userGroupProviders.forEach((key, value) -> value.preDestruction());
+        }
+    }
+
+    private Authorizers loadAuthorizersConfiguration() throws Exception {
+        final File authorizersConfigurationFile = properties.getAuthorizersConfigurationFile();
+
+        // load the authorizers from the specified file
+        if (authorizersConfigurationFile.exists()) {
+            try {
+                // find the schema
+                final SchemaFactory schemaFactory = SchemaFactory.newInstance(XMLConstants.W3C_XML_SCHEMA_NS_URI);
+                final Schema schema = schemaFactory.newSchema(Authorizers.class.getResource(AUTHORIZERS_XSD));
+
+                // attempt to unmarshal
+                final Unmarshaller unmarshaller = JAXB_CONTEXT.createUnmarshaller();
+                unmarshaller.setSchema(schema);
+                final JAXBElement<Authorizers> element = unmarshaller.unmarshal(XmlUtils.createSafeReader(new StreamSource(authorizersConfigurationFile)), Authorizers.class);
+                return element.getValue();
+            } catch (XMLStreamException | SAXException | JAXBException e) {
+                throw new Exception("Unable to load the authorizer configuration file at: " + authorizersConfigurationFile.getAbsolutePath(), e);
+            }
+        } else {
+            throw new Exception("Unable to find the authorizer configuration file at " + authorizersConfigurationFile.getAbsolutePath());
+        }
+    }
+
+    private AuthorizerConfigurationContext loadAuthorizerConfiguration(final String identifier, final List<Prop> properties) {
+        final Map<String, String> authorizerProperties = new HashMap<>();
+
+        for (final Prop property : properties) {
+            if (!StringUtils.isBlank(property.getEncryption())) {
+                String decryptedValue = decryptValue(property.getValue(), property.getEncryption());
+                authorizerProperties.put(property.getName(), decryptedValue);
+            } else {
+                authorizerProperties.put(property.getName(), property.getValue());
+            }
+        }
+        return new StandardAuthorizerConfigurationContext(identifier, authorizerProperties);
+    }
+
+    private UserGroupProvider createUserGroupProvider(final String identifier, final String userGroupProviderClassName) throws Exception {
+
+        final UserGroupProvider instance;
+
+        final ClassLoader classLoader = extensionManager.getExtensionClassLoader(userGroupProviderClassName);
+        if (classLoader == null) {
+            throw new IllegalStateException("Extension not found in any of the configured class loaders: " + userGroupProviderClassName);
+        }
+
+        // attempt to load the class
+        Class<?> rawUserGroupProviderClass = Class.forName(userGroupProviderClassName, true, classLoader);
+        Class<? extends UserGroupProvider> userGroupProviderClass = rawUserGroupProviderClass.asSubclass(UserGroupProvider.class);
+
+        // otherwise create a new instance
+        Constructor constructor = userGroupProviderClass.getConstructor();
+        instance = (UserGroupProvider) constructor.newInstance();
+
+        // method injection
+        performMethodInjection(instance, userGroupProviderClass);
+
+        // field injection
+        performFieldInjection(instance, userGroupProviderClass);
+
+        // call post construction lifecycle event
+        instance.initialize(new StandardAuthorizerInitializationContext(identifier, this, this, this));
+
+        return instance;
+    }
+
+    private AccessPolicyProvider createAccessPolicyProvider(final String identifier, final String accessPolicyProviderClassName) throws Exception {
+        final AccessPolicyProvider instance;
+
+        final ClassLoader classLoader = extensionManager.getExtensionClassLoader(accessPolicyProviderClassName);
+        if (classLoader == null) {
+            throw new IllegalStateException("Extension not found in any of the configured class loaders: " + accessPolicyProviderClassName);
+        }
+
+        // attempt to load the class
+        Class<?> rawAccessPolicyProviderClass = Class.forName(accessPolicyProviderClassName, true, classLoader);
+        Class<? extends AccessPolicyProvider> accessPolicyClass = rawAccessPolicyProviderClass.asSubclass(AccessPolicyProvider.class);
+
+        // otherwise create a new instance
+        Constructor constructor = accessPolicyClass.getConstructor();
+        instance = (AccessPolicyProvider) constructor.newInstance();
+
+        // method injection
+        performMethodInjection(instance, accessPolicyClass);
+
+        // field injection
+        performFieldInjection(instance, accessPolicyClass);
+
+        // call post construction lifecycle event
+        instance.initialize(new StandardAuthorizerInitializationContext(identifier, this, this, this));
+
+        return instance;
+    }
+
+    private Authorizer createAuthorizer(final String identifier, final String authorizerClassName, final String classpathResources) throws Exception {
+        final Authorizer instance;
+
+        final ClassLoader classLoader = extensionManager.getExtensionClassLoader(authorizerClassName);
+        if (classLoader == null) {
+            throw new IllegalStateException("Extension not found in any of the configured class loaders: " + authorizerClassName);
+        }
+
+        // attempt to load the class
+        Class<?> rawAuthorizerClass = Class.forName(authorizerClassName, true, classLoader);
+        Class<? extends Authorizer> authorizerClass = rawAuthorizerClass.asSubclass(Authorizer.class);
+
+        // otherwise create a new instance
+        Constructor constructor = authorizerClass.getConstructor();
+        instance = (Authorizer) constructor.newInstance();
+
+        // method injection
+        performMethodInjection(instance, authorizerClass);
+
+        // field injection
+        performFieldInjection(instance, authorizerClass);
+
+        // call post construction lifecycle event
+        instance.initialize(new StandardAuthorizerInitializationContext(identifier, this, this, this));
+
+        return installIntegrityChecks(instance);
+    }
+
+        private void performMethodInjection(final Object instance, final Class authorizerClass) throws IllegalAccessException, IllegalArgumentException, InvocationTargetException {
+        for (final Method method : authorizerClass.getMethods()) {
+            if (method.isAnnotationPresent(AuthorizerContext.class)) {
+                // make the method accessible
+                final boolean isAccessible = method.isAccessible();
+                method.setAccessible(true);
+
+                try {
+                    final Class<?>[] argumentTypes = method.getParameterTypes();
+
+                    // look for setters (single argument)
+                    if (argumentTypes.length == 1) {
+                        final Class<?> argumentType = argumentTypes[0];
+
+                        // look for well known types
+                        if (NiFiRegistryProperties.class.isAssignableFrom(argumentType)) {
+                            // nifi properties injection
+                            method.invoke(instance, properties);
+                        }
+                    }
+                } finally {
+                    method.setAccessible(isAccessible);
+                }
+            }
+        }
+
+        final Class parentClass = authorizerClass.getSuperclass();
+        if (parentClass != null && Authorizer.class.isAssignableFrom(parentClass)) {
+            performMethodInjection(instance, parentClass);
+        }
+    }
+
+    private void performFieldInjection(final Object instance, final Class authorizerClass) throws IllegalArgumentException, IllegalAccessException {
+        for (final Field field : authorizerClass.getDeclaredFields()) {
+            if (field.isAnnotationPresent(AuthorizerContext.class)) {
+                // make the method accessible
+                final boolean isAccessible = field.isAccessible();
+                field.setAccessible(true);
+
+                try {
+                    // get the type
+                    final Class<?> fieldType = field.getType();
+
+                    // only consider this field if it isn't set yet
+                    if (field.get(instance) == null) {
+                        // look for well known types
+                        if (NiFiRegistryProperties.class.isAssignableFrom(fieldType)) {
+                            // nifi properties injection
+                            field.set(instance, properties);
+                        }
+                    }
+
+                } finally {
+                    field.setAccessible(isAccessible);
+                }
+            }
+        }
+
+        final Class parentClass = authorizerClass.getSuperclass();
+        if (parentClass != null && Authorizer.class.isAssignableFrom(parentClass)) {
+            performFieldInjection(instance, parentClass);
+        }
+    }
+
+    private String decryptValue(String cipherText, String encryptionScheme) throws SensitivePropertyProtectionException {
+        if (sensitivePropertyProvider == null) {
+            throw new SensitivePropertyProtectionException("Sensitive Property Provider dependency was never wired, so protected" +
+                    "properties cannot be decrypted. This usually indicates that a master key for this NiFi Registry was not " +
+                    "detected and configured during the bootstrap startup sequence. Contact the system administrator.");
+        }
+
+        if (!sensitivePropertyProvider.getIdentifierKey().equalsIgnoreCase(encryptionScheme)) {
+            throw new SensitivePropertyProtectionException("Identity Provider configuration XML was protected using " +
+                    encryptionScheme +
+                    ", but the configured Sensitive Property Provider supports " +
+                    sensitivePropertyProvider.getIdentifierKey() +
+                    ". Cannot configure this Identity Provider due to failing to decrypt protected configuration properties.");
+        }
+
+        return sensitivePropertyProvider.unprotect(cipherText);
+    }
+
+
+    /**
+     * @return a default Authorizer to use when running unsecurely with no authorizer configured
+     */
+    private Authorizer createDefaultAuthorizer() {
+        return new Authorizer() {
+            @Override
+            public AuthorizationResult authorize(final AuthorizationRequest request) throws AuthorizationAccessException {
+                return AuthorizationResult.approved();
+            }
+
+            @Override
+            public void initialize(AuthorizerInitializationContext initializationContext) throws SecurityProviderCreationException {
+            }
+
+            @Override
+            public void onConfigured(AuthorizerConfigurationContext configurationContext) throws SecurityProviderCreationException {
+            }
+
+            @Override
+            public void preDestruction() throws SecurityProviderCreationException {
+            }
+        };
+    }
+
+    private interface WrappedAuthorizer {
+        Authorizer getBaseAuthorizer();
+    }
+
+    private static class ManagedAuthorizerWrapper implements ManagedAuthorizer, WrappedAuthorizer {
+        private final ManagedAuthorizer baseManagedAuthorizer;
+
+        public ManagedAuthorizerWrapper(ManagedAuthorizer baseManagedAuthorizer) {
+            this.baseManagedAuthorizer = baseManagedAuthorizer;
+        }
+
+        @Override
+        public Authorizer getBaseAuthorizer() {
+            return baseManagedAuthorizer;
+        }
+
+        @Override
+        public String getFingerprint() throws AuthorizationAccessException {
+            return baseManagedAuthorizer.getFingerprint();
+        }
+
+        @Override
+        public void inheritFingerprint(String fingerprint) throws AuthorizationAccessException {
+            baseManagedAuthorizer.inheritFingerprint(fingerprint);
+        }
+
+        @Override
+        public void checkInheritability(String proposedFingerprint) throws AuthorizationAccessException, UninheritableAuthorizationsException {
+            baseManagedAuthorizer.checkInheritability(proposedFingerprint);
+        }
+
+        @Override
+        public AccessPolicyProvider getAccessPolicyProvider() {
+            final AccessPolicyProvider baseAccessPolicyProvider = baseManagedAuthorizer.getAccessPolicyProvider();
+            if (baseAccessPolicyProvider instanceof ConfigurableAccessPolicyProvider) {
+                final ConfigurableAccessPolicyProvider baseConfigurableAccessPolicyProvider = (ConfigurableAccessPolicyProvider) baseAccessPolicyProvider;
+                return new ConfigurableAccessPolicyProvider() {
+                    @Override
+                    public String getFingerprint() throws AuthorizationAccessException {
+                        return baseConfigurableAccessPolicyProvider.getFingerprint();
+                    }
+
+                    @Override
+                    public void inheritFingerprint(String fingerprint) throws AuthorizationAccessException {
+                        baseConfigurableAccessPolicyProvider.inheritFingerprint(fingerprint);
+                    }
+
+                    @Override
+                    public void checkInheritability(String proposedFingerprint) throws AuthorizationAccessException, UninheritableAuthorizationsException {
+                        baseConfigurableAccessPolicyProvider.checkInheritability(proposedFingerprint);
+                    }
+
+                    @Override
+                    public AccessPolicy addAccessPolicy(AccessPolicy accessPolicy) throws AuthorizationAccessException {
+                        if (policyExists(baseConfigurableAccessPolicyProvider, accessPolicy)) {
+                            throw new IllegalStateException(String.format("Found multiple policies for '%s' with '%s'.", accessPolicy.getResource(), accessPolicy.getAction()));
+                        }
+                        return baseConfigurableAccessPolicyProvider.addAccessPolicy(accessPolicy);
+                    }
+
+                    @Override
+                    public boolean isConfigurable(AccessPolicy accessPolicy) {
+                        return baseConfigurableAccessPolicyProvider.isConfigurable(accessPolicy);
+                    }
+
+                    @Override
+                    public AccessPolicy updateAccessPolicy(AccessPolicy accessPolicy) throws AuthorizationAccessException {
+                        if (!baseConfigurableAccessPolicyProvider.isConfigurable(accessPolicy)) {
+                            throw new IllegalArgumentException("The specified access policy is not support modification.");
+                        }
+                        return baseConfigurableAccessPolicyProvider.updateAccessPolicy(accessPolicy);
+                    }
+
+                    @Override
+                    public AccessPolicy deleteAccessPolicy(AccessPolicy accessPolicy) throws AuthorizationAccessException {
+                        if (!baseConfigurableAccessPolicyProvider.isConfigurable(accessPolicy)) {
+                            throw new IllegalArgumentException("The specified access policy is not support modification.");
+                        }
+                        return baseConfigurableAccessPolicyProvider.deleteAccessPolicy(accessPolicy);
+                    }
+
+                    @Override
+                    public AccessPolicy deleteAccessPolicy(String accessPolicyIdentifier) throws AuthorizationAccessException {
+                        if (!baseConfigurableAccessPolicyProvider.isConfigurable(baseConfigurableAccessPolicyProvider.getAccessPolicy(accessPolicyIdentifier))) {
+                            throw new IllegalArgumentException("The specified access policy is not support modification.");
+                        }
+                        return baseConfigurableAccessPolicyProvider.deleteAccessPolicy(accessPolicyIdentifier);
+                    }
+
+                    @Override
+                    public Set<AccessPolicy> getAccessPolicies() throws AuthorizationAccessException {
+                        return baseConfigurableAccessPolicyProvider.getAccessPolicies();
+                    }
+
+                    @Override
+                    public AccessPolicy getAccessPolicy(String identifier) throws AuthorizationAccessException {
+                        return baseConfigurableAccessPolicyProvider.getAccessPolicy(identifier);
+                    }
+
+                    @Override
+                    public AccessPolicy getAccessPolicy(String resourceIdentifier, RequestAction action) throws AuthorizationAccessException {
+                        return baseConfigurableAccessPolicyProvider.getAccessPolicy(resourceIdentifier, action);
+                    }
+
+                    @Override
+                    public UserGroupProvider getUserGroupProvider() {
+                        final UserGroupProvider baseUserGroupProvider = baseConfigurableAccessPolicyProvider.getUserGroupProvider();
+                        if (baseUserGroupProvider instanceof ConfigurableUserGroupProvider) {
+                            final ConfigurableUserGroupProvider baseConfigurableUserGroupProvider = (ConfigurableUserGroupProvider) baseUserGroupProvider;
+                            return new ConfigurableUserGroupProvider() {
+                                @Override
+                                public String getFingerprint() throws AuthorizationAccessException {
+                                    return baseConfigurableUserGroupProvider.getFingerprint();
+                                }
+
+                                @Override
+                                public void inheritFingerprint(String fingerprint) throws AuthorizationAccessException {
+                                    baseConfigurableUserGroupProvider.inheritFingerprint(fingerprint);
+                                }
+
+                                @Override
+                                public void checkInheritability(String proposedFingerprint) throws AuthorizationAccessException, UninheritableAuthorizationsException {
+                                    baseConfigurableUserGroupProvider.checkInheritability(proposedFingerprint);
+                                }
+
+                                @Override
+                                public User addUser(User user) throws AuthorizationAccessException {
+                                    if (tenantExists(baseConfigurableUserGroupProvider, user.getIdentifier(), user.getIdentity())) {
+                                        throw new IllegalStateException(String.format("User/user group already exists with the identity '%s'.", user.getIdentity()));
+                                    }
+                                    return baseConfigurableUserGroupProvider.addUser(user);
+                                }
+
+                                @Override
+                                public boolean isConfigurable(User user) {
+                                    return baseConfigurableUserGroupProvider.isConfigurable(user);
+                                }
+
+                                @Override
+                                public User updateUser(User user) throws AuthorizationAccessException {
+                                    if (tenantExists(baseConfigurableUserGroupProvider, user.getIdentifier(), user.getIdentity())) {
+                                        throw new IllegalStateException(String.format("User/user group already exists with the identity '%s'.", user.getIdentity()));
+                                    }
+                                    if (!baseConfigurableUserGroupProvider.isConfigurable(user)) {
+                                        throw new IllegalArgumentException("The specified user does not support modification.");
+                                    }
+                                    return baseConfigurableUserGroupProvider.updateUser(user);
+                                }
+
+                                @Override
+                                public User deleteUser(User user) throws AuthorizationAccessException {
+                                    if (!baseConfigurableUserGroupProvider.isConfigurable(user)) {
+                                        throw new IllegalArgumentException("The specified user does not support modification.");
+                                    }
+                                    return baseConfigurableUserGroupProvider.deleteUser(user);
+                                }
+
+                                @Override
+                                public User deleteUser(String userIdentifier) throws AuthorizationAccessException {
+                                    if (!baseConfigurableUserGroupProvider.isConfigurable(baseConfigurableUserGroupProvider.getUser(userIdentifier))) {
+                                        throw new IllegalArgumentException("The specified user does not support modification.");
+                                    }
+                                    return baseConfigurableUserGroupProvider.deleteUser(userIdentifier);
+                                }
+
+                                @Override
+                                public Group addGroup(Group group) throws AuthorizationAccessException {
+                                    if (tenantExists(baseConfigurableUserGroupProvider, group.getIdentifier(), group.getName())) {
+                                        throw new IllegalStateException(String.format("User/user group already exists with the identity '%s'.", group.getName()));
+                                    }
+                                    if (!allGroupUsersExist(baseUserGroupProvider, group)) {
+                                        throw new IllegalStateException(String.format("Cannot create group '%s' with users that don't exist.", group.getName()));
+                                    }
+                                    return baseConfigurableUserGroupProvider.addGroup(group);
+                                }
+
+                                @Override
+                                public boolean isConfigurable(Group group) {
+                                    return baseConfigurableUserGroupProvider.isConfigurable(group);
+                                }
+
+                                @Override
+                                public Group updateGroup(Group group) throws AuthorizationAccessException {
+                                    if (tenantExists(baseConfigurableUserGroupProvider, group.getIdentifier(), group.getName())) {
+                                        throw new IllegalStateException(String.format("User/user group already exists with the identity '%s'.", group.getName()));
+                                    }
+                                    if (!baseConfigurableUserGroupProvider.isConfigurable(group)) {
+                                        throw new IllegalArgumentException("The specified group does not support modification.");
+                                    }
+                                    if (!allGroupUsersExist(baseUserGroupProvider, group)) {
+                                        throw new IllegalStateException(String.format("Cannot update group '%s' to add users that don't exist.", group.getName()));
+                                    }
+                                    return baseConfigurableUserGroupProvider.updateGroup(group);
+                                }
+
+                                @Override
+                                public Group deleteGroup(Group group) throws AuthorizationAccessException {
+                                    if (!baseConfigurableUserGroupProvider.isConfigurable(group)) {
+                                        throw new IllegalArgumentException("The specified group does not support modification.");
+                                    }
+                                    return baseConfigurableUserGroupProvider.deleteGroup(group);
+                                }
+
+                                @Override
+                                public Group deleteGroup(String groupId) throws AuthorizationAccessException {
+                                    if (!baseConfigurableUserGroupProvider.isConfigurable(baseConfigurableUserGroupProvider.getGroup(groupId))) {
+                                        throw new IllegalArgumentException("The specified group does not support modification.");
+                                    }
+                                    return baseConfigurableUserGroupProvider.deleteGroup(groupId);
+                                }
+
+                                @Override
+                                public Set<User> getUsers() throws AuthorizationAccessException {
+                                    return baseConfigurableUserGroupProvider.getUsers();
+                                }
+
+                                @Override
+                                public User getUser(String identifier) throws AuthorizationAccessException {
+                                    return baseConfigurableUserGroupProvider.getUser(identifier);
+                                }
+
+                                @Override
+                                public User getUserByIdentity(String identity) throws AuthorizationAccessException {
+                                    return baseConfigurableUserGroupProvider.getUserByIdentity(identity);
+                                }
+
+                                @Override
+                                public Set<Group> getGroups() throws AuthorizationAccessException {
+                                    return baseConfigurableUserGroupProvider.getGroups();
+                                }
+
+                                @Override
+                                public Group getGroup(String identifier) throws AuthorizationAccessException {
+                                    return baseConfigurableUserGroupProvider.getGroup(identifier);
+                                }
+
+                                @Override
+                                public UserAndGroups getUserAndGroups(String identity) throws AuthorizationAccessException {
+                                    return baseConfigurableUserGroupProvider.getUserAndGroups(identity);
+                                }
+
+                                @Override
+                                public void initialize(UserGroupProviderInitializationContext initializationContext) throws SecurityProviderCreationException {
+                                    baseConfigurableUserGroupProvider.initialize(initializationContext);
+                                }
+
+                                @Override
+                                public void onConfigured(AuthorizerConfigurationContext configurationContext) throws SecurityProviderCreationException {
+                                    baseConfigurableUserGroupProvider.onConfigured(configurationContext);
+                                }
+
+                                @Override
+                                public void preDestruction() throws SecurityProviderDestructionException {
+                                    baseConfigurableUserGroupProvider.preDestruction();
+                                }
+                            };
+                        } else {
+                            return baseUserGroupProvider;
+                        }
+                    }
+
+                    @Override
+                    public void initialize(AccessPolicyProviderInitializationContext initializationContext) throws SecurityProviderCreationException {
+                        baseConfigurableAccessPolicyProvider.initialize(initializationContext);
+                    }
+
+                    @Override
+                    public void onConfigured(AuthorizerConfigurationContext configurationContext) throws SecurityProviderCreationException {
+                        baseConfigurableAccessPolicyProvider.onConfigured(configurationContext);
+                    }
+
+                    @Override
+                    public void preDestruction() throws SecurityProviderDestructionException {
+                        baseConfigurableAccessPolicyProvider.preDestruction();
+                    }
+                };
+            } else {
+                return baseAccessPolicyProvider;
+            }
+        }
+
+        @Override
+        public AuthorizationResult authorize(AuthorizationRequest request) throws AuthorizationAccessException {
+            final AuthorizationResult result = baseManagedAuthorizer.authorize(request);
+
+            // audit the authorization request
+            audit(baseManagedAuthorizer, request, result);
+
+            return result;
+        }
+
+        @Override
+        public void initialize(AuthorizerInitializationContext initializationContext) throws SecurityProviderCreationException {
+            baseManagedAuthorizer.initialize(initializationContext);
+        }
+
+        @Override
+        public void onConfigured(AuthorizerConfigurationContext configurationContext) throws SecurityProviderCreationException {
+            baseManagedAuthorizer.onConfigured(configurationContext);
+
+            final AccessPolicyProvider accessPolicyProvider = baseManagedAuthorizer.getAccessPolicyProvider();
+            final UserGroupProvider userGroupProvider = accessPolicyProvider.getUserGroupProvider();
+
+            // ensure that only one policy per resource-action exists
+            for (AccessPolicy accessPolicy : accessPolicyProvider.getAccessPolicies()) {
+                if (policyExists(accessPolicyProvider, accessPolicy)) {
+                    throw new SecurityProviderCreationException(String.format("Found multiple policies for '%s' with '%s'.", accessPolicy.getResource(), accessPolicy.getAction()));
+                }
+            }
+
+            // ensure that only one group exists per identity
+            for (User user : userGroupProvider.getUsers()) {
+                if (tenantExists(userGroupProvider, user.getIdentifier(), user.getIdentity())) {
+                    throw new SecurityProviderCreationException(String.format("Found multiple users/user groups with identity '%s'.", user.getIdentity()));
+                }
+            }
+
+            // ensure that only one group exists per identity
+            for (Group group : userGroupProvider.getGroups()) {
+                if (tenantExists(userGroupProvider, group.getIdentifier(), group.getName())) {
+                    throw new SecurityProviderCreationException(String.format("Found multiple users/user groups with name '%s'.", group.getName()));
+                }
+            }
+        }
+
+        @Override
+        public void preDestruction() throws SecurityProviderDestructionException {
+            baseManagedAuthorizer.preDestruction();
+        }
+    }
+
+    private static class AuthorizerWrapper implements Authorizer, WrappedAuthorizer {
+        private final Authorizer baseAuthorizer;
+
+        public AuthorizerWrapper(Authorizer baseAuthorizer) {
+            this.baseAuthorizer = baseAuthorizer;
+        }
+
+        @Override
+        public Authorizer getBaseAuthorizer() {
+            return baseAuthorizer;
+        }
+
+        @Override
+        public AuthorizationResult authorize(AuthorizationRequest request) throws AuthorizationAccessException {
+            final AuthorizationResult result = baseAuthorizer.authorize(request);
+
+            // audit the authorization request
+            audit(baseAuthorizer, request, result);
+
+            return result;
+        }
+
+        @Override
+        public void initialize(AuthorizerInitializationContext initializationContext) throws SecurityProviderCreationException {
+            baseAuthorizer.initialize(initializationContext);
+        }
+
+        @Override
+        public void onConfigured(AuthorizerConfigurationContext configurationContext) throws SecurityProviderCreationException {
+            baseAuthorizer.onConfigured(configurationContext);
+        }
+
+        @Override
+        public void preDestruction() throws SecurityProviderDestructionException {
+            baseAuthorizer.preDestruction();
+        }
+    }
+
+    private static Authorizer installIntegrityChecks(final Authorizer baseAuthorizer) {
+        if (baseAuthorizer instanceof ManagedAuthorizer) {
+            return new ManagedAuthorizerWrapper((ManagedAuthorizer) baseAuthorizer);
+        } else {
+            return new AuthorizerWrapper(baseAuthorizer);
+        }
+    }
+
+    private static void audit(final Authorizer authorizer, final AuthorizationRequest request, final AuthorizationResult result) {
+        // audit when...
+        // 1 - the authorizer supports auditing
+        // 2 - the request is an access attempt
+        // 3 - the result is either approved/denied, when resource is not found a subsequent request may be following with the parent resource
+        if (authorizer instanceof AuthorizationAuditor && request.isAccessAttempt() && !AuthorizationResult.Result.ResourceNotFound.equals(result.getResult())) {
+            ((AuthorizationAuditor) authorizer).auditAccessAttempt(request, result);
+        }
+    }
+
+    /**
+     * Checks if another policy exists with the same resource and action as the given policy.
+     *
+     * @param checkAccessPolicy an access policy being checked
+     * @return true if another access policy exists with the same resource and action, false otherwise
+     */
+    private static boolean policyExists(final AccessPolicyProvider accessPolicyProvider, final AccessPolicy checkAccessPolicy) {
+        for (AccessPolicy accessPolicy : accessPolicyProvider.getAccessPolicies()) {
+            if (!accessPolicy.getIdentifier().equals(checkAccessPolicy.getIdentifier())
+                    && accessPolicy.getResource().equals(checkAccessPolicy.getResource())
+                    && accessPolicy.getAction().equals(checkAccessPolicy.getAction())) {
+                return true;
+            }
+        }
+        return false;
+    }
+
+    /**
+     * Checks if another user or group exists with the same identity.
+     *
+     * @param userGroupProvider the userGroupProvider to use to lookup the tenant
+     * @param identifier identity of the tenant
+     * @param identity identity of the tenant
+     * @return true if another user exists with the same identity, false otherwise
+     */
+    private static boolean tenantExists(final UserGroupProvider userGroupProvider, final String identifier, final String identity) {
+        for (User user : userGroupProvider.getUsers()) {
+            if (!user.getIdentifier().equals(identifier)
+                    && user.getIdentity().equals(identity)) {
+                return true;
+            }
+        }
+
+        for (Group group : userGroupProvider.getGroups()) {
+            if (!group.getIdentifier().equals(identifier)
+                    && group.getName().equals(identity)) {
+                return true;
+            }
+        }
+
+        return false;
+    }
+
+    /**
+     * Check that all users in the group exist.
+     *
+     * @param userGroupProvider the userGroupProvider to use to lookup the users
+     * @param group the group whose users will be checked for existence.
+     * @return true if another user exists with the same identity, false otherwise
+     */
+    private static boolean allGroupUsersExist(final UserGroupProvider userGroupProvider, final Group group) {
+        for (String userIdentifier : group.getUsers()) {
+            User user = userGroupProvider.getUser(userIdentifier);
+            if (user == null) {
+                return false;
+            }
+        }
+
+        return true;
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/6f26290d/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/security/authorization/AuthorizerFactoryException.java
----------------------------------------------------------------------
diff --git a/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/security/authorization/AuthorizerFactoryException.java b/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/security/authorization/AuthorizerFactoryException.java
new file mode 100644
index 0000000..a479555
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/security/authorization/AuthorizerFactoryException.java
@@ -0,0 +1,33 @@
+/*
+ * 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.security.authorization;
+
+public class AuthorizerFactoryException extends RuntimeException {
+
+    public AuthorizerFactoryException(String message) {
+        super(message);
+    }
+
+    public AuthorizerFactoryException(String message, Throwable cause) {
+        super(message, cause);
+    }
+
+    public AuthorizerFactoryException(Throwable cause) {
+        super(cause);
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/6f26290d/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/security/authorization/CompositeConfigurableUserGroupProvider.java
----------------------------------------------------------------------
diff --git a/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/security/authorization/CompositeConfigurableUserGroupProvider.java b/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/security/authorization/CompositeConfigurableUserGroupProvider.java
new file mode 100644
index 0000000..6581e89
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/security/authorization/CompositeConfigurableUserGroupProvider.java
@@ -0,0 +1,242 @@
+/*
+ * 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.security.authorization;
+
+import org.apache.commons.lang3.StringUtils;
+import org.apache.nifi.registry.security.authorization.exception.AuthorizationAccessException;
+import org.apache.nifi.registry.security.authorization.exception.UninheritableAuthorizationsException;
+import org.apache.nifi.registry.security.exception.SecurityProviderCreationException;
+import org.apache.nifi.registry.security.exception.SecurityProviderDestructionException;
+import org.apache.nifi.registry.util.PropertyValue;
+
+import java.util.HashSet;
+import java.util.Map;
+import java.util.Set;
+import java.util.regex.Matcher;
+
+public class CompositeConfigurableUserGroupProvider extends CompositeUserGroupProvider implements ConfigurableUserGroupProvider {
+
+    static final String PROP_CONFIGURABLE_USER_GROUP_PROVIDER = "Configurable User Group Provider";
+
+    private UserGroupProviderLookup userGroupProviderLookup;
+    private ConfigurableUserGroupProvider configurableUserGroupProvider;
+
+    public CompositeConfigurableUserGroupProvider() {
+        super(true);
+    }
+
+    @Override
+    public void initialize(UserGroupProviderInitializationContext initializationContext) throws SecurityProviderCreationException {
+        userGroupProviderLookup = initializationContext.getUserGroupProviderLookup();
+
+        // initialize the CompositeUserGroupProvider
+        super.initialize(initializationContext);
+    }
+
+    @Override
+    public void onConfigured(AuthorizerConfigurationContext configurationContext) throws SecurityProviderCreationException {
+        final PropertyValue configurableUserGroupProviderKey = configurationContext.getProperty(PROP_CONFIGURABLE_USER_GROUP_PROVIDER);
+        if (!configurableUserGroupProviderKey.isSet()) {
+            throw new SecurityProviderCreationException("The Configurable User Group Provider must be set.");
+        }
+
+        final UserGroupProvider userGroupProvider = userGroupProviderLookup.getUserGroupProvider(configurableUserGroupProviderKey.getValue());
+
+        if (userGroupProvider == null) {
+            throw new SecurityProviderCreationException(String.format("Unable to locate the Configurable User Group Provider: %s", configurableUserGroupProviderKey));
+        }
+
+        if (!(userGroupProvider instanceof ConfigurableUserGroupProvider)) {
+            throw new SecurityProviderCreationException(String.format("The Configurable User Group Provider is not configurable: %s", configurableUserGroupProviderKey));
+        }
+
+        // Ensure that the ConfigurableUserGroupProvider is not also listed as one of the providers for the CompositeUserGroupProvider
+        for (Map.Entry<String,String> entry : configurationContext.getProperties().entrySet()) {
+            Matcher matcher = USER_GROUP_PROVIDER_PATTERN.matcher(entry.getKey());
+            if (matcher.matches() && !StringUtils.isBlank(entry.getValue())) {
+                final String userGroupProviderKey = entry.getValue();
+
+                if (userGroupProviderKey.equals(configurableUserGroupProviderKey.getValue())) {
+                    throw new SecurityProviderCreationException(String.format("Duplicate provider in Composite Configurable User Group Provider configuration: %s", userGroupProviderKey));
+                }
+            }
+        }
+
+        configurableUserGroupProvider = (ConfigurableUserGroupProvider) userGroupProvider;
+
+        // configure the CompositeUserGroupProvider
+        super.onConfigured(configurationContext);
+    }
+
+    @Override
+    public String getFingerprint() throws AuthorizationAccessException {
+        return configurableUserGroupProvider.getFingerprint();
+    }
+
+    @Override
+    public void inheritFingerprint(String fingerprint) throws AuthorizationAccessException {
+        configurableUserGroupProvider.inheritFingerprint(fingerprint);
+    }
+
+    @Override
+    public void checkInheritability(String proposedFingerprint) throws AuthorizationAccessException, UninheritableAuthorizationsException {
+        configurableUserGroupProvider.checkInheritability(proposedFingerprint);
+    }
+
+    @Override
+    public User addUser(User user) throws AuthorizationAccessException {
+        return configurableUserGroupProvider.addUser(user);
+    }
+
+    @Override
+    public boolean isConfigurable(User user) {
+        return configurableUserGroupProvider.isConfigurable(user);
+    }
+
+    @Override
+    public User updateUser(User user) throws AuthorizationAccessException {
+        return configurableUserGroupProvider.updateUser(user);
+    }
+
+    @Override
+    public User deleteUser(User user) throws AuthorizationAccessException {
+        return configurableUserGroupProvider.deleteUser(user);
+    }
+
+    @Override
+    public User deleteUser(String userIdentifier) throws AuthorizationAccessException {
+        return configurableUserGroupProvider.deleteUser(userIdentifier);
+    }
+
+    @Override
+    public Group addGroup(Group group) throws AuthorizationAccessException {
+        return configurableUserGroupProvider.addGroup(group);
+    }
+
+    @Override
+    public boolean isConfigurable(Group group) {
+        return configurableUserGroupProvider.isConfigurable(group);
+    }
+
+    @Override
+    public Group updateGroup(Group group) throws AuthorizationAccessException {
+        return configurableUserGroupProvider.updateGroup(group);
+    }
+
+    @Override
+    public Group deleteGroup(Group group) throws AuthorizationAccessException {
+        return configurableUserGroupProvider.deleteGroup(group);
+    }
+
+    @Override
+    public Group deleteGroup(String groupIdentifier) throws AuthorizationAccessException {
+        return configurableUserGroupProvider.deleteGroup(groupIdentifier);
+    }
+
+    @Override
+    public Set<User> getUsers() throws AuthorizationAccessException {
+        final Set<User> users = new HashSet<>(configurableUserGroupProvider.getUsers());
+        users.addAll(super.getUsers());
+        return users;
+    }
+
+    @Override
+    public User getUser(String identifier) throws AuthorizationAccessException {
+        User user = configurableUserGroupProvider.getUser(identifier);
+
+        if (user == null) {
+            user = super.getUser(identifier);
+        }
+
+        return user;
+    }
+
+    @Override
+    public User getUserByIdentity(String identity) throws AuthorizationAccessException {
+        User user = configurableUserGroupProvider.getUserByIdentity(identity);
+
+        if (user == null) {
+            user = super.getUserByIdentity(identity);
+        }
+
+        return user;
+    }
+
+    @Override
+    public Set<Group> getGroups() throws AuthorizationAccessException {
+        final Set<Group> groups = new HashSet<>(configurableUserGroupProvider.getGroups());
+        groups.addAll(super.getGroups());
+        return groups;
+    }
+
+    @Override
+    public Group getGroup(String identifier) throws AuthorizationAccessException {
+        Group group = configurableUserGroupProvider.getGroup(identifier);
+
+        if (group == null) {
+            group = super.getGroup(identifier);
+        }
+
+        return group;
+    }
+
+    @Override
+    public UserAndGroups getUserAndGroups(String identity) throws AuthorizationAccessException {
+
+        final CompositeUserAndGroups combinedResult;
+
+        // First, lookup user and groups by identity and combine data from all providers
+        UserAndGroups configurableProviderResult = configurableUserGroupProvider.getUserAndGroups(identity);
+        UserAndGroups compositeProvidersResult = super.getUserAndGroups(identity);
+
+        if (configurableProviderResult.getUser() != null && compositeProvidersResult.getUser() != null) {
+            throw new IllegalStateException("Multiple UserGroupProviders claim to provide user " + identity);
+
+        } else if (configurableProviderResult.getUser() != null) {
+            combinedResult = new CompositeUserAndGroups(configurableProviderResult.getUser(), configurableProviderResult.getGroups());
+            combinedResult.addAllGroups(compositeProvidersResult.getGroups());
+
+        } else if (compositeProvidersResult.getUser() != null) {
+            combinedResult = new CompositeUserAndGroups(compositeProvidersResult.getUser(), compositeProvidersResult.getGroups());
+            combinedResult.addAllGroups(configurableProviderResult.getGroups());
+
+        } else {
+            return UserAndGroups.EMPTY;
+        }
+
+        // Second, lookup groups containing the user identifier
+        String userIdentifier = combinedResult.getUser().getIdentifier();
+        for (final Group group : configurableUserGroupProvider.getGroups()) {
+            if (group.getUsers() != null && group.getUsers().contains(userIdentifier)) {
+                combinedResult.addGroup(group);
+            }
+        }
+        for (final Group group : super.getGroups()) {
+            if (group.getUsers() != null && group.getUsers().contains(userIdentifier)) {
+                combinedResult.addGroup(group);
+            }
+        }
+
+        return combinedResult;
+    }
+
+    @Override
+    public void preDestruction() throws SecurityProviderDestructionException {
+        super.preDestruction();
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/6f26290d/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/security/authorization/CompositeUserAndGroups.java
----------------------------------------------------------------------
diff --git a/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/security/authorization/CompositeUserAndGroups.java b/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/security/authorization/CompositeUserAndGroups.java
new file mode 100644
index 0000000..5665122
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/security/authorization/CompositeUserAndGroups.java
@@ -0,0 +1,79 @@
+/*
+ * 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.security.authorization;
+
+import java.util.HashSet;
+import java.util.Set;
+
+public class CompositeUserAndGroups implements UserAndGroups {
+
+    private User user;
+    private Set<Group> groups;
+
+    public CompositeUserAndGroups() {
+        this.user = null;
+        this.groups = null;
+    }
+
+    public CompositeUserAndGroups(User user, Set<Group> groups) {
+        this.user = user;
+        setGroups(groups);
+    }
+
+    @Override
+    public User getUser() {
+        return user;
+    }
+
+    public void setUser(User user) {
+        this.user = user;
+    }
+
+    @Override
+    public Set<Group> getGroups() {
+        return groups;
+    }
+
+    public void setGroups(Set<Group> groups) {
+        // copy the collection so that if we add to this collection it does not modify other references
+        if (groups != null) {
+            this.groups = new HashSet<>(groups);
+        } else {
+            this.groups = null;
+        }
+    }
+
+    public void addAllGroups(Set<Group> groups) {
+        if (groups != null) {
+            if (this.groups == null) {
+                this.groups = new HashSet<>();
+            }
+            this.groups.addAll(groups);
+        }
+    }
+
+    public void addGroup(Group group) {
+        if (group != null) {
+            if (this.groups == null) {
+                this.groups = new HashSet<>();
+            }
+            this.groups.add(group);
+        }
+    }
+
+
+}

http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/6f26290d/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/security/authorization/CompositeUserGroupProvider.java
----------------------------------------------------------------------
diff --git a/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/security/authorization/CompositeUserGroupProvider.java b/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/security/authorization/CompositeUserGroupProvider.java
new file mode 100644
index 0000000..3d84c14
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/security/authorization/CompositeUserGroupProvider.java
@@ -0,0 +1,207 @@
+/*
+ * 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.security.authorization;
+
+import org.apache.commons.lang3.StringUtils;
+import org.apache.nifi.registry.security.authorization.exception.AuthorizationAccessException;
+import org.apache.nifi.registry.security.exception.SecurityProviderCreationException;
+import org.apache.nifi.registry.security.exception.SecurityProviderDestructionException;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.util.ArrayList;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+public class CompositeUserGroupProvider implements UserGroupProvider {
+
+    private static final Logger logger = LoggerFactory.getLogger(CompositeUserGroupProvider.class);
+
+    static final String PROP_USER_GROUP_PROVIDER_PREFIX = "User Group Provider ";
+    static final Pattern USER_GROUP_PROVIDER_PATTERN = Pattern.compile(PROP_USER_GROUP_PROVIDER_PREFIX + "\\S+");
+
+    private final boolean allowEmptyProviderList;
+
+    private UserGroupProviderLookup userGroupProviderLookup;
+    private List<UserGroupProvider> userGroupProviders = new ArrayList<>(); // order matters
+
+    public CompositeUserGroupProvider() {
+        this(false);
+    }
+
+    public CompositeUserGroupProvider(boolean allowEmptyProviderList) {
+        this.allowEmptyProviderList = allowEmptyProviderList;
+    }
+
+    @Override
+    public void initialize(UserGroupProviderInitializationContext initializationContext) throws SecurityProviderCreationException {
+        userGroupProviderLookup = initializationContext.getUserGroupProviderLookup();
+    }
+
+    @Override
+    public void onConfigured(AuthorizerConfigurationContext configurationContext) throws SecurityProviderCreationException {
+        for (Map.Entry<String,String> entry : configurationContext.getProperties().entrySet()) {
+            Matcher matcher = USER_GROUP_PROVIDER_PATTERN.matcher(entry.getKey());
+            if (matcher.matches() && !StringUtils.isBlank(entry.getValue())) {
+                final String userGroupProviderKey = entry.getValue();
+                final UserGroupProvider userGroupProvider = userGroupProviderLookup.getUserGroupProvider(userGroupProviderKey);
+
+                if (userGroupProvider == null) {
+                    throw new SecurityProviderCreationException(String.format("Unable to locate the configured User Group Provider: %s", userGroupProviderKey));
+                }
+
+                if (userGroupProviders.contains(userGroupProvider)) {
+                    throw new SecurityProviderCreationException(String.format("Duplicate provider in Composite User Group Provider configuration: %s", userGroupProviderKey));
+                }
+
+                userGroupProviders.add(userGroupProvider);
+            }
+        }
+
+        if (!allowEmptyProviderList && userGroupProviders.isEmpty()) {
+            throw new SecurityProviderCreationException("At least one User Group Provider must be configured.");
+        }
+    }
+
+    @Override
+    public Set<User> getUsers() throws AuthorizationAccessException {
+        final Set<User> users = new HashSet<>();
+
+        for (final UserGroupProvider userGroupProvider : userGroupProviders) {
+            users.addAll(userGroupProvider.getUsers());
+        }
+
+        return users;
+    }
+
+    @Override
+    public User getUser(String identifier) throws AuthorizationAccessException {
+        User user = null;
+
+        for (final UserGroupProvider userGroupProvider : userGroupProviders) {
+            user = userGroupProvider.getUser(identifier);
+
+            if (user != null) {
+                break;
+            }
+        }
+
+        return user;
+    }
+
+    @Override
+    public User getUserByIdentity(String identity) throws AuthorizationAccessException {
+        User user = null;
+
+        for (final UserGroupProvider userGroupProvider : userGroupProviders) {
+            user = userGroupProvider.getUserByIdentity(identity);
+
+            if (user != null) {
+                break;
+            }
+        }
+
+        return user;
+    }
+
+    @Override
+    public Set<Group> getGroups() throws AuthorizationAccessException {
+        final Set<Group> groups = new HashSet<>();
+
+        for (final UserGroupProvider userGroupProvider : userGroupProviders) {
+            groups.addAll(userGroupProvider.getGroups());
+        }
+
+        return groups;
+    }
+
+    @Override
+    public Group getGroup(String identifier) throws AuthorizationAccessException {
+        Group group = null;
+
+        for (final UserGroupProvider userGroupProvider : userGroupProviders) {
+            group = userGroupProvider.getGroup(identifier);
+
+            if (group != null) {
+                break;
+            }
+        }
+
+        return group;
+    }
+
+    @Override
+    public UserAndGroups getUserAndGroups(String identity) throws AuthorizationAccessException {
+
+        // This method builds a UserAndGroups response by combining the data from all providers using a two-pass approach
+
+        CompositeUserAndGroups compositeUserAndGroups = new CompositeUserAndGroups();
+
+        // First pass - call getUserAndGroups(identity) on all providers, aggregate the responses, check for multiple
+        // user identity matches, which should not happen as identities should by globally unique.
+        String providerClassForUser = "";
+        for (final UserGroupProvider userGroupProvider : userGroupProviders) {
+            UserAndGroups userAndGroups = userGroupProvider.getUserAndGroups(identity);
+
+            if (userAndGroups.getUser() != null) {
+                // is this the first match on the user?
+                if(compositeUserAndGroups.getUser() == null) {
+                    compositeUserAndGroups.setUser(userAndGroups.getUser());
+                    providerClassForUser = userGroupProvider.getClass().getName();
+                } else {
+                    logger.warn("Multiple UserGroupProviders are claiming to provide user '{}': [{} and {}] ",
+                            identity,
+                            userAndGroups.getUser(),
+                            providerClassForUser, userGroupProvider.getClass().getName());
+                    throw new IllegalStateException("Multiple UserGroupProviders are claiming to provide user " + identity);
+                }
+            }
+
+            if (userAndGroups.getGroups() != null) {
+                compositeUserAndGroups.addAllGroups(userAndGroups.getGroups());
+            }
+        }
+
+        if (compositeUserAndGroups.getUser() == null) {
+            logger.debug("No user found for identity {}", identity);
+            return UserAndGroups.EMPTY;
+        }
+
+        // Second pass - Now that we've matched a user, call getGroups() on all providers, and
+        // check all groups to see if they contain the user identifier corresponding to the identity.
+        // This is necessary because a provider might only know about a group<->userIdentifier mapping
+        // without knowing the user identifier.
+        String userIdentifier = compositeUserAndGroups.getUser().getIdentifier();
+        for (final UserGroupProvider userGroupProvider : userGroupProviders) {
+            for (final Group group : userGroupProvider.getGroups()) {
+                if (group.getUsers() != null && group.getUsers().contains(userIdentifier)) {
+                    compositeUserAndGroups.addGroup(group);
+                }
+            }
+        }
+
+        return compositeUserAndGroups;
+    }
+
+    @Override
+    public void preDestruction() throws SecurityProviderDestructionException {
+    }
+}

http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/6f26290d/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/security/authorization/StandardAuthorizableLookup.java
----------------------------------------------------------------------
diff --git a/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/security/authorization/StandardAuthorizableLookup.java b/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/security/authorization/StandardAuthorizableLookup.java
new file mode 100644
index 0000000..18c2a52
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/security/authorization/StandardAuthorizableLookup.java
@@ -0,0 +1,220 @@
+/*
+ * 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.security.authorization;
+
+import org.apache.commons.lang3.StringUtils;
+import org.apache.nifi.registry.exception.ResourceNotFoundException;
+import org.apache.nifi.registry.security.authorization.resource.Authorizable;
+import org.apache.nifi.registry.security.authorization.resource.InheritingAuthorizable;
+import org.apache.nifi.registry.security.authorization.resource.ResourceFactory;
+import org.apache.nifi.registry.security.authorization.resource.ResourceType;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.stereotype.Component;
+
+@Component
+public class StandardAuthorizableLookup implements AuthorizableLookup {
+
+    private static final Logger logger = LoggerFactory.getLogger(StandardAuthorizableLookup.class);
+
+    private static final Authorizable TENANTS_AUTHORIZABLE = new Authorizable() {
+        @Override
+        public Authorizable getParentAuthorizable() {
+            return null;
+        }
+
+        @Override
+        public Resource getResource() {
+            return ResourceFactory.getTenantsResource();
+        }
+    };
+
+    private static final Authorizable POLICIES_AUTHORIZABLE = new Authorizable() {
+        @Override
+        public Authorizable getParentAuthorizable() {
+            return null;
+        }
+
+        @Override
+        public Resource getResource() {
+            return ResourceFactory.getPoliciesResource();
+        }
+    };
+
+    private static final Authorizable BUCKETS_AUTHORIZABLE = new Authorizable() {
+        @Override
+        public Authorizable getParentAuthorizable() {
+            return null;
+        }
+
+        @Override
+        public Resource getResource() {
+            return ResourceFactory.getBucketsResource();
+        }
+    };
+
+    private static final Authorizable PROXY_AUTHORIZABLE = new Authorizable() {
+        @Override
+        public Authorizable getParentAuthorizable() {
+            return null;
+        }
+
+        @Override
+        public Resource getResource() {
+            return ResourceFactory.getProxyResource();
+        }
+    };
+
+    private static final Authorizable ACTUATOR_AUTHORIZABLE = new Authorizable() {
+        @Override
+        public Authorizable getParentAuthorizable() {
+            return null;
+        }
+
+        @Override
+        public Resource getResource() {
+            return ResourceFactory.getActuatorResource();
+        }
+    };
+
+    private static final Authorizable SWAGGER_AUTHORIZABLE = new Authorizable() {
+        @Override
+        public Authorizable getParentAuthorizable() {
+            return null;
+        }
+
+        @Override
+        public Resource getResource() {
+            return ResourceFactory.getSwaggerResource();
+        }
+    };
+
+    @Override
+    public Authorizable getActuatorAuthorizable() {
+        return ACTUATOR_AUTHORIZABLE;
+    }
+
+    @Override
+    public Authorizable getSwaggerAuthorizable() {
+        return SWAGGER_AUTHORIZABLE;
+    }
+
+    @Override
+    public Authorizable getProxyAuthorizable() {
+        return PROXY_AUTHORIZABLE;
+    }
+
+    @Override
+    public Authorizable getTenantsAuthorizable() {
+        return TENANTS_AUTHORIZABLE;
+    }
+
+    @Override
+    public Authorizable getPoliciesAuthorizable() {
+        return POLICIES_AUTHORIZABLE;
+    }
+
+    @Override
+    public Authorizable getBucketsAuthorizable() {
+        return BUCKETS_AUTHORIZABLE;
+    }
+
+    @Override
+    public Authorizable getBucketAuthorizable(String bucketIdentifier) {
+        // Note - this returns a special Authorizable type that inherits permissions from the parent Authorizable
+        return new InheritingAuthorizable() {
+
+            @Override
+            public Authorizable getParentAuthorizable() {
+                return getBucketsAuthorizable();
+            }
+
+            @Override
+            public Resource getResource() {
+                return ResourceFactory.getBucketResource(bucketIdentifier, "Bucket with ID " + bucketIdentifier);
+            }
+        };
+    }
+
+    @Override
+    public Authorizable getAuthorizableByResource(String resource) {
+        ResourceType resourceType = ResourceType.mapFullResourcePathToResourceType(resource);
+
+        if (resourceType == null) {
+            throw new ResourceNotFoundException("Unrecognized resource: " + resource);
+        }
+
+        return getAuthorizableByResource(resourceType, resource);
+    }
+
+    private Authorizable getAuthorizableByResource(final ResourceType resourceType, final String resource) {
+        Authorizable authorizable = null;
+        switch (resourceType) {
+
+            /* Access to these resources are always authorized by the top-level resource */
+            case Policy:
+                authorizable = getPoliciesAuthorizable();
+                break;
+            case Tenant:
+                authorizable = getTenantsAuthorizable();
+                break;
+            case Proxy:
+                authorizable = getProxyAuthorizable();
+                break;
+            case Actuator:
+                authorizable = getActuatorAuthorizable();
+                break;
+            case Swagger:
+                authorizable = getSwaggerAuthorizable();
+                break;
+
+            /* Access to buckets can be authorized by the top-level /buckets resource or an individual /buckets/{id} resource */
+            case Bucket:
+                final String childResourceId = StringUtils.substringAfter(resource, resourceType.getValue());
+                if (childResourceId.startsWith("/")) {
+                    authorizable = getAuthorizableByChildResource(resourceType, childResourceId);
+                } else {
+                    authorizable = getBucketsAuthorizable();
+                }
+        }
+
+        if (authorizable == null) {
+            logger.debug("Could not determine the Authorizable for resource type='{}', path='{}', ", resourceType.getValue(), resource);
+            throw new IllegalArgumentException("This an unexpected type of authorizable resource: " + resourceType.getValue());
+        }
+
+        return authorizable;
+    }
+
+    private Authorizable getAuthorizableByChildResource(final ResourceType baseResourceType, final String childResourceId) {
+        Authorizable authorizable;
+        switch (baseResourceType) {
+            case Bucket:
+                String[] childResourcePathParts = childResourceId.split("/");
+                if (childResourcePathParts.length >= 1) {
+                    final String bucketId = childResourcePathParts[1];
+                    authorizable = getBucketAuthorizable(bucketId);
+                    break;
+                }
+            default:
+                throw new IllegalArgumentException("Unexpected lookup for child resource authorizable for base resource type " + baseResourceType.getValue());
+        }
+
+        return authorizable;
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/6f26290d/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/security/authorization/StandardAuthorizerConfigurationContext.java
----------------------------------------------------------------------
diff --git a/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/security/authorization/StandardAuthorizerConfigurationContext.java b/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/security/authorization/StandardAuthorizerConfigurationContext.java
new file mode 100644
index 0000000..9d274f7
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/security/authorization/StandardAuthorizerConfigurationContext.java
@@ -0,0 +1,54 @@
+/*
+ * 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.security.authorization;
+
+import org.apache.nifi.registry.util.PropertyValue;
+import org.apache.nifi.registry.util.StandardPropertyValue;
+
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ *
+ */
+public class StandardAuthorizerConfigurationContext implements AuthorizerConfigurationContext {
+
+    private final String identifier;
+    private final Map<String, String> properties;
+
+    public StandardAuthorizerConfigurationContext(String identifier, Map<String, String> properties) {
+        this.identifier = identifier;
+        this.properties = Collections.unmodifiableMap(new HashMap<String, String>(properties));
+    }
+
+    @Override
+    public String getIdentifier() {
+        return identifier;
+    }
+
+    @Override
+    public Map<String, String> getProperties() {
+        return properties;
+    }
+
+    @Override
+    public PropertyValue getProperty(String property) {
+        return new StandardPropertyValue(properties.get(property));
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/6f26290d/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/security/authorization/StandardAuthorizerInitializationContext.java
----------------------------------------------------------------------
diff --git a/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/security/authorization/StandardAuthorizerInitializationContext.java b/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/security/authorization/StandardAuthorizerInitializationContext.java
new file mode 100644
index 0000000..d643e91
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/security/authorization/StandardAuthorizerInitializationContext.java
@@ -0,0 +1,52 @@
+/*
+ * 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.security.authorization;
+
+public class StandardAuthorizerInitializationContext implements AuthorizerInitializationContext {
+
+    private final String identifier;
+    private final UserGroupProviderLookup userGroupProviderLookup;
+    private final AccessPolicyProviderLookup accessPolicyProviderLookup;
+    private final AuthorizerLookup authorizerLookup;
+
+    public StandardAuthorizerInitializationContext(String identifier, UserGroupProviderLookup userGroupProviderLookup,
+                                                   AccessPolicyProviderLookup accessPolicyProviderLookup, AuthorizerLookup authorizerLookup) {
+        this.identifier = identifier;
+        this.userGroupProviderLookup = userGroupProviderLookup;
+        this.accessPolicyProviderLookup = accessPolicyProviderLookup;
+        this.authorizerLookup = authorizerLookup;
+    }
+
+    @Override
+    public String getIdentifier() {
+        return identifier;
+    }
+
+    public AuthorizerLookup getAuthorizerLookup() {
+        return authorizerLookup;
+    }
+
+    @Override
+    public AccessPolicyProviderLookup getAccessPolicyProviderLookup() {
+        return accessPolicyProviderLookup;
+    }
+
+    @Override
+    public UserGroupProviderLookup getUserGroupProviderLookup() {
+        return userGroupProviderLookup;
+    }
+}