mirror of
https://github.com/jlengrand/github-api.git
synced 2026-03-10 08:21:21 +00:00
Merge pull request #945 from MarcosCela/feat/credential-provider-refresh
Feat/credential provider refresh
This commit is contained in:
13
pom.xml
13
pom.xml
@@ -45,6 +45,7 @@
|
|||||||
<jacoco.coverage.target.class.method>0.25</jacoco.coverage.target.class.method>
|
<jacoco.coverage.target.class.method>0.25</jacoco.coverage.target.class.method>
|
||||||
<!-- For non-ci builds we'd like the build to still complete if jacoco metrics aren't met. -->
|
<!-- For non-ci builds we'd like the build to still complete if jacoco metrics aren't met. -->
|
||||||
<jacoco.haltOnFailure>false</jacoco.haltOnFailure>
|
<jacoco.haltOnFailure>false</jacoco.haltOnFailure>
|
||||||
|
<jjwt.suite.version>0.11.2</jjwt.suite.version>
|
||||||
</properties>
|
</properties>
|
||||||
|
|
||||||
<build>
|
<build>
|
||||||
@@ -489,20 +490,20 @@
|
|||||||
<dependency>
|
<dependency>
|
||||||
<groupId>io.jsonwebtoken</groupId>
|
<groupId>io.jsonwebtoken</groupId>
|
||||||
<artifactId>jjwt-api</artifactId>
|
<artifactId>jjwt-api</artifactId>
|
||||||
<version>0.11.2</version>
|
<version>${jjwt.suite.version}</version>
|
||||||
<scope>test</scope>
|
<optional>true</optional>
|
||||||
</dependency>
|
</dependency>
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>io.jsonwebtoken</groupId>
|
<groupId>io.jsonwebtoken</groupId>
|
||||||
<artifactId>jjwt-impl</artifactId>
|
<artifactId>jjwt-impl</artifactId>
|
||||||
<version>0.11.2</version>
|
<version>${jjwt.suite.version}</version>
|
||||||
<scope>test</scope>
|
<optional>true</optional>
|
||||||
</dependency>
|
</dependency>
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>io.jsonwebtoken</groupId>
|
<groupId>io.jsonwebtoken</groupId>
|
||||||
<artifactId>jjwt-jackson</artifactId>
|
<artifactId>jjwt-jackson</artifactId>
|
||||||
<version>0.11.2</version>
|
<version>${jjwt.suite.version}</version>
|
||||||
<scope>test</scope>
|
<optional>true</optional>
|
||||||
</dependency>
|
</dependency>
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>com.squareup.okio</groupId>
|
<groupId>com.squareup.okio</groupId>
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import java.util.Locale;
|
|||||||
public enum GHEvent {
|
public enum GHEvent {
|
||||||
CHECK_RUN,
|
CHECK_RUN,
|
||||||
CHECK_SUITE,
|
CHECK_SUITE,
|
||||||
|
CODE_SCANNING_ALERT,
|
||||||
COMMIT_COMMENT,
|
COMMIT_COMMENT,
|
||||||
CONTENT_REFERENCE,
|
CONTENT_REFERENCE,
|
||||||
CREATE,
|
CREATE,
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ package org.kohsuke.github;
|
|||||||
import com.fasterxml.jackson.databind.ObjectReader;
|
import com.fasterxml.jackson.databind.ObjectReader;
|
||||||
import com.fasterxml.jackson.databind.ObjectWriter;
|
import com.fasterxml.jackson.databind.ObjectWriter;
|
||||||
import com.infradna.tool.bridge_method_injector.WithBridgeMethods;
|
import com.infradna.tool.bridge_method_injector.WithBridgeMethods;
|
||||||
|
import org.kohsuke.github.authorization.AuthorizationProvider;
|
||||||
import org.kohsuke.github.internal.Previews;
|
import org.kohsuke.github.internal.Previews;
|
||||||
|
|
||||||
import java.io.*;
|
import java.io.*;
|
||||||
@@ -94,39 +95,112 @@ public class GitHub {
|
|||||||
* "http://ghe.acme.com/api/v3". Note that GitHub Enterprise has <code>/api/v3</code> in the URL. For
|
* "http://ghe.acme.com/api/v3". Note that GitHub Enterprise has <code>/api/v3</code> in the URL. For
|
||||||
* historical reasons, this parameter still accepts the bare domain name, but that's considered
|
* historical reasons, this parameter still accepts the bare domain name, but that's considered
|
||||||
* deprecated. Password is also considered deprecated as it is no longer required for api usage.
|
* deprecated. Password is also considered deprecated as it is no longer required for api usage.
|
||||||
* @param login
|
|
||||||
* The user ID on GitHub that you are logging in as. Can be omitted if the OAuth token is provided or if
|
|
||||||
* logging in anonymously. Specifying this would save one API call.
|
|
||||||
* @param oauthAccessToken
|
|
||||||
* Secret OAuth token.
|
|
||||||
* @param password
|
|
||||||
* User's password. Always used in conjunction with the {@code login} parameter
|
|
||||||
* @param connector
|
* @param connector
|
||||||
* HttpConnector to use. Pass null to use default connector.
|
* a connector
|
||||||
|
* @param rateLimitHandler
|
||||||
|
* rateLimitHandler
|
||||||
|
* @param abuseLimitHandler
|
||||||
|
* abuseLimitHandler
|
||||||
|
* @param rateLimitChecker
|
||||||
|
* rateLimitChecker
|
||||||
|
* @param authorizationProvider
|
||||||
|
* a authorization provider
|
||||||
*/
|
*/
|
||||||
GitHub(String apiUrl,
|
GitHub(String apiUrl,
|
||||||
String login,
|
|
||||||
String oauthAccessToken,
|
|
||||||
String jwtToken,
|
|
||||||
String password,
|
|
||||||
HttpConnector connector,
|
HttpConnector connector,
|
||||||
RateLimitHandler rateLimitHandler,
|
RateLimitHandler rateLimitHandler,
|
||||||
AbuseLimitHandler abuseLimitHandler,
|
AbuseLimitHandler abuseLimitHandler,
|
||||||
GitHubRateLimitChecker rateLimitChecker) throws IOException {
|
GitHubRateLimitChecker rateLimitChecker,
|
||||||
|
AuthorizationProvider authorizationProvider) throws IOException {
|
||||||
|
if (authorizationProvider instanceof DependentAuthorizationProvider) {
|
||||||
|
((DependentAuthorizationProvider) authorizationProvider).bind(this);
|
||||||
|
}
|
||||||
|
|
||||||
this.client = new GitHubHttpUrlConnectionClient(apiUrl,
|
this.client = new GitHubHttpUrlConnectionClient(apiUrl,
|
||||||
login,
|
|
||||||
oauthAccessToken,
|
|
||||||
jwtToken,
|
|
||||||
password,
|
|
||||||
connector,
|
connector,
|
||||||
rateLimitHandler,
|
rateLimitHandler,
|
||||||
abuseLimitHandler,
|
abuseLimitHandler,
|
||||||
rateLimitChecker,
|
rateLimitChecker,
|
||||||
(myself) -> setMyself(myself));
|
(myself) -> setMyself(myself),
|
||||||
|
authorizationProvider);
|
||||||
users = new ConcurrentHashMap<>();
|
users = new ConcurrentHashMap<>();
|
||||||
orgs = new ConcurrentHashMap<>();
|
orgs = new ConcurrentHashMap<>();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private GitHub(GitHubClient client) {
|
||||||
|
this.client = client;
|
||||||
|
users = new ConcurrentHashMap<>();
|
||||||
|
orgs = new ConcurrentHashMap<>();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static abstract class DependentAuthorizationProvider implements AuthorizationProvider {
|
||||||
|
|
||||||
|
private GitHub baseGitHub;
|
||||||
|
private GitHub gitHub;
|
||||||
|
private final AuthorizationProvider authorizationProvider;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* An AuthorizationProvider that requires an authenticated GitHub instance to provide its authorization.
|
||||||
|
*
|
||||||
|
* @param authorizationProvider
|
||||||
|
* A authorization provider to be used when refreshing this authorization provider.
|
||||||
|
*/
|
||||||
|
@BetaApi
|
||||||
|
@Deprecated
|
||||||
|
protected DependentAuthorizationProvider(AuthorizationProvider authorizationProvider) {
|
||||||
|
this.authorizationProvider = authorizationProvider;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Binds this authorization provider to a github instance.
|
||||||
|
*
|
||||||
|
* Only needs to be implemented by dynamic credentials providers that use a github instance in order to refresh.
|
||||||
|
*
|
||||||
|
* @param github
|
||||||
|
* The github instance to be used for refreshing dynamic credentials
|
||||||
|
*/
|
||||||
|
synchronized void bind(GitHub github) {
|
||||||
|
if (baseGitHub != null) {
|
||||||
|
throw new IllegalStateException("Already bound to another GitHub instance.");
|
||||||
|
}
|
||||||
|
this.baseGitHub = github;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected synchronized final GitHub gitHub() {
|
||||||
|
if (gitHub == null) {
|
||||||
|
gitHub = new GitHub.AuthorizationRefreshGitHubWrapper(this.baseGitHub, authorizationProvider);
|
||||||
|
}
|
||||||
|
return gitHub;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static class AuthorizationRefreshGitHubWrapper extends GitHub {
|
||||||
|
|
||||||
|
private final AuthorizationProvider authorizationProvider;
|
||||||
|
|
||||||
|
AuthorizationRefreshGitHubWrapper(GitHub github, AuthorizationProvider authorizationProvider) {
|
||||||
|
super(github.client);
|
||||||
|
this.authorizationProvider = authorizationProvider;
|
||||||
|
|
||||||
|
// no dependent authorization providers nest like this currently, but they might in future
|
||||||
|
if (authorizationProvider instanceof DependentAuthorizationProvider) {
|
||||||
|
((DependentAuthorizationProvider) authorizationProvider).bind(this);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Nonnull
|
||||||
|
@Override
|
||||||
|
Requester createRequest() {
|
||||||
|
try {
|
||||||
|
// Override
|
||||||
|
return super.createRequest().setHeader("Authorization", authorizationProvider.getEncodedAuthorization())
|
||||||
|
.rateLimit(RateLimitTarget.NONE);
|
||||||
|
} catch (IOException e) {
|
||||||
|
throw new GHException("Failed to create requester to refresh credentials", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Obtains the credential from "~/.github" or from the System Environment Properties.
|
* Obtains the credential from "~/.github" or from the System Environment Properties.
|
||||||
*
|
*
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
package org.kohsuke.github;
|
package org.kohsuke.github;
|
||||||
|
|
||||||
import org.apache.commons.io.IOUtils;
|
import org.apache.commons.io.IOUtils;
|
||||||
|
import org.kohsuke.github.authorization.AuthorizationProvider;
|
||||||
|
import org.kohsuke.github.authorization.ImmutableAuthorizationProvider;
|
||||||
import org.kohsuke.github.extras.ImpatientHttpConnector;
|
import org.kohsuke.github.extras.ImpatientHttpConnector;
|
||||||
|
|
||||||
import java.io.File;
|
import java.io.File;
|
||||||
@@ -24,16 +26,13 @@ public class GitHubBuilder implements Cloneable {
|
|||||||
|
|
||||||
// default scoped so unit tests can read them.
|
// default scoped so unit tests can read them.
|
||||||
/* private */ String endpoint = GitHubClient.GITHUB_URL;
|
/* private */ String endpoint = GitHubClient.GITHUB_URL;
|
||||||
/* private */ String user;
|
|
||||||
/* private */ String password;
|
|
||||||
/* private */ String oauthToken;
|
|
||||||
/* private */ String jwtToken;
|
|
||||||
|
|
||||||
private HttpConnector connector;
|
private HttpConnector connector;
|
||||||
|
|
||||||
private RateLimitHandler rateLimitHandler = RateLimitHandler.WAIT;
|
private RateLimitHandler rateLimitHandler = RateLimitHandler.WAIT;
|
||||||
private AbuseLimitHandler abuseLimitHandler = AbuseLimitHandler.WAIT;
|
private AbuseLimitHandler abuseLimitHandler = AbuseLimitHandler.WAIT;
|
||||||
private GitHubRateLimitChecker rateLimitChecker = new GitHubRateLimitChecker();
|
private GitHubRateLimitChecker rateLimitChecker = new GitHubRateLimitChecker();
|
||||||
|
/* private */ AuthorizationProvider authorizationProvider = AuthorizationProvider.ANONYMOUS;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Instantiates a new Git hub builder.
|
* Instantiates a new Git hub builder.
|
||||||
@@ -61,13 +60,13 @@ public class GitHubBuilder implements Cloneable {
|
|||||||
|
|
||||||
builder = fromEnvironment();
|
builder = fromEnvironment();
|
||||||
|
|
||||||
if (builder.oauthToken != null || builder.user != null || builder.jwtToken != null)
|
if (builder.authorizationProvider != null)
|
||||||
return builder;
|
return builder;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
builder = fromPropertyFile();
|
builder = fromPropertyFile();
|
||||||
|
|
||||||
if (builder.oauthToken != null || builder.user != null || builder.jwtToken != null)
|
if (builder.authorizationProvider != null)
|
||||||
return builder;
|
return builder;
|
||||||
} catch (FileNotFoundException e) {
|
} catch (FileNotFoundException e) {
|
||||||
// fall through
|
// fall through
|
||||||
@@ -215,9 +214,20 @@ public class GitHubBuilder implements Cloneable {
|
|||||||
*/
|
*/
|
||||||
public static GitHubBuilder fromProperties(Properties props) {
|
public static GitHubBuilder fromProperties(Properties props) {
|
||||||
GitHubBuilder self = new GitHubBuilder();
|
GitHubBuilder self = new GitHubBuilder();
|
||||||
self.withOAuthToken(props.getProperty("oauth"), props.getProperty("login"));
|
String oauth = props.getProperty("oauth");
|
||||||
self.withJwtToken(props.getProperty("jwt"));
|
String jwt = props.getProperty("jwt");
|
||||||
self.withPassword(props.getProperty("login"), props.getProperty("password"));
|
String login = props.getProperty("login");
|
||||||
|
String password = props.getProperty("password");
|
||||||
|
|
||||||
|
if (oauth != null) {
|
||||||
|
self.withOAuthToken(oauth, login);
|
||||||
|
}
|
||||||
|
if (jwt != null) {
|
||||||
|
self.withJwtToken(jwt);
|
||||||
|
}
|
||||||
|
if (password != null) {
|
||||||
|
self.withPassword(login, password);
|
||||||
|
}
|
||||||
self.withEndpoint(props.getProperty("endpoint", GitHubClient.GITHUB_URL));
|
self.withEndpoint(props.getProperty("endpoint", GitHubClient.GITHUB_URL));
|
||||||
return self;
|
return self;
|
||||||
}
|
}
|
||||||
@@ -247,9 +257,7 @@ public class GitHubBuilder implements Cloneable {
|
|||||||
* @return the git hub builder
|
* @return the git hub builder
|
||||||
*/
|
*/
|
||||||
public GitHubBuilder withPassword(String user, String password) {
|
public GitHubBuilder withPassword(String user, String password) {
|
||||||
this.user = user;
|
return withAuthorizationProvider(ImmutableAuthorizationProvider.fromLoginAndPassword(user, password));
|
||||||
this.password = password;
|
|
||||||
return this;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -260,7 +268,7 @@ public class GitHubBuilder implements Cloneable {
|
|||||||
* @return the git hub builder
|
* @return the git hub builder
|
||||||
*/
|
*/
|
||||||
public GitHubBuilder withOAuthToken(String oauthToken) {
|
public GitHubBuilder withOAuthToken(String oauthToken) {
|
||||||
return withOAuthToken(oauthToken, null);
|
return withAuthorizationProvider(ImmutableAuthorizationProvider.fromOauthToken(oauthToken));
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -273,8 +281,21 @@ public class GitHubBuilder implements Cloneable {
|
|||||||
* @return the git hub builder
|
* @return the git hub builder
|
||||||
*/
|
*/
|
||||||
public GitHubBuilder withOAuthToken(String oauthToken, String user) {
|
public GitHubBuilder withOAuthToken(String oauthToken, String user) {
|
||||||
this.oauthToken = oauthToken;
|
return withAuthorizationProvider(ImmutableAuthorizationProvider.fromOauthToken(oauthToken, user));
|
||||||
this.user = user;
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Configures a {@link AuthorizationProvider} for this builder
|
||||||
|
*
|
||||||
|
* There can be only one authorization provider per client instance.
|
||||||
|
*
|
||||||
|
* @param authorizationProvider
|
||||||
|
* the authorization provider
|
||||||
|
* @return the git hub builder
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
public GitHubBuilder withAuthorizationProvider(final AuthorizationProvider authorizationProvider) {
|
||||||
|
this.authorizationProvider = authorizationProvider;
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -287,7 +308,7 @@ public class GitHubBuilder implements Cloneable {
|
|||||||
* @see GHAppInstallation#createToken(java.util.Map) GHAppInstallation#createToken(java.util.Map)
|
* @see GHAppInstallation#createToken(java.util.Map) GHAppInstallation#createToken(java.util.Map)
|
||||||
*/
|
*/
|
||||||
public GitHubBuilder withAppInstallationToken(String appInstallationToken) {
|
public GitHubBuilder withAppInstallationToken(String appInstallationToken) {
|
||||||
return withOAuthToken(appInstallationToken, "");
|
return withAuthorizationProvider(ImmutableAuthorizationProvider.fromAppInstallationToken(appInstallationToken));
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -298,8 +319,7 @@ public class GitHubBuilder implements Cloneable {
|
|||||||
* @return the git hub builder
|
* @return the git hub builder
|
||||||
*/
|
*/
|
||||||
public GitHubBuilder withJwtToken(String jwtToken) {
|
public GitHubBuilder withJwtToken(String jwtToken) {
|
||||||
this.jwtToken = jwtToken;
|
return withAuthorizationProvider(ImmutableAuthorizationProvider.fromJwtToken(jwtToken));
|
||||||
return this;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -421,14 +441,11 @@ public class GitHubBuilder implements Cloneable {
|
|||||||
*/
|
*/
|
||||||
public GitHub build() throws IOException {
|
public GitHub build() throws IOException {
|
||||||
return new GitHub(endpoint,
|
return new GitHub(endpoint,
|
||||||
user,
|
|
||||||
oauthToken,
|
|
||||||
jwtToken,
|
|
||||||
password,
|
|
||||||
connector,
|
connector,
|
||||||
rateLimitHandler,
|
rateLimitHandler,
|
||||||
abuseLimitHandler,
|
abuseLimitHandler,
|
||||||
rateLimitChecker);
|
rateLimitChecker,
|
||||||
|
authorizationProvider);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|||||||
@@ -1,33 +1,19 @@
|
|||||||
package org.kohsuke.github;
|
package org.kohsuke.github;
|
||||||
|
|
||||||
import com.fasterxml.jackson.databind.DeserializationFeature;
|
import com.fasterxml.jackson.databind.*;
|
||||||
import com.fasterxml.jackson.databind.InjectableValues;
|
|
||||||
import com.fasterxml.jackson.databind.MapperFeature;
|
|
||||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
|
||||||
import com.fasterxml.jackson.databind.ObjectReader;
|
|
||||||
import com.fasterxml.jackson.databind.ObjectWriter;
|
|
||||||
import com.fasterxml.jackson.databind.PropertyNamingStrategy;
|
|
||||||
import com.fasterxml.jackson.databind.introspect.VisibilityChecker;
|
import com.fasterxml.jackson.databind.introspect.VisibilityChecker;
|
||||||
import org.apache.commons.io.IOUtils;
|
import org.apache.commons.io.IOUtils;
|
||||||
|
import org.kohsuke.github.authorization.AuthorizationProvider;
|
||||||
|
import org.kohsuke.github.authorization.UserAuthorizationProvider;
|
||||||
|
|
||||||
import java.io.FileNotFoundException;
|
import java.io.FileNotFoundException;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.io.InterruptedIOException;
|
import java.io.InterruptedIOException;
|
||||||
import java.net.HttpURLConnection;
|
import java.net.*;
|
||||||
import java.net.MalformedURLException;
|
|
||||||
import java.net.SocketException;
|
|
||||||
import java.net.SocketTimeoutException;
|
|
||||||
import java.net.URL;
|
|
||||||
import java.nio.charset.StandardCharsets;
|
|
||||||
import java.time.Instant;
|
import java.time.Instant;
|
||||||
import java.time.format.DateTimeFormatter;
|
import java.time.format.DateTimeFormatter;
|
||||||
import java.time.temporal.ChronoUnit;
|
import java.time.temporal.ChronoUnit;
|
||||||
import java.util.Base64;
|
import java.util.*;
|
||||||
import java.util.Date;
|
|
||||||
import java.util.HashMap;
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.Map;
|
|
||||||
import java.util.Objects;
|
|
||||||
import java.util.function.Consumer;
|
import java.util.function.Consumer;
|
||||||
import java.util.logging.Logger;
|
import java.util.logging.Logger;
|
||||||
|
|
||||||
@@ -56,17 +42,13 @@ abstract class GitHubClient {
|
|||||||
static final int retryTimeoutMillis = 100;
|
static final int retryTimeoutMillis = 100;
|
||||||
/* private */ final String login;
|
/* private */ final String login;
|
||||||
|
|
||||||
/**
|
|
||||||
* Value of the authorization header to be sent with the request.
|
|
||||||
*/
|
|
||||||
/* private */ final String encodedAuthorization;
|
|
||||||
|
|
||||||
// Cache of myself object.
|
// Cache of myself object.
|
||||||
private final String apiUrl;
|
private final String apiUrl;
|
||||||
|
|
||||||
protected final RateLimitHandler rateLimitHandler;
|
protected final RateLimitHandler rateLimitHandler;
|
||||||
protected final AbuseLimitHandler abuseLimitHandler;
|
protected final AbuseLimitHandler abuseLimitHandler;
|
||||||
private final GitHubRateLimitChecker rateLimitChecker;
|
private final GitHubRateLimitChecker rateLimitChecker;
|
||||||
|
private final AuthorizationProvider authorizationProvider;
|
||||||
|
|
||||||
private HttpConnector connector;
|
private HttpConnector connector;
|
||||||
|
|
||||||
@@ -91,15 +73,12 @@ abstract class GitHubClient {
|
|||||||
}
|
}
|
||||||
|
|
||||||
GitHubClient(String apiUrl,
|
GitHubClient(String apiUrl,
|
||||||
String login,
|
|
||||||
String oauthAccessToken,
|
|
||||||
String jwtToken,
|
|
||||||
String password,
|
|
||||||
HttpConnector connector,
|
HttpConnector connector,
|
||||||
RateLimitHandler rateLimitHandler,
|
RateLimitHandler rateLimitHandler,
|
||||||
AbuseLimitHandler abuseLimitHandler,
|
AbuseLimitHandler abuseLimitHandler,
|
||||||
GitHubRateLimitChecker rateLimitChecker,
|
GitHubRateLimitChecker rateLimitChecker,
|
||||||
Consumer<GHMyself> myselfConsumer) throws IOException {
|
Consumer<GHMyself> myselfConsumer,
|
||||||
|
AuthorizationProvider authorizationProvider) throws IOException {
|
||||||
|
|
||||||
if (apiUrl.endsWith("/")) {
|
if (apiUrl.endsWith("/")) {
|
||||||
apiUrl = apiUrl.substring(0, apiUrl.length() - 1); // normalize
|
apiUrl = apiUrl.substring(0, apiUrl.length() - 1); // normalize
|
||||||
@@ -111,33 +90,38 @@ abstract class GitHubClient {
|
|||||||
this.apiUrl = apiUrl;
|
this.apiUrl = apiUrl;
|
||||||
this.connector = connector;
|
this.connector = connector;
|
||||||
|
|
||||||
if (oauthAccessToken != null) {
|
// Prefer credential configuration via provider
|
||||||
encodedAuthorization = "token " + oauthAccessToken;
|
this.authorizationProvider = authorizationProvider;
|
||||||
} else {
|
|
||||||
if (jwtToken != null) {
|
|
||||||
encodedAuthorization = "Bearer " + jwtToken;
|
|
||||||
} else if (password != null) {
|
|
||||||
String authorization = (login + ':' + password);
|
|
||||||
String charsetName = StandardCharsets.UTF_8.name();
|
|
||||||
encodedAuthorization = "Basic "
|
|
||||||
+ Base64.getEncoder().encodeToString(authorization.getBytes(charsetName));
|
|
||||||
} else {// anonymous access
|
|
||||||
encodedAuthorization = null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
this.rateLimitHandler = rateLimitHandler;
|
this.rateLimitHandler = rateLimitHandler;
|
||||||
this.abuseLimitHandler = abuseLimitHandler;
|
this.abuseLimitHandler = abuseLimitHandler;
|
||||||
this.rateLimitChecker = rateLimitChecker;
|
this.rateLimitChecker = rateLimitChecker;
|
||||||
|
|
||||||
if (login == null && encodedAuthorization != null && jwtToken == null) {
|
this.login = getCurrentUser(myselfConsumer);
|
||||||
GHMyself myself = fetch(GHMyself.class, "/user");
|
}
|
||||||
login = myself.getLogin();
|
|
||||||
if (myselfConsumer != null) {
|
private String getCurrentUser(Consumer<GHMyself> myselfConsumer) throws IOException {
|
||||||
myselfConsumer.accept(myself);
|
String login = null;
|
||||||
|
if (this.authorizationProvider instanceof UserAuthorizationProvider
|
||||||
|
&& this.authorizationProvider.getEncodedAuthorization() != null) {
|
||||||
|
|
||||||
|
UserAuthorizationProvider userAuthorizationProvider = (UserAuthorizationProvider) this.authorizationProvider;
|
||||||
|
|
||||||
|
login = userAuthorizationProvider.getLogin();
|
||||||
|
|
||||||
|
if (login == null) {
|
||||||
|
try {
|
||||||
|
GHMyself myself = fetch(GHMyself.class, "/user");
|
||||||
|
if (myselfConsumer != null) {
|
||||||
|
myselfConsumer.accept(myself);
|
||||||
|
}
|
||||||
|
login = myself.getLogin();
|
||||||
|
} catch (IOException e) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
this.login = login;
|
return login;
|
||||||
}
|
}
|
||||||
|
|
||||||
private <T> T fetch(Class<T> type, String urlPath) throws IOException {
|
private <T> T fetch(Class<T> type, String urlPath) throws IOException {
|
||||||
@@ -202,7 +186,13 @@ abstract class GitHubClient {
|
|||||||
* @return {@code true} if operations that require authentication will fail.
|
* @return {@code true} if operations that require authentication will fail.
|
||||||
*/
|
*/
|
||||||
public boolean isAnonymous() {
|
public boolean isAnonymous() {
|
||||||
return login == null && encodedAuthorization == null;
|
try {
|
||||||
|
return login == null && this.authorizationProvider.getEncodedAuthorization() == null;
|
||||||
|
} catch (IOException e) {
|
||||||
|
// An exception here means that the provider failed to provide authorization parameters,
|
||||||
|
// basically meaning the same as "no auth"
|
||||||
|
return false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -224,6 +214,11 @@ abstract class GitHubClient {
|
|||||||
return getRateLimit(RateLimitTarget.NONE);
|
return getRateLimit(RateLimitTarget.NONE);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@CheckForNull
|
||||||
|
protected String getEncodedAuthorization() throws IOException {
|
||||||
|
return authorizationProvider.getEncodedAuthorization();
|
||||||
|
}
|
||||||
|
|
||||||
@Nonnull
|
@Nonnull
|
||||||
GHRateLimit getRateLimit(@Nonnull RateLimitTarget rateLimitTarget) throws IOException {
|
GHRateLimit getRateLimit(@Nonnull RateLimitTarget rateLimitTarget) throws IOException {
|
||||||
GHRateLimit result;
|
GHRateLimit result;
|
||||||
@@ -394,7 +389,6 @@ abstract class GitHubClient {
|
|||||||
"GitHub API request [" + (login == null ? "anonymous" : login) + "]: "
|
"GitHub API request [" + (login == null ? "anonymous" : login) + "]: "
|
||||||
+ request.method() + " " + request.url().toString());
|
+ request.method() + " " + request.url().toString());
|
||||||
}
|
}
|
||||||
|
|
||||||
rateLimitChecker.checkRateLimit(this, request);
|
rateLimitChecker.checkRateLimit(this, request);
|
||||||
|
|
||||||
responseInfo = getResponseInfo(request);
|
responseInfo = getResponseInfo(request);
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
package org.kohsuke.github;
|
package org.kohsuke.github;
|
||||||
|
|
||||||
import org.apache.commons.io.IOUtils;
|
import org.apache.commons.io.IOUtils;
|
||||||
|
import org.kohsuke.github.authorization.AuthorizationProvider;
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.io.InputStream;
|
import java.io.InputStream;
|
||||||
@@ -33,25 +34,19 @@ import static org.apache.commons.lang3.StringUtils.defaultString;
|
|||||||
class GitHubHttpUrlConnectionClient extends GitHubClient {
|
class GitHubHttpUrlConnectionClient extends GitHubClient {
|
||||||
|
|
||||||
GitHubHttpUrlConnectionClient(String apiUrl,
|
GitHubHttpUrlConnectionClient(String apiUrl,
|
||||||
String login,
|
|
||||||
String oauthAccessToken,
|
|
||||||
String jwtToken,
|
|
||||||
String password,
|
|
||||||
HttpConnector connector,
|
HttpConnector connector,
|
||||||
RateLimitHandler rateLimitHandler,
|
RateLimitHandler rateLimitHandler,
|
||||||
AbuseLimitHandler abuseLimitHandler,
|
AbuseLimitHandler abuseLimitHandler,
|
||||||
GitHubRateLimitChecker rateLimitChecker,
|
GitHubRateLimitChecker rateLimitChecker,
|
||||||
Consumer<GHMyself> myselfConsumer) throws IOException {
|
Consumer<GHMyself> myselfConsumer,
|
||||||
|
AuthorizationProvider authorizationProvider) throws IOException {
|
||||||
super(apiUrl,
|
super(apiUrl,
|
||||||
login,
|
|
||||||
oauthAccessToken,
|
|
||||||
jwtToken,
|
|
||||||
password,
|
|
||||||
connector,
|
connector,
|
||||||
rateLimitHandler,
|
rateLimitHandler,
|
||||||
abuseLimitHandler,
|
abuseLimitHandler,
|
||||||
rateLimitChecker,
|
rateLimitChecker,
|
||||||
myselfConsumer);
|
myselfConsumer,
|
||||||
|
authorizationProvider);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Nonnull
|
@Nonnull
|
||||||
@@ -114,8 +109,12 @@ class GitHubHttpUrlConnectionClient extends GitHubClient {
|
|||||||
|
|
||||||
// if the authentication is needed but no credential is given, try it anyway (so that some calls
|
// if the authentication is needed but no credential is given, try it anyway (so that some calls
|
||||||
// that do work with anonymous access in the reduced form should still work.)
|
// that do work with anonymous access in the reduced form should still work.)
|
||||||
if (client.encodedAuthorization != null)
|
if (!request.headers().containsKey("Authorization")) {
|
||||||
connection.setRequestProperty("Authorization", client.encodedAuthorization);
|
String authorization = client.getEncodedAuthorization();
|
||||||
|
if (authorization != null) {
|
||||||
|
connection.setRequestProperty("Authorization", client.getEncodedAuthorization());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
setRequestMethod(request.method(), connection);
|
setRequestMethod(request.method(), connection);
|
||||||
buildRequest(request, connection);
|
buildRequest(request, connection);
|
||||||
|
|||||||
@@ -0,0 +1,43 @@
|
|||||||
|
package org.kohsuke.github.authorization;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Provides a functional interface that returns a valid encodedAuthorization. This strategy allows for a provider that
|
||||||
|
* dynamically changes the credentials. Each request will request the credentials from the provider.
|
||||||
|
*/
|
||||||
|
public interface AuthorizationProvider {
|
||||||
|
/**
|
||||||
|
* An static instance for an ANONYMOUS authorization provider
|
||||||
|
*/
|
||||||
|
AuthorizationProvider ANONYMOUS = new AnonymousAuthorizationProvider();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the credentials to be used with a given request. As an example, a authorization provider for a bearer
|
||||||
|
* token will return something like:
|
||||||
|
*
|
||||||
|
* <pre>
|
||||||
|
* {@code
|
||||||
|
* @Override
|
||||||
|
* public String getEncodedAuthorization() {
|
||||||
|
* return "Bearer myBearerToken";
|
||||||
|
* }
|
||||||
|
* }
|
||||||
|
* </pre>
|
||||||
|
*
|
||||||
|
* @return encoded authorization string, can be null
|
||||||
|
* @throws IOException
|
||||||
|
* on any error that prevents the provider from getting a valid authorization
|
||||||
|
*/
|
||||||
|
String getEncodedAuthorization() throws IOException;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A {@link AuthorizationProvider} that ensures that no credentials are returned
|
||||||
|
*/
|
||||||
|
class AnonymousAuthorizationProvider implements AuthorizationProvider {
|
||||||
|
@Override
|
||||||
|
public String getEncodedAuthorization() throws IOException {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,123 @@
|
|||||||
|
package org.kohsuke.github.authorization;
|
||||||
|
|
||||||
|
import java.io.UnsupportedEncodingException;
|
||||||
|
import java.nio.charset.StandardCharsets;
|
||||||
|
import java.util.Base64;
|
||||||
|
|
||||||
|
import javax.annotation.CheckForNull;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A {@link AuthorizationProvider} that always returns the same credentials
|
||||||
|
*/
|
||||||
|
public class ImmutableAuthorizationProvider implements AuthorizationProvider {
|
||||||
|
|
||||||
|
private final String authorization;
|
||||||
|
|
||||||
|
public ImmutableAuthorizationProvider(String authorization) {
|
||||||
|
this.authorization = authorization;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Builds and returns a {@link AuthorizationProvider} from a given oauthAccessToken
|
||||||
|
*
|
||||||
|
* @param oauthAccessToken
|
||||||
|
* The token
|
||||||
|
* @return a correctly configured {@link AuthorizationProvider} that will always return the same provided
|
||||||
|
* oauthAccessToken
|
||||||
|
*/
|
||||||
|
public static AuthorizationProvider fromOauthToken(String oauthAccessToken) {
|
||||||
|
return new UserProvider(String.format("token %s", oauthAccessToken));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Builds and returns a {@link AuthorizationProvider} from a given oauthAccessToken
|
||||||
|
*
|
||||||
|
* @param oauthAccessToken
|
||||||
|
* The token
|
||||||
|
* @param login
|
||||||
|
* The login for this token
|
||||||
|
*
|
||||||
|
* @return a correctly configured {@link AuthorizationProvider} that will always return the same provided
|
||||||
|
* oauthAccessToken
|
||||||
|
*/
|
||||||
|
public static AuthorizationProvider fromOauthToken(String oauthAccessToken, String login) {
|
||||||
|
return new UserProvider(String.format("token %s", oauthAccessToken), login);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Builds and returns a {@link AuthorizationProvider} from a given App Installation Token
|
||||||
|
*
|
||||||
|
* @param appInstallationToken
|
||||||
|
* A string containing the GitHub App installation token
|
||||||
|
* @return the configured Builder from given GitHub App installation token.
|
||||||
|
*/
|
||||||
|
public static AuthorizationProvider fromAppInstallationToken(String appInstallationToken) {
|
||||||
|
return fromOauthToken(appInstallationToken, "");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Builds and returns a {@link AuthorizationProvider} from a given jwtToken
|
||||||
|
*
|
||||||
|
* @param jwtToken
|
||||||
|
* The JWT token
|
||||||
|
* @return a correctly configured {@link AuthorizationProvider} that will always return the same provided jwtToken
|
||||||
|
*/
|
||||||
|
public static AuthorizationProvider fromJwtToken(String jwtToken) {
|
||||||
|
return new ImmutableAuthorizationProvider(String.format("Bearer %s", jwtToken));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Builds and returns a {@link AuthorizationProvider} from the given user/password pair
|
||||||
|
*
|
||||||
|
* @param login
|
||||||
|
* The login for the user, usually the same as the username
|
||||||
|
* @param password
|
||||||
|
* The password for the associated user
|
||||||
|
* @return a correctly configured {@link AuthorizationProvider} that will always return the credentials for the same
|
||||||
|
* user and password combo
|
||||||
|
* @deprecated Login with password credentials are no longer supported by GitHub
|
||||||
|
*/
|
||||||
|
@Deprecated
|
||||||
|
public static AuthorizationProvider fromLoginAndPassword(String login, String password) {
|
||||||
|
try {
|
||||||
|
String authorization = (String.format("%s:%s", login, password));
|
||||||
|
String charsetName = StandardCharsets.UTF_8.name();
|
||||||
|
String b64encoded = Base64.getEncoder().encodeToString(authorization.getBytes(charsetName));
|
||||||
|
String encodedAuthorization = String.format("Basic %s", b64encoded);
|
||||||
|
return new UserProvider(encodedAuthorization, login);
|
||||||
|
} catch (UnsupportedEncodingException e) {
|
||||||
|
// If UTF-8 isn't supported, there are bigger problems
|
||||||
|
throw new IllegalStateException("Could not generate encoded authorization", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getEncodedAuthorization() {
|
||||||
|
return this.authorization;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* An internal class representing all user-related credentials, which are credentials that have a login or should
|
||||||
|
* query the user endpoint for the login matching this credential.
|
||||||
|
*/
|
||||||
|
private static class UserProvider extends ImmutableAuthorizationProvider implements UserAuthorizationProvider {
|
||||||
|
|
||||||
|
private final String login;
|
||||||
|
|
||||||
|
UserProvider(String authorization) {
|
||||||
|
this(authorization, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
UserProvider(String authorization, String login) {
|
||||||
|
super(authorization);
|
||||||
|
this.login = login;
|
||||||
|
}
|
||||||
|
|
||||||
|
@CheckForNull
|
||||||
|
@Override
|
||||||
|
public String getLogin() {
|
||||||
|
return login;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,63 @@
|
|||||||
|
package org.kohsuke.github.authorization;
|
||||||
|
|
||||||
|
import org.kohsuke.github.BetaApi;
|
||||||
|
import org.kohsuke.github.GHAppInstallation;
|
||||||
|
import org.kohsuke.github.GHAppInstallationToken;
|
||||||
|
import org.kohsuke.github.GitHub;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.time.Duration;
|
||||||
|
import java.time.Instant;
|
||||||
|
import java.util.Objects;
|
||||||
|
|
||||||
|
import javax.annotation.Nonnull;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Provides an AuthorizationProvider that performs automatic token refresh.
|
||||||
|
*/
|
||||||
|
public class OrgAppInstallationAuthorizationProvider extends GitHub.DependentAuthorizationProvider {
|
||||||
|
|
||||||
|
private final String organizationName;
|
||||||
|
|
||||||
|
private String latestToken;
|
||||||
|
|
||||||
|
@Nonnull
|
||||||
|
private Instant validUntil = Instant.MIN;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Provides an AuthorizationProvider that performs automatic token refresh, based on an previously authenticated
|
||||||
|
* github client.
|
||||||
|
*
|
||||||
|
* @param organizationName
|
||||||
|
* The name of the organization where the application is installed
|
||||||
|
* @param authorizationProvider
|
||||||
|
* A authorization provider that returns a JWT token that can be used to refresh the App Installation
|
||||||
|
* token from GitHub.
|
||||||
|
*/
|
||||||
|
@BetaApi
|
||||||
|
@Deprecated
|
||||||
|
public OrgAppInstallationAuthorizationProvider(String organizationName,
|
||||||
|
AuthorizationProvider authorizationProvider) {
|
||||||
|
super(authorizationProvider);
|
||||||
|
this.organizationName = organizationName;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getEncodedAuthorization() throws IOException {
|
||||||
|
synchronized (this) {
|
||||||
|
if (latestToken == null || Instant.now().isAfter(this.validUntil)) {
|
||||||
|
refreshToken();
|
||||||
|
}
|
||||||
|
return String.format("token %s", latestToken);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void refreshToken() throws IOException {
|
||||||
|
GitHub gitHub = this.gitHub();
|
||||||
|
GHAppInstallation installationByOrganization = gitHub.getApp()
|
||||||
|
.getInstallationByOrganization(this.organizationName);
|
||||||
|
GHAppInstallationToken ghAppInstallationToken = installationByOrganization.createToken().create();
|
||||||
|
this.validUntil = ghAppInstallationToken.getExpiresAt().toInstant().minus(Duration.ofMinutes(5));
|
||||||
|
this.latestToken = Objects.requireNonNull(ghAppInstallationToken.getToken());
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
package org.kohsuke.github.authorization;
|
||||||
|
|
||||||
|
import javax.annotation.CheckForNull;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Interface for all user-related authorization providers.
|
||||||
|
*
|
||||||
|
* {@link AuthorizationProvider}s can apply to a number of different account types. This interface applies to providers
|
||||||
|
* for user accounts, ones that have a login or should query the "/user" endpoint for the login matching this
|
||||||
|
* credential.
|
||||||
|
*/
|
||||||
|
public interface UserAuthorizationProvider extends AuthorizationProvider {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the user login name.
|
||||||
|
*
|
||||||
|
* @return the user login for this provider, or {@code null} if the login value should be queried from the "/user"
|
||||||
|
* endpoint.
|
||||||
|
*/
|
||||||
|
@CheckForNull
|
||||||
|
String getLogin();
|
||||||
|
}
|
||||||
@@ -0,0 +1,130 @@
|
|||||||
|
package org.kohsuke.github.extras.authorization;
|
||||||
|
|
||||||
|
import io.jsonwebtoken.JwtBuilder;
|
||||||
|
import io.jsonwebtoken.Jwts;
|
||||||
|
import io.jsonwebtoken.SignatureAlgorithm;
|
||||||
|
import org.kohsuke.github.authorization.AuthorizationProvider;
|
||||||
|
|
||||||
|
import java.io.File;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.nio.charset.StandardCharsets;
|
||||||
|
import java.nio.file.Files;
|
||||||
|
import java.nio.file.Path;
|
||||||
|
import java.security.GeneralSecurityException;
|
||||||
|
import java.security.KeyFactory;
|
||||||
|
import java.security.PrivateKey;
|
||||||
|
import java.security.spec.InvalidKeySpecException;
|
||||||
|
import java.security.spec.PKCS8EncodedKeySpec;
|
||||||
|
import java.time.Duration;
|
||||||
|
import java.time.Instant;
|
||||||
|
import java.util.Base64;
|
||||||
|
import java.util.Date;
|
||||||
|
|
||||||
|
import javax.annotation.Nonnull;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A authorization provider that gives valid JWT tokens. These tokens are then used to create a time-based token to
|
||||||
|
* authenticate as an application. This token provider does not provide any kind of caching, and will always request a
|
||||||
|
* new token to the API.
|
||||||
|
*/
|
||||||
|
public class JWTTokenProvider implements AuthorizationProvider {
|
||||||
|
|
||||||
|
private final PrivateKey privateKey;
|
||||||
|
|
||||||
|
@Nonnull
|
||||||
|
private Instant validUntil = Instant.MIN;
|
||||||
|
|
||||||
|
private String token;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The identifier for the application
|
||||||
|
*/
|
||||||
|
private final String applicationId;
|
||||||
|
|
||||||
|
public JWTTokenProvider(String applicationId, File keyFile) throws GeneralSecurityException, IOException {
|
||||||
|
this(applicationId, loadPrivateKey(keyFile.toPath()));
|
||||||
|
}
|
||||||
|
|
||||||
|
public JWTTokenProvider(String applicationId, Path keyPath) throws GeneralSecurityException, IOException {
|
||||||
|
this(applicationId, loadPrivateKey(keyPath));
|
||||||
|
}
|
||||||
|
|
||||||
|
public JWTTokenProvider(String applicationId, PrivateKey privateKey) {
|
||||||
|
this.privateKey = privateKey;
|
||||||
|
this.applicationId = applicationId;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getEncodedAuthorization() throws IOException {
|
||||||
|
synchronized (this) {
|
||||||
|
if (Instant.now().isAfter(validUntil)) {
|
||||||
|
token = refreshJWT();
|
||||||
|
}
|
||||||
|
return String.format("Bearer %s", token);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* add dependencies for a jwt suite You can generate a key to load in this method with:
|
||||||
|
*
|
||||||
|
* <pre>
|
||||||
|
* openssl pkcs8 -topk8 -inform PEM -outform DER -in ~/github-api-app.private-key.pem -out ~/github-api-app.private-key.der -nocrypt
|
||||||
|
* </pre>
|
||||||
|
*/
|
||||||
|
private static PrivateKey loadPrivateKey(Path keyPath) throws GeneralSecurityException, IOException {
|
||||||
|
String keyString = new String(Files.readAllBytes(keyPath), StandardCharsets.UTF_8);
|
||||||
|
return getPrivateKeyFromString(keyString);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Convert a PKCS#8 formatted private key in string format into a java PrivateKey
|
||||||
|
*
|
||||||
|
* @param key
|
||||||
|
* PCKS#8 string
|
||||||
|
* @return private key
|
||||||
|
* @throws GeneralSecurityException
|
||||||
|
* if we couldn't parse the string
|
||||||
|
*/
|
||||||
|
private static PrivateKey getPrivateKeyFromString(final String key) throws GeneralSecurityException {
|
||||||
|
if (key.contains(" RSA ")) {
|
||||||
|
throw new InvalidKeySpecException(
|
||||||
|
"Private key must be a PKCS#8 formatted string, to convert it from PKCS#1 use: "
|
||||||
|
+ "openssl pkcs8 -topk8 -inform PEM -outform PEM -in current-key.pem -out new-key.pem -nocrypt");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove all comments and whitespace from PEM
|
||||||
|
// such as "-----BEGIN PRIVATE KEY-----" and newlines
|
||||||
|
String privateKeyContent = key.replaceAll("(?m)^--.*", "").replaceAll("\\s", "");
|
||||||
|
|
||||||
|
KeyFactory kf = KeyFactory.getInstance("RSA");
|
||||||
|
|
||||||
|
try {
|
||||||
|
byte[] decode = Base64.getDecoder().decode(privateKeyContent);
|
||||||
|
PKCS8EncodedKeySpec keySpecPKCS8 = new PKCS8EncodedKeySpec(decode);
|
||||||
|
|
||||||
|
return kf.generatePrivate(keySpecPKCS8);
|
||||||
|
} catch (IllegalArgumentException e) {
|
||||||
|
throw new InvalidKeySpecException("Failed to decode private key: " + e.getMessage(), e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private String refreshJWT() {
|
||||||
|
Instant now = Instant.now();
|
||||||
|
|
||||||
|
// Token expires in 10 minutes
|
||||||
|
Instant expiration = Instant.now().plus(Duration.ofMinutes(10));
|
||||||
|
|
||||||
|
// Let's set the JWT Claims
|
||||||
|
JwtBuilder builder = Jwts.builder()
|
||||||
|
.setIssuedAt(Date.from(now))
|
||||||
|
.setExpiration(Date.from(expiration))
|
||||||
|
.setIssuer(this.applicationId)
|
||||||
|
.signWith(privateKey, SignatureAlgorithm.RS256);
|
||||||
|
|
||||||
|
// Token will refresh after 8 minutes
|
||||||
|
validUntil = expiration.minus(Duration.ofMinutes(2));
|
||||||
|
|
||||||
|
// Builds the JWT and serializes it to a compact, URL-safe string
|
||||||
|
return builder.compact();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -2,8 +2,12 @@ package org.kohsuke.github;
|
|||||||
|
|
||||||
import io.jsonwebtoken.Jwts;
|
import io.jsonwebtoken.Jwts;
|
||||||
import org.apache.commons.io.IOUtils;
|
import org.apache.commons.io.IOUtils;
|
||||||
|
import org.kohsuke.github.authorization.AuthorizationProvider;
|
||||||
|
import org.kohsuke.github.extras.authorization.JWTTokenProvider;
|
||||||
|
|
||||||
|
import java.io.File;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
|
import java.security.GeneralSecurityException;
|
||||||
import java.security.KeyFactory;
|
import java.security.KeyFactory;
|
||||||
import java.security.PrivateKey;
|
import java.security.PrivateKey;
|
||||||
import java.security.spec.PKCS8EncodedKeySpec;
|
import java.security.spec.PKCS8EncodedKeySpec;
|
||||||
@@ -21,6 +25,23 @@ public class AbstractGHAppInstallationTest extends AbstractGitHubWireMockTest {
|
|||||||
private static String PRIVATE_KEY_FILE_APP_2 = "/ghapi-test-app-2.private-key.pem";
|
private static String PRIVATE_KEY_FILE_APP_2 = "/ghapi-test-app-2.private-key.pem";
|
||||||
private static String PRIVATE_KEY_FILE_APP_3 = "/ghapi-test-app-3.private-key.pem";
|
private static String PRIVATE_KEY_FILE_APP_3 = "/ghapi-test-app-3.private-key.pem";
|
||||||
|
|
||||||
|
private static AuthorizationProvider JWT_PROVIDER_1;
|
||||||
|
private static AuthorizationProvider JWT_PROVIDER_2;
|
||||||
|
private static AuthorizationProvider JWT_PROVIDER_3;
|
||||||
|
|
||||||
|
AbstractGHAppInstallationTest() {
|
||||||
|
try {
|
||||||
|
JWT_PROVIDER_1 = new JWTTokenProvider(TEST_APP_ID_1,
|
||||||
|
new File(this.getClass().getResource(PRIVATE_KEY_FILE_APP_1).getFile()));
|
||||||
|
JWT_PROVIDER_2 = new JWTTokenProvider(TEST_APP_ID_2,
|
||||||
|
new File(this.getClass().getResource(PRIVATE_KEY_FILE_APP_2).getFile()));
|
||||||
|
JWT_PROVIDER_3 = new JWTTokenProvider(TEST_APP_ID_3,
|
||||||
|
new File(this.getClass().getResource(PRIVATE_KEY_FILE_APP_3).getFile()));
|
||||||
|
} catch (GeneralSecurityException | IOException e) {
|
||||||
|
throw new RuntimeException("These should never fail", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private String createJwtToken(String keyFileResouceName, String appId) {
|
private String createJwtToken(String keyFileResouceName, String appId) {
|
||||||
try {
|
try {
|
||||||
String keyPEM = IOUtils.toString(this.getClass().getResource(keyFileResouceName), "US-ASCII")
|
String keyPEM = IOUtils.toString(this.getClass().getResource(keyFileResouceName), "US-ASCII")
|
||||||
@@ -63,15 +84,15 @@ public class AbstractGHAppInstallationTest extends AbstractGitHubWireMockTest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
protected GHAppInstallation getAppInstallationWithTokenApp1() throws IOException {
|
protected GHAppInstallation getAppInstallationWithTokenApp1() throws IOException {
|
||||||
return getAppInstallationWithToken(createJwtToken(PRIVATE_KEY_FILE_APP_1, TEST_APP_ID_1));
|
return getAppInstallationWithToken(JWT_PROVIDER_1.getEncodedAuthorization());
|
||||||
}
|
}
|
||||||
|
|
||||||
protected GHAppInstallation getAppInstallationWithTokenApp2() throws IOException {
|
protected GHAppInstallation getAppInstallationWithTokenApp2() throws IOException {
|
||||||
return getAppInstallationWithToken(createJwtToken(PRIVATE_KEY_FILE_APP_2, TEST_APP_ID_2));
|
return getAppInstallationWithToken(JWT_PROVIDER_2.getEncodedAuthorization());
|
||||||
}
|
}
|
||||||
|
|
||||||
protected GHAppInstallation getAppInstallationWithTokenApp3() throws IOException {
|
protected GHAppInstallation getAppInstallationWithTokenApp3() throws IOException {
|
||||||
return getAppInstallationWithToken(createJwtToken(PRIVATE_KEY_FILE_APP_3, TEST_APP_ID_3));
|
return getAppInstallationWithToken(JWT_PROVIDER_3.getEncodedAuthorization());
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -100,7 +100,6 @@ public abstract class AbstractGitHubWireMockTest extends Assert {
|
|||||||
// This sets the user and password to a placeholder for wiremock testing
|
// This sets the user and password to a placeholder for wiremock testing
|
||||||
// This makes the tests believe they are running with permissions
|
// This makes the tests believe they are running with permissions
|
||||||
// The recorded stubs will behave like they running with permissions
|
// The recorded stubs will behave like they running with permissions
|
||||||
builder.oauthToken = null;
|
|
||||||
builder.withPassword(STUBBED_USER_LOGIN, STUBBED_USER_PASSWORD);
|
builder.withPassword(STUBBED_USER_LOGIN, STUBBED_USER_PASSWORD);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,11 +1,14 @@
|
|||||||
package org.kohsuke.github;
|
package org.kohsuke.github;
|
||||||
|
|
||||||
import org.junit.Test;
|
import org.junit.Test;
|
||||||
|
import org.kohsuke.github.authorization.UserAuthorizationProvider;
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.lang.reflect.Field;
|
import java.lang.reflect.Field;
|
||||||
import java.util.*;
|
import java.util.*;
|
||||||
|
|
||||||
|
import static org.hamcrest.CoreMatchers.*;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Unit test for {@link GitHub}.
|
* Unit test for {@link GitHub}.
|
||||||
*/
|
*/
|
||||||
@@ -56,19 +59,40 @@ public class GitHubConnectionTest extends AbstractGitHubWireMockTest {
|
|||||||
|
|
||||||
Map<String, String> props = new HashMap<String, String>();
|
Map<String, String> props = new HashMap<String, String>();
|
||||||
|
|
||||||
props.put("login", "bogus");
|
props.put("endpoint", "bogus endpoint url");
|
||||||
props.put("oauth", "bogus");
|
props.put("oauth", "bogus oauth token string");
|
||||||
props.put("password", "bogus");
|
|
||||||
props.put("jwt", "bogus");
|
|
||||||
|
|
||||||
setupEnvironment(props);
|
setupEnvironment(props);
|
||||||
|
|
||||||
GitHubBuilder builder = GitHubBuilder.fromEnvironment();
|
GitHubBuilder builder = GitHubBuilder.fromEnvironment();
|
||||||
|
|
||||||
assertEquals("bogus", builder.user);
|
assertThat(builder.endpoint, equalTo("bogus endpoint url"));
|
||||||
assertEquals("bogus", builder.oauthToken);
|
|
||||||
assertEquals("bogus", builder.password);
|
assertThat(builder.authorizationProvider, instanceOf(UserAuthorizationProvider.class));
|
||||||
assertEquals("bogus", builder.jwtToken);
|
assertThat(builder.authorizationProvider.getEncodedAuthorization(), equalTo("token bogus oauth token string"));
|
||||||
|
assertThat(((UserAuthorizationProvider) builder.authorizationProvider).getLogin(), nullValue());
|
||||||
|
|
||||||
|
props.put("login", "bogus login");
|
||||||
|
setupEnvironment(props);
|
||||||
|
builder = GitHubBuilder.fromEnvironment();
|
||||||
|
|
||||||
|
assertThat(builder.authorizationProvider, instanceOf(UserAuthorizationProvider.class));
|
||||||
|
assertThat(builder.authorizationProvider.getEncodedAuthorization(), equalTo("token bogus oauth token string"));
|
||||||
|
assertThat(((UserAuthorizationProvider) builder.authorizationProvider).getLogin(), equalTo("bogus login"));
|
||||||
|
|
||||||
|
props.put("jwt", "bogus jwt token string");
|
||||||
|
setupEnvironment(props);
|
||||||
|
builder = GitHubBuilder.fromEnvironment();
|
||||||
|
|
||||||
|
assertThat(builder.authorizationProvider, not(instanceOf(UserAuthorizationProvider.class)));
|
||||||
|
assertThat(builder.authorizationProvider.getEncodedAuthorization(), equalTo("Bearer bogus jwt token string"));
|
||||||
|
|
||||||
|
props.put("password", "bogus weak password");
|
||||||
|
setupEnvironment(props);
|
||||||
|
builder = GitHubBuilder.fromEnvironment();
|
||||||
|
|
||||||
|
assertThat(builder.authorizationProvider, instanceOf(UserAuthorizationProvider.class));
|
||||||
|
assertThat(builder.authorizationProvider.getEncodedAuthorization(),
|
||||||
|
equalTo("Basic Ym9ndXMgbG9naW46Ym9ndXMgd2VhayBwYXNzd29yZA=="));
|
||||||
|
assertThat(((UserAuthorizationProvider) builder.authorizationProvider).getLogin(), equalTo("bogus login"));
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -76,32 +100,48 @@ public class GitHubConnectionTest extends AbstractGitHubWireMockTest {
|
|||||||
public void testGitHubBuilderFromCustomEnvironment() throws IOException {
|
public void testGitHubBuilderFromCustomEnvironment() throws IOException {
|
||||||
Map<String, String> props = new HashMap<String, String>();
|
Map<String, String> props = new HashMap<String, String>();
|
||||||
|
|
||||||
props.put("customLogin", "bogusLogin");
|
props.put("customEndpoint", "bogus endpoint url");
|
||||||
props.put("customOauth", "bogusOauth");
|
props.put("customOauth", "bogus oauth token string");
|
||||||
props.put("customPassword", "bogusPassword");
|
|
||||||
props.put("customEndpoint", "bogusEndpoint");
|
|
||||||
|
|
||||||
setupEnvironment(props);
|
setupEnvironment(props);
|
||||||
|
|
||||||
GitHubBuilder builder = GitHubBuilder
|
GitHubBuilder builder = GitHubBuilder
|
||||||
.fromEnvironment("customLogin", "customPassword", "customOauth", "customEndpoint");
|
.fromEnvironment("customLogin", "customPassword", "customOauth", "customEndpoint");
|
||||||
|
|
||||||
assertEquals("bogusLogin", builder.user);
|
assertThat(builder.endpoint, equalTo("bogus endpoint url"));
|
||||||
assertEquals("bogusOauth", builder.oauthToken);
|
|
||||||
assertEquals("bogusPassword", builder.password);
|
assertThat(builder.authorizationProvider, instanceOf(UserAuthorizationProvider.class));
|
||||||
assertEquals("bogusEndpoint", builder.endpoint);
|
assertThat(builder.authorizationProvider.getEncodedAuthorization(), equalTo("token bogus oauth token string"));
|
||||||
|
assertThat(((UserAuthorizationProvider) builder.authorizationProvider).getLogin(), nullValue());
|
||||||
|
|
||||||
|
props.put("customLogin", "bogus login");
|
||||||
|
setupEnvironment(props);
|
||||||
|
builder = GitHubBuilder.fromEnvironment("customLogin", "customPassword", "customOauth", "customEndpoint");
|
||||||
|
|
||||||
|
assertThat(builder.authorizationProvider, instanceOf(UserAuthorizationProvider.class));
|
||||||
|
assertThat(builder.authorizationProvider.getEncodedAuthorization(), equalTo("token bogus oauth token string"));
|
||||||
|
assertThat(((UserAuthorizationProvider) builder.authorizationProvider).getLogin(), equalTo("bogus login"));
|
||||||
|
|
||||||
|
props.put("customPassword", "bogus weak password");
|
||||||
|
setupEnvironment(props);
|
||||||
|
builder = GitHubBuilder.fromEnvironment("customLogin", "customPassword", "customOauth", "customEndpoint");
|
||||||
|
|
||||||
|
assertThat(builder.authorizationProvider, instanceOf(UserAuthorizationProvider.class));
|
||||||
|
assertThat(builder.authorizationProvider.getEncodedAuthorization(),
|
||||||
|
equalTo("Basic Ym9ndXMgbG9naW46Ym9ndXMgd2VhayBwYXNzd29yZA=="));
|
||||||
|
assertThat(((UserAuthorizationProvider) builder.authorizationProvider).getLogin(), equalTo("bogus login"));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void testGithubBuilderWithAppInstallationToken() throws Exception {
|
public void testGithubBuilderWithAppInstallationToken() throws Exception {
|
||||||
GitHubBuilder builder = new GitHubBuilder().withAppInstallationToken("bogus");
|
|
||||||
assertEquals("bogus", builder.oauthToken);
|
GitHubBuilder builder = new GitHubBuilder().withAppInstallationToken("bogus app token");
|
||||||
assertEquals("", builder.user);
|
assertThat(builder.authorizationProvider, instanceOf(UserAuthorizationProvider.class));
|
||||||
|
assertThat(builder.authorizationProvider.getEncodedAuthorization(), equalTo("token bogus app token"));
|
||||||
|
assertThat(((UserAuthorizationProvider) builder.authorizationProvider).getLogin(), equalTo(""));
|
||||||
|
|
||||||
// test authorization header is set as in the RFC6749
|
// test authorization header is set as in the RFC6749
|
||||||
GitHub github = builder.build();
|
GitHub github = builder.build();
|
||||||
// change this to get a request
|
// change this to get a request
|
||||||
assertEquals("token bogus", github.getClient().encodedAuthorization);
|
assertEquals("token bogus app token", github.getClient().getEncodedAuthorization());
|
||||||
assertEquals("", github.getClient().login);
|
assertEquals("", github.getClient().login);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,43 @@
|
|||||||
|
package org.kohsuke.github;
|
||||||
|
|
||||||
|
import org.junit.Test;
|
||||||
|
import org.kohsuke.github.authorization.ImmutableAuthorizationProvider;
|
||||||
|
import org.kohsuke.github.authorization.OrgAppInstallationAuthorizationProvider;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
|
||||||
|
import static org.hamcrest.CoreMatchers.equalTo;
|
||||||
|
import static org.hamcrest.CoreMatchers.notNullValue;
|
||||||
|
|
||||||
|
public class OrgAppInstallationAuthorizationProviderTest extends AbstractGHAppInstallationTest {
|
||||||
|
|
||||||
|
public OrgAppInstallationAuthorizationProviderTest() {
|
||||||
|
useDefaultGitHub = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test(expected = HttpException.class)
|
||||||
|
public void invalidJWTTokenRaisesException() throws IOException {
|
||||||
|
OrgAppInstallationAuthorizationProvider provider = new OrgAppInstallationAuthorizationProvider(
|
||||||
|
"testOrganization",
|
||||||
|
ImmutableAuthorizationProvider.fromJwtToken("myToken"));
|
||||||
|
gitHub = getGitHubBuilder().withAuthorizationProvider(provider)
|
||||||
|
.withEndpoint(mockGitHub.apiServer().baseUrl())
|
||||||
|
.build();
|
||||||
|
|
||||||
|
provider.getEncodedAuthorization();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void validJWTTokenAllowsOauthTokenRequest() throws IOException {
|
||||||
|
OrgAppInstallationAuthorizationProvider provider = new OrgAppInstallationAuthorizationProvider("hub4j-test-org",
|
||||||
|
ImmutableAuthorizationProvider.fromJwtToken("bogus-valid-token"));
|
||||||
|
gitHub = getGitHubBuilder().withAuthorizationProvider(provider)
|
||||||
|
.withEndpoint(mockGitHub.apiServer().baseUrl())
|
||||||
|
.build();
|
||||||
|
String encodedAuthorization = provider.getEncodedAuthorization();
|
||||||
|
|
||||||
|
assertThat(encodedAuthorization, notNullValue());
|
||||||
|
assertThat(encodedAuthorization, equalTo("token v1.9a12d913f980a45a16ac9c3a9d34d9b7sa314cb6"));
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,47 @@
|
|||||||
|
package org.kohsuke.github.extras.authorization;
|
||||||
|
|
||||||
|
import org.junit.Test;
|
||||||
|
import org.kohsuke.github.AbstractGitHubWireMockTest;
|
||||||
|
import org.kohsuke.github.GitHub;
|
||||||
|
|
||||||
|
import java.io.File;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.security.GeneralSecurityException;
|
||||||
|
/*
|
||||||
|
* This test will request an application ensuring that the header for the "Authorization" matches a valid JWT token.
|
||||||
|
* A JWT token in the Authorization header will always start with "ey" which is always the start of the base64
|
||||||
|
* encoding of the JWT Header , so a valid header will look like this:
|
||||||
|
*
|
||||||
|
* <pre>
|
||||||
|
* Authorization: Bearer ey{rest of the header}.{payload}.{signature}
|
||||||
|
* </pre>
|
||||||
|
*
|
||||||
|
* Matched by the regular expression:
|
||||||
|
*
|
||||||
|
* <pre>
|
||||||
|
* ^Bearer (?<JWTHeader>ey\S*)\.(?<JWTPayload>\S*)\.(?<JWTSignature>\S*)$
|
||||||
|
* </pre>
|
||||||
|
*
|
||||||
|
* Which is present in the wiremock matcher. Note that we need to use a matcher because the JWT token is encoded
|
||||||
|
* with a private key and a random nonce, so it will never be the same (under normal conditions). For more
|
||||||
|
* information on the format of a JWT token, see: https://jwt.io/introduction/
|
||||||
|
*/
|
||||||
|
public class JWTTokenProviderTest extends AbstractGitHubWireMockTest {
|
||||||
|
|
||||||
|
private static String TEST_APP_ID_2 = "83009";
|
||||||
|
private static String PRIVATE_KEY_FILE_APP_2 = "/ghapi-test-app-2.private-key.pem";
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testAuthorizationHeaderPattern() throws GeneralSecurityException, IOException {
|
||||||
|
JWTTokenProvider jwtTokenProvider = new JWTTokenProvider(TEST_APP_ID_2,
|
||||||
|
new File(this.getClass().getResource(PRIVATE_KEY_FILE_APP_2).getFile()));
|
||||||
|
GitHub gh = getGitHubBuilder().withEndpoint(mockGitHub.apiServer().baseUrl())
|
||||||
|
.withAuthorizationProvider(jwtTokenProvider)
|
||||||
|
.build();
|
||||||
|
|
||||||
|
// Request the application, the wiremock matcher will ensure that the header
|
||||||
|
// for the authorization is present and has a the format of a valid JWT token
|
||||||
|
gh.getApp();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
{
|
||||||
|
"id": "960b4085-803f-43aa-a291-ccb6fd003adb",
|
||||||
|
"name": "app",
|
||||||
|
"request": {
|
||||||
|
"url": "/app",
|
||||||
|
"method": "GET",
|
||||||
|
"headers": {
|
||||||
|
"Accept": {
|
||||||
|
"equalTo": "application/vnd.github.machine-man-preview+json"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"response": {
|
||||||
|
"status": 401,
|
||||||
|
"body": "{\"message\":\"A JSON web token could not be decoded\",\"documentation_url\":\"https://docs.github.com/rest\"}",
|
||||||
|
"headers": {
|
||||||
|
"Date": "Tue, 29 Sep 2020 12:35:35 GMT",
|
||||||
|
"Content-Type": "application/json; charset=utf-8",
|
||||||
|
"Server": "GitHub.com",
|
||||||
|
"Status": "401 Unauthorized",
|
||||||
|
"X-GitHub-Media-Type": "github.v3; param=machine-man-preview; format=json",
|
||||||
|
"Strict-Transport-Security": "max-age=31536000; includeSubdomains; preload",
|
||||||
|
"X-Frame-Options": "deny",
|
||||||
|
"X-Content-Type-Options": "nosniff",
|
||||||
|
"X-XSS-Protection": "1; mode=block",
|
||||||
|
"Referrer-Policy": "origin-when-cross-origin, strict-origin-when-cross-origin",
|
||||||
|
"Content-Security-Policy": "default-src 'none'",
|
||||||
|
"Vary": "Accept-Encoding, Accept, X-Requested-With",
|
||||||
|
"X-GitHub-Request-Id": "D236:47C4:1909E17E:1DD010FD:5F732A16"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"uuid": "960b4085-803f-43aa-a291-ccb6fd003adb",
|
||||||
|
"persistent": true,
|
||||||
|
"insertionIndex": 2
|
||||||
|
}
|
||||||
@@ -0,0 +1,39 @@
|
|||||||
|
{
|
||||||
|
"id": "31df960e-9966-4b89-8a99-0d6688accca9",
|
||||||
|
"name": "user",
|
||||||
|
"request": {
|
||||||
|
"url": "/user",
|
||||||
|
"method": "GET",
|
||||||
|
"headers": {
|
||||||
|
"Accept": {
|
||||||
|
"equalTo": "text/html, image/gif, image/jpeg, *; q=.2, */*; q=.2"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"response": {
|
||||||
|
"status": 401,
|
||||||
|
"body": "{\"message\":\"Bad credentials\",\"documentation_url\":\"https://docs.github.com/rest\"}",
|
||||||
|
"headers": {
|
||||||
|
"Date": "Tue, 29 Sep 2020 12:35:34 GMT",
|
||||||
|
"Content-Type": "application/json; charset=utf-8",
|
||||||
|
"Server": "GitHub.com",
|
||||||
|
"Status": "401 Unauthorized",
|
||||||
|
"X-GitHub-Media-Type": "unknown, github.v3",
|
||||||
|
"X-RateLimit-Limit": "60",
|
||||||
|
"X-RateLimit-Remaining": "55",
|
||||||
|
"X-RateLimit-Reset": "1601386475",
|
||||||
|
"X-RateLimit-Used": "5",
|
||||||
|
"Strict-Transport-Security": "max-age=31536000; includeSubdomains; preload",
|
||||||
|
"X-Frame-Options": "deny",
|
||||||
|
"X-Content-Type-Options": "nosniff",
|
||||||
|
"X-XSS-Protection": "1; mode=block",
|
||||||
|
"Referrer-Policy": "origin-when-cross-origin, strict-origin-when-cross-origin",
|
||||||
|
"Content-Security-Policy": "default-src 'none'",
|
||||||
|
"Vary": "Accept-Encoding, Accept, X-Requested-With",
|
||||||
|
"X-GitHub-Request-Id": "D236:47C4:1909E038:1DD010AD:5F732A16"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"uuid": "31df960e-9966-4b89-8a99-0d6688accca9",
|
||||||
|
"persistent": true,
|
||||||
|
"insertionIndex": 1
|
||||||
|
}
|
||||||
@@ -0,0 +1,39 @@
|
|||||||
|
{
|
||||||
|
"id": 79253,
|
||||||
|
"slug": "hub4j-test-application",
|
||||||
|
"node_id": "MDM6QXBwNzkyNTM=",
|
||||||
|
"owner": {
|
||||||
|
"login": "hub4j-test-org",
|
||||||
|
"id": 70590530,
|
||||||
|
"node_id": "MDEyOk9yZ2FuaXphdGlvbjcwNTkwNTMw",
|
||||||
|
"avatar_url": "https://avatars1.githubusercontent.com/u/70590530?v=4",
|
||||||
|
"gravatar_id": "",
|
||||||
|
"url": "https://api.github.com/users/hub4j-test-org",
|
||||||
|
"html_url": "https://github.com/hub4j-test-org",
|
||||||
|
"followers_url": "https://api.github.com/users/hub4j-test-org/followers",
|
||||||
|
"following_url": "https://api.github.com/users/hub4j-test-org/following{/other_user}",
|
||||||
|
"gists_url": "https://api.github.com/users/hub4j-test-org/gists{/gist_id}",
|
||||||
|
"starred_url": "https://api.github.com/users/hub4j-test-org/starred{/owner}{/repo}",
|
||||||
|
"subscriptions_url": "https://api.github.com/users/hub4j-test-org/subscriptions",
|
||||||
|
"organizations_url": "https://api.github.com/users/hub4j-test-org/orgs",
|
||||||
|
"repos_url": "https://api.github.com/users/hub4j-test-org/repos",
|
||||||
|
"events_url": "https://api.github.com/users/hub4j-test-org/events{/privacy}",
|
||||||
|
"received_events_url": "https://api.github.com/users/hub4j-test-org/received_events",
|
||||||
|
"type": "Organization",
|
||||||
|
"site_admin": false
|
||||||
|
},
|
||||||
|
"name": "hub4j-test-application",
|
||||||
|
"description": "",
|
||||||
|
"external_url": "https://example.com",
|
||||||
|
"html_url": "https://github.com/apps/hub4j-test-application",
|
||||||
|
"created_at": "2020-09-01T14:56:16Z",
|
||||||
|
"updated_at": "2020-09-01T14:56:16Z",
|
||||||
|
"permissions": {
|
||||||
|
"metadata": "read",
|
||||||
|
"pull_requests": "write"
|
||||||
|
},
|
||||||
|
"events": [
|
||||||
|
"pull_request"
|
||||||
|
],
|
||||||
|
"installations_count": 1
|
||||||
|
}
|
||||||
@@ -0,0 +1,43 @@
|
|||||||
|
{
|
||||||
|
"id": 11575015,
|
||||||
|
"account": {
|
||||||
|
"login": "hub4j-test-org",
|
||||||
|
"id": 70590530,
|
||||||
|
"node_id": "MDEyOk9yZ2FuaXphdGlvbjcwNTkwNTMw",
|
||||||
|
"avatar_url": "https://avatars1.githubusercontent.com/u/70590530?v=4",
|
||||||
|
"gravatar_id": "",
|
||||||
|
"url": "https://api.github.com/users/hub4j-test-org",
|
||||||
|
"html_url": "https://github.com/hub4j-test-org",
|
||||||
|
"followers_url": "https://api.github.com/users/hub4j-test-org/followers",
|
||||||
|
"following_url": "https://api.github.com/users/hub4j-test-org/following{/other_user}",
|
||||||
|
"gists_url": "https://api.github.com/users/hub4j-test-org/gists{/gist_id}",
|
||||||
|
"starred_url": "https://api.github.com/users/hub4j-test-org/starred{/owner}{/repo}",
|
||||||
|
"subscriptions_url": "https://api.github.com/users/hub4j-test-org/subscriptions",
|
||||||
|
"organizations_url": "https://api.github.com/users/hub4j-test-org/orgs",
|
||||||
|
"repos_url": "https://api.github.com/users/hub4j-test-org/repos",
|
||||||
|
"events_url": "https://api.github.com/users/hub4j-test-org/events{/privacy}",
|
||||||
|
"received_events_url": "https://api.github.com/users/hub4j-test-org/received_events",
|
||||||
|
"type": "Organization",
|
||||||
|
"site_admin": false
|
||||||
|
},
|
||||||
|
"repository_selection": "all",
|
||||||
|
"access_tokens_url": "https://api.github.com/app/installations/11575015/access_tokens",
|
||||||
|
"repositories_url": "https://api.github.com/installation/repositories",
|
||||||
|
"html_url": "https://github.com/organizations/hub4j-test-org/settings/installations/11575015",
|
||||||
|
"app_id": 79253,
|
||||||
|
"app_slug": "hub4j-test-application",
|
||||||
|
"target_id": 70590530,
|
||||||
|
"target_type": "Organization",
|
||||||
|
"permissions": {
|
||||||
|
"metadata": "read",
|
||||||
|
"pull_requests": "write"
|
||||||
|
},
|
||||||
|
"events": [
|
||||||
|
"pull_request"
|
||||||
|
],
|
||||||
|
"created_at": "2020-09-01T14:56:49.000Z",
|
||||||
|
"updated_at": "2020-09-01T14:56:49.000Z",
|
||||||
|
"single_file_name": null,
|
||||||
|
"suspended_by": null,
|
||||||
|
"suspended_at": null
|
||||||
|
}
|
||||||
@@ -0,0 +1,41 @@
|
|||||||
|
{
|
||||||
|
"id": "7b483ea8-ace3-4af3-ae23-b081d717fa53",
|
||||||
|
"name": "app",
|
||||||
|
"request": {
|
||||||
|
"url": "/app",
|
||||||
|
"method": "GET",
|
||||||
|
"headers": {
|
||||||
|
"Accept": {
|
||||||
|
"equalTo": "application/vnd.github.machine-man-preview+json"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"response": {
|
||||||
|
"status": 200,
|
||||||
|
"bodyFileName": "app-2.json",
|
||||||
|
"headers": {
|
||||||
|
"Date": "Tue, 29 Sep 2020 12:35:36 GMT",
|
||||||
|
"Content-Type": "application/json; charset=utf-8",
|
||||||
|
"Server": "GitHub.com",
|
||||||
|
"Status": "200 OK",
|
||||||
|
"Cache-Control": "public, max-age=60, s-maxage=60",
|
||||||
|
"Vary": [
|
||||||
|
"Accept",
|
||||||
|
"Accept-Encoding, Accept, X-Requested-With",
|
||||||
|
"Accept-Encoding"
|
||||||
|
],
|
||||||
|
"ETag": "W/\"a4f1cab410e5b80ee9775d1ecb4d3296f067ddcdfa22ba2122dd382c992b55fe\"",
|
||||||
|
"X-GitHub-Media-Type": "github.v3; param=machine-man-preview; format=json",
|
||||||
|
"Strict-Transport-Security": "max-age=31536000; includeSubdomains; preload",
|
||||||
|
"X-Frame-Options": "deny",
|
||||||
|
"X-Content-Type-Options": "nosniff",
|
||||||
|
"X-XSS-Protection": "1; mode=block",
|
||||||
|
"Referrer-Policy": "origin-when-cross-origin, strict-origin-when-cross-origin",
|
||||||
|
"Content-Security-Policy": "default-src 'none'",
|
||||||
|
"X-GitHub-Request-Id": "D11A:F68D:17924B62:1C1232BE:5F732A18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"uuid": "7b483ea8-ace3-4af3-ae23-b081d717fa53",
|
||||||
|
"persistent": true,
|
||||||
|
"insertionIndex": 2
|
||||||
|
}
|
||||||
@@ -0,0 +1,48 @@
|
|||||||
|
{
|
||||||
|
"id": "7e25da60-68c9-41c5-b603-359192783583",
|
||||||
|
"name": "app_installations_11575015_access_tokens",
|
||||||
|
"request": {
|
||||||
|
"url": "/app/installations/11575015/access_tokens",
|
||||||
|
"method": "POST",
|
||||||
|
"headers": {
|
||||||
|
"Accept": {
|
||||||
|
"equalTo": "application/vnd.github.machine-man-preview+json"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"bodyPatterns": [
|
||||||
|
{
|
||||||
|
"equalToJson": "{}",
|
||||||
|
"ignoreArrayOrder": true,
|
||||||
|
"ignoreExtraElements": false
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"response": {
|
||||||
|
"status": 201,
|
||||||
|
"body": "{\"token\":\"v1.9a12d913f980a45a16ac9c3a9d34d9b7sa314cb6\",\"expires_at\":\"2020-09-29T13:35:37Z\",\"permissions\":{\"metadata\":\"read\",\"pull_requests\":\"write\"},\"repository_selection\":\"all\"}",
|
||||||
|
"headers": {
|
||||||
|
"Date": "Tue, 29 Sep 2020 12:35:37 GMT",
|
||||||
|
"Content-Type": "application/json; charset=utf-8",
|
||||||
|
"Server": "GitHub.com",
|
||||||
|
"Status": "201 Created",
|
||||||
|
"Cache-Control": "public, max-age=60, s-maxage=60",
|
||||||
|
"Vary": [
|
||||||
|
"Accept",
|
||||||
|
"Accept-Encoding, Accept, X-Requested-With",
|
||||||
|
"Accept-Encoding"
|
||||||
|
],
|
||||||
|
"ETag": "\"168d81847da026cae71dddc5658dc87c05a2b6945d4e635787c451df823fc72a\"",
|
||||||
|
"X-GitHub-Media-Type": "github.v3; param=machine-man-preview; format=json",
|
||||||
|
"Strict-Transport-Security": "max-age=31536000; includeSubdomains; preload",
|
||||||
|
"X-Frame-Options": "deny",
|
||||||
|
"X-Content-Type-Options": "nosniff",
|
||||||
|
"X-XSS-Protection": "1; mode=block",
|
||||||
|
"Referrer-Policy": "origin-when-cross-origin, strict-origin-when-cross-origin",
|
||||||
|
"Content-Security-Policy": "default-src 'none'",
|
||||||
|
"X-GitHub-Request-Id": "D11A:F68D:17924C69:1C12341C:5F732A18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"uuid": "7e25da60-68c9-41c5-b603-359192783583",
|
||||||
|
"persistent": true,
|
||||||
|
"insertionIndex": 4
|
||||||
|
}
|
||||||
@@ -0,0 +1,41 @@
|
|||||||
|
{
|
||||||
|
"id": "9ffe1e34-1d0e-495a-abdc-86fdf1d15334",
|
||||||
|
"name": "orgs_hub4j-test-org_installation",
|
||||||
|
"request": {
|
||||||
|
"url": "/orgs/hub4j-test-org/installation",
|
||||||
|
"method": "GET",
|
||||||
|
"headers": {
|
||||||
|
"Accept": {
|
||||||
|
"equalTo": "application/vnd.github.machine-man-preview+json"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"response": {
|
||||||
|
"status": 200,
|
||||||
|
"bodyFileName": "orgs_hub4j-test-org_installation-3.json",
|
||||||
|
"headers": {
|
||||||
|
"Date": "Tue, 29 Sep 2020 12:35:36 GMT",
|
||||||
|
"Content-Type": "application/json; charset=utf-8",
|
||||||
|
"Server": "GitHub.com",
|
||||||
|
"Status": "200 OK",
|
||||||
|
"Cache-Control": "public, max-age=60, s-maxage=60",
|
||||||
|
"Vary": [
|
||||||
|
"Accept",
|
||||||
|
"Accept-Encoding, Accept, X-Requested-With",
|
||||||
|
"Accept-Encoding"
|
||||||
|
],
|
||||||
|
"ETag": "W/\"5fa17d9ba74cf1c58441056ab43311b39f39e78976e8524ad3962278c5224955\"",
|
||||||
|
"X-GitHub-Media-Type": "github.v3; param=machine-man-preview; format=json",
|
||||||
|
"Strict-Transport-Security": "max-age=31536000; includeSubdomains; preload",
|
||||||
|
"X-Frame-Options": "deny",
|
||||||
|
"X-Content-Type-Options": "nosniff",
|
||||||
|
"X-XSS-Protection": "1; mode=block",
|
||||||
|
"Referrer-Policy": "origin-when-cross-origin, strict-origin-when-cross-origin",
|
||||||
|
"Content-Security-Policy": "default-src 'none'",
|
||||||
|
"X-GitHub-Request-Id": "D11A:F68D:17924BFB:1C12335A:5F732A18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"uuid": "9ffe1e34-1d0e-495a-abdc-86fdf1d15334",
|
||||||
|
"persistent": true,
|
||||||
|
"insertionIndex": 3
|
||||||
|
}
|
||||||
@@ -0,0 +1,39 @@
|
|||||||
|
{
|
||||||
|
"id": "85ae1237-62c3-4f75-888b-8d751677aa07",
|
||||||
|
"name": "user",
|
||||||
|
"request": {
|
||||||
|
"url": "/user",
|
||||||
|
"method": "GET",
|
||||||
|
"headers": {
|
||||||
|
"Accept": {
|
||||||
|
"equalTo": "text/html, image/gif, image/jpeg, *; q=.2, */*; q=.2"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"response": {
|
||||||
|
"status": 401,
|
||||||
|
"body": "{\"message\":\"Bad credentials\",\"documentation_url\":\"https://docs.github.com/rest\"}",
|
||||||
|
"headers": {
|
||||||
|
"Date": "Tue, 29 Sep 2020 12:35:36 GMT",
|
||||||
|
"Content-Type": "application/json; charset=utf-8",
|
||||||
|
"Server": "GitHub.com",
|
||||||
|
"Status": "401 Unauthorized",
|
||||||
|
"X-GitHub-Media-Type": "unknown, github.v3",
|
||||||
|
"X-RateLimit-Limit": "60",
|
||||||
|
"X-RateLimit-Remaining": "53",
|
||||||
|
"X-RateLimit-Reset": "1601386475",
|
||||||
|
"X-RateLimit-Used": "7",
|
||||||
|
"Strict-Transport-Security": "max-age=31536000; includeSubdomains; preload",
|
||||||
|
"X-Frame-Options": "deny",
|
||||||
|
"X-Content-Type-Options": "nosniff",
|
||||||
|
"X-XSS-Protection": "1; mode=block",
|
||||||
|
"Referrer-Policy": "origin-when-cross-origin, strict-origin-when-cross-origin",
|
||||||
|
"Content-Security-Policy": "default-src 'none'",
|
||||||
|
"Vary": "Accept-Encoding, Accept, X-Requested-With",
|
||||||
|
"X-GitHub-Request-Id": "D11A:F68D:17924B00:1C12327C:5F732A17"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"uuid": "85ae1237-62c3-4f75-888b-8d751677aa07",
|
||||||
|
"persistent": true,
|
||||||
|
"insertionIndex": 1
|
||||||
|
}
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
{
|
||||||
|
"id": 83009,
|
||||||
|
"slug": "ghapi-test-app-2",
|
||||||
|
"node_id": "MDM6QXBwODMwMDk=",
|
||||||
|
"owner": {
|
||||||
|
"login": "hub4j-test-org",
|
||||||
|
"id": 7544739,
|
||||||
|
"node_id": "MDEyOk9yZ2FuaXphdGlvbjc1NDQ3Mzk=",
|
||||||
|
"avatar_url": "https://avatars3.githubusercontent.com/u/7544739?v=4",
|
||||||
|
"gravatar_id": "",
|
||||||
|
"url": "https://api.github.com/users/hub4j-test-org",
|
||||||
|
"html_url": "https://github.com/hub4j-test-org",
|
||||||
|
"followers_url": "https://api.github.com/users/hub4j-test-org/followers",
|
||||||
|
"following_url": "https://api.github.com/users/hub4j-test-org/following{/other_user}",
|
||||||
|
"gists_url": "https://api.github.com/users/hub4j-test-org/gists{/gist_id}",
|
||||||
|
"starred_url": "https://api.github.com/users/hub4j-test-org/starred{/owner}{/repo}",
|
||||||
|
"subscriptions_url": "https://api.github.com/users/hub4j-test-org/subscriptions",
|
||||||
|
"organizations_url": "https://api.github.com/users/hub4j-test-org/orgs",
|
||||||
|
"repos_url": "https://api.github.com/users/hub4j-test-org/repos",
|
||||||
|
"events_url": "https://api.github.com/users/hub4j-test-org/events{/privacy}",
|
||||||
|
"received_events_url": "https://api.github.com/users/hub4j-test-org/received_events",
|
||||||
|
"type": "Organization",
|
||||||
|
"site_admin": false
|
||||||
|
},
|
||||||
|
"name": "GHApi Test app 2",
|
||||||
|
"description": "",
|
||||||
|
"external_url": "https://localhost",
|
||||||
|
"html_url": "https://github.com/apps/ghapi-test-app-2",
|
||||||
|
"created_at": "2020-09-30T15:02:20Z",
|
||||||
|
"updated_at": "2020-09-30T15:02:20Z",
|
||||||
|
"permissions": {},
|
||||||
|
"events": [],
|
||||||
|
"installations_count": 1
|
||||||
|
}
|
||||||
@@ -0,0 +1,44 @@
|
|||||||
|
{
|
||||||
|
"id": "bb7cf5bb-45b3-fba2-afd8-939b2c24787a",
|
||||||
|
"name": "app",
|
||||||
|
"request": {
|
||||||
|
"url": "/app",
|
||||||
|
"method": "GET",
|
||||||
|
"headers": {
|
||||||
|
"Authorization": {
|
||||||
|
"matches": "^Bearer (?<JWTHeader>ey\\S*)\\.(?<JWTPayload>\\S*)\\.(?<JWTSignature>\\S*)$"
|
||||||
|
},
|
||||||
|
"Accept": {
|
||||||
|
"equalTo": "application/vnd.github.machine-man-preview+json"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"response": {
|
||||||
|
"status": 200,
|
||||||
|
"bodyFileName": "app-1.json",
|
||||||
|
"headers": {
|
||||||
|
"Date": "Thu, 05 Nov 2020 20:42:31 GMT",
|
||||||
|
"Content-Type": "application/json; charset=utf-8",
|
||||||
|
"Server": "GitHub.com",
|
||||||
|
"Status": "200 OK",
|
||||||
|
"Cache-Control": "public, max-age=60, s-maxage=60",
|
||||||
|
"Vary": [
|
||||||
|
"Accept",
|
||||||
|
"Accept-Encoding, Accept, X-Requested-With",
|
||||||
|
"Accept-Encoding"
|
||||||
|
],
|
||||||
|
"ETag": "W/\"b3d319dbb4dba93fbda071208d874e5ab566d827e1ad1d7dc59f26d68694dc48\"",
|
||||||
|
"X-GitHub-Media-Type": "github.v3; param=machine-man-preview; format=json",
|
||||||
|
"Strict-Transport-Security": "max-age=31536000; includeSubdomains; preload",
|
||||||
|
"X-Frame-Options": "deny",
|
||||||
|
"X-Content-Type-Options": "nosniff",
|
||||||
|
"X-XSS-Protection": "1; mode=block",
|
||||||
|
"Referrer-Policy": "origin-when-cross-origin, strict-origin-when-cross-origin",
|
||||||
|
"Content-Security-Policy": "default-src 'none'",
|
||||||
|
"X-GitHub-Request-Id": "9294:AE05:BDAC761:DB35838:5FA463B6"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"uuid": "bb7cf5bb-45b3-fba2-afd8-939b2c24787a",
|
||||||
|
"persistent": true,
|
||||||
|
"insertionIndex": 1
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user