NIFIREG-212 Separating proxy into Read, Write, and Delete so some proxies can be set to read-only

This closes #194.

Signed-off-by: Kevin Doran <kdoran@apache.org>
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
index 2193734..75c6f15 100644
--- 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
@@ -686,10 +686,18 @@
 | Allows users to delete policies
 | `resource="/policies" action="D"`
 
-| Can Proxy Requests
-| Allows users to proxy requests
+| Can Proxy Requests (Read)
+| Allows users to proxy read requests (GET)
+| `resource="/proxy" action="R"`
+
+| Can Proxy Requests (Write)
+| Allows users to proxy write requests (POST, PUT, PATCH)
 | `resource="/proxy" action="W"`
 
+| Can Proxy Requests (Delete)
+| Allows users to proxy delete requests (DELETE)
+| `resource="/proxy" action="D"`
+
 | View Swagger
 | Allows users to access the self-hosted Swagger UI
 | `resource="/swagger" action="R"`
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
index c3434c4..5eb1874 100644
--- 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
@@ -131,14 +131,18 @@
             new ResourceActionPair("/swagger", READ_CODE),
             new ResourceActionPair("/swagger", WRITE_CODE),
             new ResourceActionPair("/swagger", DELETE_CODE),
-            new ResourceActionPair("/proxy", WRITE_CODE)
+            new ResourceActionPair("/proxy", READ_CODE),
+            new ResourceActionPair("/proxy", WRITE_CODE),
+            new ResourceActionPair("/proxy", DELETE_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)
+            new ResourceActionPair("/proxy", READ_CODE),
+            new ResourceActionPair("/proxy", WRITE_CODE),
+            new ResourceActionPair("/proxy", DELETE_CODE)
     };
 
     static final String PROP_NIFI_IDENTITY_PREFIX = "NiFi Identity ";
diff --git a/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/security/authentication/x509/X509AuthenticationRequestDetails.java b/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/security/authentication/x509/X509AuthenticationRequestDetails.java
new file mode 100644
index 0000000..aa24cd6
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/security/authentication/x509/X509AuthenticationRequestDetails.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.authentication.x509;
+
+import java.util.Objects;
+
+public class X509AuthenticationRequestDetails {
+
+    private final String proxiedEntitiesChain;
+
+    private final String httpMethod;
+
+    public X509AuthenticationRequestDetails(final String proxiedEntitiesChain, final String httpMethod) {
+        this.proxiedEntitiesChain = proxiedEntitiesChain;
+        this.httpMethod = Objects.requireNonNull(httpMethod);
+    }
+
+    public String getProxiedEntitiesChain() {
+        return proxiedEntitiesChain;
+    }
+
+    public String getHttpMethod() {
+        return httpMethod;
+    }
+
+}
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
index aefdd5b..9d724ac 100644
--- 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
@@ -35,6 +35,9 @@
 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 org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.http.HttpMethod;
 
 import java.util.List;
 import java.util.ListIterator;
@@ -42,6 +45,8 @@
 
 public class X509IdentityAuthenticationProvider extends IdentityAuthenticationProvider {
 
+    private static final Logger LOGGER = LoggerFactory.getLogger(X509IdentityAuthenticationProvider.class);
+
     private static final Authorizable PROXY_AUTHORIZABLE = new Authorizable() {
         @Override
         public Authorizable getParentAuthorizable() {
@@ -63,12 +68,16 @@
             AuthenticationRequestToken requestToken,
             AuthenticationResponse response) {
 
-        AuthenticationRequest authenticationRequest = requestToken.getAuthenticationRequest();
+        final AuthenticationRequest authenticationRequest = requestToken.getAuthenticationRequest();
 
-        String proxiedEntitiesChain = authenticationRequest.getDetails() != null
-                ? (String)authenticationRequest.getDetails()
-                : null;
+        final Object requestDetails = authenticationRequest.getDetails();
+        if (requestDetails == null || !(requestDetails instanceof X509AuthenticationRequestDetails)) {
+            throw new IllegalStateException("Invalid request details specified");
+        }
 
+        final X509AuthenticationRequestDetails x509RequestDetails = (X509AuthenticationRequestDetails) authenticationRequest.getDetails();
+
+        final String proxiedEntitiesChain = x509RequestDetails.getProxiedEntitiesChain();
         if (StringUtils.isBlank(proxiedEntitiesChain)) {
             return super.buildAuthenticatedToken(requestToken, response);
         }
@@ -77,6 +86,10 @@
         final List<String> proxyChain = ProxiedEntitiesUtils.tokenizeProxiedEntitiesChain(proxiedEntitiesChain);
         proxyChain.add(response.getIdentity());
 
+        final String httpMethodStr = x509RequestDetails.getHttpMethod().toUpperCase();
+        final HttpMethod httpMethod = HttpMethod.resolve(httpMethodStr);
+        LOGGER.debug("HTTP method is {}", new Object[]{httpMethod});
+
         // add the chain as appropriate to each proxy
         NiFiUser proxy = null;
         for (final ListIterator<String> chainIter = proxyChain.listIterator(proxyChain.size()); chainIter.hasPrevious(); ) {
@@ -97,16 +110,47 @@
             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));
+                switch (httpMethod) {
+                    case POST:
+                    case PUT:
+                    case PATCH:
+                        authorizeWrite(proxy);
+                        break;
+                    case DELETE:
+                        authorizeDelete(proxy);
+                        break;
+                    default:
+                        authorizeRead(proxy);
+                        break;
                 }
             }
         }
 
         return new AuthenticationSuccessToken(new NiFiUserDetails(proxy));
