This is an automated email from the ASF dual-hosted git repository.

exceptionfactory pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/nifi.git


The following commit(s) were added to refs/heads/main by this push:
     new cfcae70d37 NIFI-13231 Added App Private Key Auth to GitHub 
FlowRegistryClient
cfcae70d37 is described below

commit cfcae70d3781a2d425251b21e88dc61ffbd12d98
Author: maybevanshh <[email protected]>
AuthorDate: Wed May 29 15:57:28 2024 +0530

    NIFI-13231 Added App Private Key Auth to GitHub FlowRegistryClient
    
    This closes #8890
    
    Signed-off-by: David Handermann <[email protected]>
    Co-authored-by: David Handermann <[email protected]>
---
 .../nifi-github-extensions/pom.xml                 |  22 ++++-
 .../nifi/github/GitHubAuthenticationType.java      |   4 +-
 .../nifi/github/GitHubFlowRegistryClient.java      |  24 ++++-
 .../apache/nifi/github/GitHubRepositoryClient.java |  50 ++++++++++
 ...thenticationType.java => PrivateKeyReader.java} |  21 ++--
 .../nifi/github/StandardPrivateKeyReader.java      | 110 +++++++++++++++++++++
 6 files changed, 216 insertions(+), 15 deletions(-)

diff --git 
a/nifi-extension-bundles/nifi-github-bundle/nifi-github-extensions/pom.xml 
b/nifi-extension-bundles/nifi-github-bundle/nifi-github-extensions/pom.xml
index be73f95343..d4e8fa5f3e 100644
--- a/nifi-extension-bundles/nifi-github-bundle/nifi-github-extensions/pom.xml
+++ b/nifi-extension-bundles/nifi-github-bundle/nifi-github-extensions/pom.xml
@@ -21,11 +21,10 @@
     </parent>
     <artifactId>nifi-github-extensions</artifactId>
     <packaging>jar</packaging>
+    <properties>
+        <jjwt.version>0.12.5</jjwt.version>
+    </properties>
     <dependencies>
-        <dependency>
-            <groupId>org.apache.nifi</groupId>
-            <artifactId>nifi-api</artifactId>
-        </dependency>
         <dependency>
             <groupId>org.apache.nifi</groupId>
             <artifactId>nifi-utils</artifactId>
@@ -39,5 +38,20 @@
             <artifactId>github-api</artifactId>
             <version>${github-api.version}</version>
         </dependency>
+        <dependency>
+            <groupId>io.jsonwebtoken</groupId>
+            <artifactId>jjwt-api</artifactId>
+            <version>${jjwt.version}</version>
+        </dependency>
+        <dependency>
+            <groupId>io.jsonwebtoken</groupId>
+            <artifactId>jjwt-impl</artifactId>
+            <version>${jjwt.version}</version>
+        </dependency>
+        <dependency>
+            <groupId>io.jsonwebtoken</groupId>
+            <artifactId>jjwt-jackson</artifactId>
+            <version>${jjwt.version}</version>
+        </dependency>
     </dependencies>
 </project>
diff --git 
a/nifi-extension-bundles/nifi-github-bundle/nifi-github-extensions/src/main/java/org/apache/nifi/github/GitHubAuthenticationType.java
 
b/nifi-extension-bundles/nifi-github-bundle/nifi-github-extensions/src/main/java/org/apache/nifi/github/GitHubAuthenticationType.java
index 964c376e8d..8cb7c4be7c 100644
--- 
a/nifi-extension-bundles/nifi-github-bundle/nifi-github-extensions/src/main/java/org/apache/nifi/github/GitHubAuthenticationType.java
+++ 
b/nifi-extension-bundles/nifi-github-bundle/nifi-github-extensions/src/main/java/org/apache/nifi/github/GitHubAuthenticationType.java
@@ -24,6 +24,6 @@ public enum GitHubAuthenticationType {
 
     NONE,
     PERSONAL_ACCESS_TOKEN,
-    APP_INSTALLATION_TOKEN;
-
+    APP_INSTALLATION_TOKEN,
+    APP_INSTALLATION
 }
