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