+    }
 
+    private void authorizeRead(final NiFiUser proxy) {
+        try {
+            PROXY_AUTHORIZABLE.authorize(authorizer, RequestAction.READ, proxy);
+        } catch (final AccessDeniedException e) {
+            throw new UntrustedProxyException(String.format("Untrusted proxy for read operation [%s].", proxy.getIdentity()));
+        }
+    }
+
+    private void authorizeWrite(final NiFiUser proxy) {
+        try {
+            PROXY_AUTHORIZABLE.authorize(authorizer, RequestAction.WRITE, proxy);
+        } catch (final AccessDeniedException e) {
+            throw new UntrustedProxyException(String.format("Untrusted proxy for write operation [%s].", proxy.getIdentity()));
+        }
+    }
+
+    private void authorizeDelete(final NiFiUser proxy) {
+        try {
+            PROXY_AUTHORIZABLE.authorize(authorizer, RequestAction.DELETE, proxy);
+        } catch (final AccessDeniedException e) {
+            throw new UntrustedProxyException(String.format("Untrusted proxy for delete operation [%s].", proxy.getIdentity()));
+        }
     }
 
     /**
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
index 2a1856e..fc74f66 100644
--- 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
@@ -113,9 +113,10 @@
         final String principal = certificatePrincipal.toString();
 
         // extract the proxiedEntitiesChain header value from the servletRequest
-        String proxiedEntitiesChainHeader = servletRequest.getHeader(ProxiedEntitiesUtils.PROXY_ENTITIES_CHAIN);
+        final String proxiedEntitiesChainHeader = servletRequest.getHeader(ProxiedEntitiesUtils.PROXY_ENTITIES_CHAIN);
+        final X509AuthenticationRequestDetails details = new X509AuthenticationRequestDetails(proxiedEntitiesChainHeader, servletRequest.getMethod());
 
-        return new AuthenticationRequest(principal, certificates[0], proxiedEntitiesChainHeader);
+        return new AuthenticationRequest(principal, certificates[0], details);
 
     }
 
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
index 67cb2e2..095d258 100644
--- 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
@@ -65,7 +65,7 @@
                 "\"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}}" +
+                "\"proxy\":{\"canRead\":true,\"canWrite\":true,\"canDelete\":true}}" +
                 "}";
 
         // When: the /access endpoint is queried
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
index 8d8ea97..de87fcf 100644
--- 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
@@ -195,7 +195,7 @@
                 "\"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}}" +
+                "\"proxy\":{\"canRead\":true,\"canWrite\":true,\"canDelete\":true}}" +
                 "}";
 
         // When: the /access endpoint is queried using a JWT for the kerberos user
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
index 543ea87..11d7b33 100644
--- 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
@@ -256,7 +256,7 @@
                 "\"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}}" +
+                "\"proxy\":{\"canRead\":true,\"canWrite\":true,\"canDelete\":true}}" +
                 "}";
 
         // When: the /access endpoint is queried using a JWT for the nifiadmin LDAP user
@@ -284,7 +284,7 @@
                     "\"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}}}," +
+                    "\"proxy\":{\"canRead\":true,\"canWrite\":true,\"canDelete\":true}}}," +
                 "{\"identity\":\"euler\",\"userGroups\":[{\"identity\":\"mathematicians\"}],\"accessPolicies\":[],\"configurable\":false}," +
                 "{\"identity\":\"euclid\",\"userGroups\":[{\"identity\":\"mathematicians\"}],\"accessPolicies\":[],\"configurable\":false}," +
                 "{\"identity\":\"boyle\",\"userGroups\":[{\"identity\":\"chemists\"}],\"accessPolicies\":[],\"configurable\":false}," +
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
index cb14b90..62aa098 100644
--- 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
@@ -97,7 +97,7 @@
         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());
+        Assert.assertEquals(new Permissions().withCanWrite(true).withCanRead(true).withCanDelete(true), currentUser.getResourcePermissions().getProxy());
     }
 
     @Test
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
index 8317ee2..31eade4 100644
--- 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
@@ -128,12 +128,32 @@
                 </mat-checkbox>
             </div>
             <mat-checkbox [disabled]="!canEditSpecialPrivileges()"
-                          [checked]="nfRegistryService.group.resourcePermissions.proxy.canWrite"
+                          [checked]="nfRegistryService.group.resourcePermissions.proxy.canRead && nfRegistryService.group.resourcePermissions.proxy.canWrite && nfRegistryService.group.resourcePermissions.proxy.canDelete"
                           (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 flex fxLayout="row" fxLayoutAlign="space-around center">
+                <mat-checkbox class="pad-left-md"
+                              [disabled]="!canEditSpecialPrivileges()"
+                              [(checked)]="nfRegistryService.group.resourcePermissions.proxy.canRead"
+                              (change)="toggleGroupManageProxyPrivileges($event, 'read')">
+                    <span class="description">Read</span>
+                </mat-checkbox>
+                <mat-checkbox class="pad-left-md"
+                              [disabled]="!canEditSpecialPrivileges()"
+                              [(checked)]="nfRegistryService.group.resourcePermissions.proxy.canWrite"
+                              (change)="toggleGroupManageProxyPrivileges($event, 'write')">
+                    <span class="description">Write</span>
+                </mat-checkbox>
+                <mat-checkbox class="pad-left-md"
+                              [disabled]="!canEditSpecialPrivileges()"
+                              [(checked)]="nfRegistryService.group.resourcePermissions.proxy.canDelete"
+                              (change)="toggleGroupManageProxyPrivileges($event, 'delete')">
+                    <span class="description">Delete</span>
+                </mat-checkbox>
+            </div>
         </div>
         <mat-button-toggle-group name="nifi-registry-manage-group-perspective" class="pad-left-md tab-toggle-group">
             <mat-button-toggle [checked]="manageGroupPerspective === 'membership'"
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
index 409dbd0..2b0a505 100644
--- 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
@@ -134,12 +134,32 @@
             </div>
             <mat-checkbox
                     [disabled]="!canEditSpecialPrivileges()"
-                    [checked]="nfRegistryService.user.resourcePermissions.proxy.canWrite"
+                    [checked]="nfRegistryService.user.resourcePermissions.proxy.canRead && nfRegistryService.user.resourcePermissions.proxy.canWrite && nfRegistryService.user.resourcePermissions.proxy.canDelete"
                     (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 flex fxLayout="row" fxLayoutAlign="space-around center">
+                <mat-checkbox class="pad-left-md"
+                              [disabled]="!canEditSpecialPrivileges()"
+                              [(checked)]="nfRegistryService.user.resourcePermissions.proxy.canRead"
+                              (change)="toggleUserManageProxyPrivileges($event, 'read')">
+                    <span class="description">Read</span>
+                </mat-checkbox>
+                <mat-checkbox class="pad-left-md"
+                              [disabled]="!canEditSpecialPrivileges()"
+                              [(checked)]="nfRegistryService.user.resourcePermissions.proxy.canWrite"
+                              (change)="toggleUserManageProxyPrivileges($event, 'write')">
+                    <span class="description">Write</span>
+                </mat-checkbox>
+                <mat-checkbox class="pad-left-md"
+                              [disabled]="!canEditSpecialPrivileges()"
+                              [(checked)]="nfRegistryService.user.resourcePermissions.proxy.canDelete"
+                              (change)="toggleUserManageProxyPrivileges($event, 'delete')">
+                    <span class="description">Delete</span>
+                </mat-checkbox>
+            </div>
         </div>
         <mat-button-toggle-group name="nifi-registry-manage-user-perspective" class="pad-left-md tab-toggle-group">
             <mat-button-toggle [checked]="manageUserPerspective === 'membership'"
diff --git a/nifi-registry-core/nifi-registry-web-ui/src/main/webapp/services/nf-registry.service.js b/nifi-registry-core/nifi-registry-web-ui/src/main/webapp/services/nf-registry.service.js
index 5395acd..060c8dd 100644
--- a/nifi-registry-core/nifi-registry-web-ui/src/main/webapp/services/nf-registry.service.js
+++ b/nifi-registry-core/nifi-registry-web-ui/src/main/webapp/services/nf-registry.service.js
@@ -194,7 +194,7 @@
 
     // model for proxy privileges
     this.PROXY_PRIVS = {
-        '/proxy': ['write']
+        '/proxy': ['read', 'write', 'delete']
     };
 
     //<editor-fold desc="application state objects">