diff --git 
a/nifi-extension-bundles/nifi-github-bundle/nifi-github-extensions/src/main/java/org/apache/nifi/github/GitHubFlowRegistryClient.java
 
b/nifi-extension-bundles/nifi-github-bundle/nifi-github-extensions/src/main/java/org/apache/nifi/github/GitHubFlowRegistryClient.java
index 34fce1d4cb..e17fc33050 100644
--- 
a/nifi-extension-bundles/nifi-github-bundle/nifi-github-extensions/src/main/java/org/apache/nifi/github/GitHubFlowRegistryClient.java
+++ 
b/nifi-extension-bundles/nifi-github-bundle/nifi-github-extensions/src/main/java/org/apache/nifi/github/GitHubFlowRegistryClient.java
@@ -129,6 +129,24 @@ public class GitHubFlowRegistryClient extends 
AbstractFlowRegistryClient {
             .dependsOn(AUTHENTICATION_TYPE, 
GitHubAuthenticationType.APP_INSTALLATION_TOKEN.name())
             .build();
 
+    static final PropertyDescriptor APP_ID = new PropertyDescriptor.Builder()
+            .name("App ID")
+            .description("Identifier of GitHub App to use for authentication")
+            .addValidator(StandardValidators.NON_BLANK_VALIDATOR)
+            .required(true)
+            .sensitive(false)
+            .dependsOn(AUTHENTICATION_TYPE, 
GitHubAuthenticationType.APP_INSTALLATION.name())
+            .build();
+
+    static final PropertyDescriptor APP_PRIVATE_KEY = new 
PropertyDescriptor.Builder()
+            .name("App Private Key")
+            .description("RSA private key associated with GitHub App to use 
for authentication.")
+            .addValidator(StandardValidators.NON_BLANK_VALIDATOR)
+            .required(true)
+            .sensitive(true)
+            .dependsOn(AUTHENTICATION_TYPE, 
GitHubAuthenticationType.APP_INSTALLATION.name())
+            .build();
+
     static final List<PropertyDescriptor> PROPERTY_DESCRIPTORS = List.of(
             GITHUB_API_URL,
             REPOSITORY_OWNER,
@@ -137,7 +155,9 @@ public class GitHubFlowRegistryClient extends 
AbstractFlowRegistryClient {
             REPOSITORY_PATH,
             AUTHENTICATION_TYPE,
             PERSONAL_ACCESS_TOKEN,
-            APP_INSTALLATION_TOKEN
+            APP_INSTALLATION_TOKEN,
+            APP_ID,
+            APP_PRIVATE_KEY
     );
 
     static final String DEFAULT_BUCKET_NAME = "default";
@@ -641,6 +661,8 @@ public class GitHubFlowRegistryClient extends 
AbstractFlowRegistryClient {
                 
.authenticationType(GitHubAuthenticationType.valueOf(context.getProperty(AUTHENTICATION_TYPE).getValue()))
                 
.personalAccessToken(context.getProperty(PERSONAL_ACCESS_TOKEN).getValue())
                 
.appInstallationToken(context.getProperty(APP_INSTALLATION_TOKEN).getValue())
+                .appId(context.getProperty(APP_ID).getValue())
+                .appPrivateKey(context.getProperty(APP_PRIVATE_KEY).getValue())
                 .repoOwner(context.getProperty(REPOSITORY_OWNER).getValue())
                 .repoName(context.getProperty(REPOSITORY_NAME).getValue())
                 .repoPath(context.getProperty(REPOSITORY_PATH).getValue())
diff --git 
a/nifi-extension-bundles/nifi-github-bundle/nifi-github-extensions/src/main/java/org/apache/nifi/github/GitHubRepositoryClient.java
 
b/nifi-extension-bundles/nifi-github-bundle/nifi-github-extensions/src/main/java/org/apache/nifi/github/GitHubRepositoryClient.java
index fc85db0fc8..32ec2cff8a 100644
--- 
a/nifi-extension-bundles/nifi-github-bundle/nifi-github-extensions/src/main/java/org/apache/nifi/github/GitHubRepositoryClient.java
+++ 
b/nifi-extension-bundles/nifi-github-bundle/nifi-github-extensions/src/main/java/org/apache/nifi/github/GitHubRepositoryClient.java
@@ -29,13 +29,19 @@ import org.kohsuke.github.GHRef;
 import org.kohsuke.github.GHRepository;
 import org.kohsuke.github.GitHub;
 import org.kohsuke.github.GitHubBuilder;
+import org.kohsuke.github.authorization.AppInstallationAuthorizationProvider;
+import org.kohsuke.github.authorization.AuthorizationProvider;
+import org.kohsuke.github.extras.authorization.JWTTokenProvider;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
 import java.io.FileNotFoundException;
 import java.io.IOException;
 import java.io.InputStream;
+import java.security.PrivateKey;
+import java.util.LinkedHashMap;
 import java.util.List;
+import java.util.Map;
 import java.util.Objects;
 import java.util.Optional;
 import java.util.Set;
@@ -49,6 +55,10 @@ public class GitHubRepositoryClient {
 
     private static final Logger LOGGER = 
LoggerFactory.getLogger(GitHubRepositoryClient.class);
 
+    private static final String REPOSITORY_CONTENTS_PERMISSION = "contents";
+
+    private static final String WRITE_ACCESS = "write";
+
     private static final String BRANCH_REF_PATTERN = "refs/heads/%s";
     private static final int COMMIT_PAGE_SIZE = 50;
 
@@ -71,9 +81,13 @@ public class GitHubRepositoryClient {
         repoName = Objects.requireNonNull(builder.repoName, "Repository Name 
is required");
         authenticationType = 
Objects.requireNonNull(builder.authenticationType, "Authentication Type is 
required");
 
+        // Map of permission to access for tracking App Installation 
permissions from internal authorization
+        final Map<String, String> appPermissions = new LinkedHashMap<>();
+
         switch (authenticationType) {
             case PERSONAL_ACCESS_TOKEN -> 
gitHubBuilder.withOAuthToken(builder.personalAccessToken);
             case APP_INSTALLATION_TOKEN -> 
gitHubBuilder.withAppInstallationToken(builder.appInstallationToken);
+            case APP_INSTALLATION -> 
gitHubBuilder.withAuthorizationProvider(getAppInstallationAuthorizationProvider(builder,
 appPermissions));
         }
 
         gitHub = gitHubBuilder.build();
@@ -90,6 +104,11 @@ public class GitHubRepositoryClient {
         if (gitHub.isAnonymous()) {
             canRead = true;
             canWrite = false;
+        } else if (GitHubAuthenticationType.APP_INSTALLATION == 
authenticationType) {
+            // The contents permission can be read or write when defined for 
an App Installation
+            canRead = 
appPermissions.containsKey(REPOSITORY_CONTENTS_PERMISSION);
+            final String repositoryContentsPermissions = 
appPermissions.get(REPOSITORY_CONTENTS_PERMISSION);
+            canWrite = WRITE_ACCESS.equals(repositoryContentsPermissions);
         } else {
             final GHMyself currentUser = gitHub.getMyself();
             canRead = repository.hasPermission(currentUser, 
GHPermissionType.READ);
@@ -397,6 +416,26 @@ public class GitHubRepositoryClient {
         }
     }
 
+    private AuthorizationProvider 
getAppInstallationAuthorizationProvider(final Builder builder, final 
Map<String, String> appPermissions) throws FlowRegistryException {
+        final AuthorizationProvider appAuthorizationProvider = 
getAppAuthorizationProvider(builder.appId, builder.appPrivateKey);
+        return new AppInstallationAuthorizationProvider(gitHubApp -> {
+            // Get Permissions for initial authentication as GitHub App before 
returning App Installation
+            appPermissions.putAll(gitHubApp.getPermissions());
+            // Get App Installation for named Repository
+            return gitHubApp.getInstallationByRepository(builder.repoOwner, 
builder.repoName);
+        }, appAuthorizationProvider);
+    }
+
+    private AuthorizationProvider getAppAuthorizationProvider(final String 
appId, final String appPrivateKey) throws FlowRegistryException {
+        try {
+            final PrivateKeyReader privateKeyReader = new 
StandardPrivateKeyReader();
+            final PrivateKey privateKey = 
privateKeyReader.readPrivateKey(appPrivateKey);
+            return new JWTTokenProvider(appId, privateKey);
+        } catch (final Exception e) {
+            throw new FlowRegistryException("Failed to build Authorization 
Provider from App ID and App Private Key", e);
+        }
+    }
+
     /**
      * Functional interface for making a request to GitHub which may throw 
IOException.
      *
@@ -427,6 +466,8 @@ public class GitHubRepositoryClient {
         private String repoOwner;
         private String repoName;
         private String repoPath;
+        private String appPrivateKey;
+        private String appId;
 
         public Builder apiUrl(final String apiUrl) {
             this.apiUrl = apiUrl;
@@ -462,6 +503,15 @@ public class GitHubRepositoryClient {
             this.repoPath = repoPath;
             return this;
         }
+        public Builder appId(final String appId) {
+            this.appId = appId;
+            return this;
+        }
+
+        public Builder appPrivateKey(final String appPrivateKey) {
+            this.appPrivateKey = appPrivateKey;
+            return this;
+        }
 
         public GitHubRepositoryClient build() throws IOException, 
FlowRegistryException {
             return new GitHubRepositoryClient(this);
diff --git 
a/nifi-extension-bundles/nifi-github-bundle/nifi-github-extensions/src/main/java/org/apache/nifi/github/GitHubAuthenticationType.java
 
b/nifi-extension-bundles/nifi-github-bundle/nifi-github-extensions/src/main/java/org/apache/nifi/github/PrivateKeyReader.java
similarity index 63%
copy from 
nifi-extension-bundles/nifi-github-bundle/nifi-github-extensions/src/main/java/org/apache/nifi/github/GitHubAuthenticationType.java
copy to 
nifi-extension-bundles/nifi-github-bundle/nifi-github-extensions/src/main/java/org/apache/nifi/github/PrivateKeyReader.java
index 964c376e8d..4b604dafb6 100644
--- 
a/nifi-extension-bundles/nifi-github-bundle/nifi-github-extensions/src/main/java/org/apache/nifi/github/GitHubAuthenticationType.java
+++ 
b/nifi-extension-bundles/nifi-github-bundle/nifi-github-extensions/src/main/java/org/apache/nifi/github/PrivateKeyReader.java
@@ -14,16 +14,21 @@
  *  See the License for the specific language governing permissions and
  *  limitations under the License.
  */
-
 package org.apache.nifi.github;
 
+import java.security.GeneralSecurityException;
+import java.security.PrivateKey;
+
 /**
- * Enumeration of authentication types for the GitHub client.
+ * Abstraction for reading Application Private Keys from encoded string
  */
-public enum GitHubAuthenticationType {
-
-    NONE,
-    PERSONAL_ACCESS_TOKEN,
-    APP_INSTALLATION_TOKEN;
-
+interface PrivateKeyReader {
+    /**
+     * Read Private Key from PEM-encoded string
+     *
+     * @param inputPrivateKey PEM-encoded string
+     * @return Private Key
+     * @throws GeneralSecurityException Thrown on failure to read Private Key
+     */
+    PrivateKey readPrivateKey(String inputPrivateKey) throws 
GeneralSecurityException;
 }
diff --git 
a/nifi-extension-bundles/nifi-github-bundle/nifi-github-extensions/src/main/java/org/apache/nifi/github/StandardPrivateKeyReader.java
 
b/nifi-extension-bundles/nifi-github-bundle/nifi-github-extensions/src/main/java/org/apache/nifi/github/StandardPrivateKeyReader.java
new file mode 100644
index 0000000000..8fab765a54
--- /dev/null
+++ 
b/nifi-extension-bundles/nifi-github-bundle/nifi-github-extensions/src/main/java/org/apache/nifi/github/StandardPrivateKeyReader.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.github;
+
+import java.io.BufferedReader;
+import java.io.IOException;
+import java.io.StringReader;
+import java.security.GeneralSecurityException;
+import java.security.InvalidKeyException;
+import java.security.Key;
+import java.security.KeyFactory;
+import java.security.PrivateKey;
+import java.security.interfaces.RSAPrivateKey;
+import java.util.Base64;
+import java.util.Objects;
+
+/**
+ * Standard implementation of Private Key Reader supporting RSA PKCS #1 
encoding
+ */
+class StandardPrivateKeyReader implements PrivateKeyReader {
+
+    private static final Base64.Decoder DECODER = Base64.getDecoder();
+
+    private static final String RSA_ALGORITHM = "RSA";
+
+    private static final String PKCS1_FORMAT = "PKCS#1";
+
+    private static final String PEM_BOUNDARY_PREFIX = "-----";
+
+    /**
+     * Read RSA Private Key from PEM-encoded PKCS #1 string
+     *
+     * @param inputPrivateKey PEM-encoded string
+     * @return RSA Private Key
+     * @throws GeneralSecurityException Thrown on failures to parse private key
+     */
+    @Override
+    public PrivateKey readPrivateKey(final String inputPrivateKey) throws 
GeneralSecurityException {
+        Objects.requireNonNull(inputPrivateKey, "Private Key required");
+
+        final byte[] decoded = getDecoded(inputPrivateKey);
+
+        final PrivateKey encodedPrivateKey = new 
PKCS1EncodedPrivateKey(decoded);
+        final KeyFactory keyFactory = KeyFactory.getInstance(RSA_ALGORITHM);
+        final Key translatedKey = keyFactory.translateKey(encodedPrivateKey);
+        if (translatedKey instanceof RSAPrivateKey) {
+            return (RSAPrivateKey) translatedKey;
+        } else {
+            throw new InvalidKeyException("Failed to parse encoded RSA Private 
Key: unsupported class [%s]".formatted(translatedKey.getClass()));
+        }
+    }
+
+    private byte[] getDecoded(final String inputPrivateKey) throws 
GeneralSecurityException {
+        try (BufferedReader bufferedReader = new BufferedReader(new 
StringReader(inputPrivateKey))) {
+            final StringBuilder encodedBuilder = new StringBuilder();
+
+            String line = bufferedReader.readLine();
+            while (line != null) {
+                if (!line.startsWith(PEM_BOUNDARY_PREFIX)) {
+                    encodedBuilder.append(line);
+                }
+
+                line = bufferedReader.readLine();
+            }
+
+            final String encoded = encodedBuilder.toString();
+            return DECODER.decode(encoded);
+        } catch (final IOException e) {
+            throw new InvalidKeyException("Failed to read Private Key", e);
+        }
+    }
+
+    private static class PKCS1EncodedPrivateKey implements PrivateKey {
+
+        private final byte[] encoded;
+
+        private PKCS1EncodedPrivateKey(final byte[] encoded) {
+            this.encoded = encoded;
+        }
+
+        @Override
+        public String getAlgorithm() {
+            return RSA_ALGORITHM;
+        }
+
+        @Override
+        public String getFormat() {
+            return PKCS1_FORMAT;
+        }
+
+        @Override
+        public byte[] getEncoded() {
+            return encoded.clone();
+        }
+    }
+}

Reply via email to