Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

1196: Add extension points on RestRequest to support more REST authorization methods #1227

Closed
wants to merge 2 commits into from
Closed
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
@@ -41,7 +41,7 @@ private static URI backportRequest(URI uri) {

JbsBackport(URI uri, JbsVault vault) {
if (vault != null) {
backportRequest = new RestRequest(backportRequest(uri), vault.authId(), () -> Arrays.asList("Cookie", vault.getCookie()));
backportRequest = new RestRequest(backportRequest(uri), vault.authId(), (r) -> Arrays.asList("Cookie", vault.getCookie()));
} else {
backportRequest = null;
}
@@ -51,15 +51,15 @@ private String checksum(String body) {

JbsVault(URI vaultUri, String vaultToken, URI jiraUri) {
authId = checksum(vaultToken);
request = new RestRequest(vaultUri, authId, () -> Arrays.asList(
request = new RestRequest(vaultUri, authId, (r) -> Arrays.asList(
"X-Vault-Token", vaultToken
));
this.authProbe = URIBuilder.base(jiraUri).setPath("/rest/api/2/myself").build();
}

String getCookie() {
if (cookie != null) {
var authProbeRequest = new RestRequest(authProbe, authId, () -> Arrays.asList("Cookie", cookie));
var authProbeRequest = new RestRequest(authProbe, authId, (r) -> Arrays.asList("Cookie", cookie));
var res = authProbeRequest.get()
.onError(error -> error.statusCode() >= 400 ? Optional.of(JSON.of("AUTH_ERROR")) : Optional.empty())
.execute();
@@ -65,7 +65,7 @@ public GitHubHost(URI uri, GitHubApplication application, Pattern webUriPattern,
.setPath("/")
.build();

request = new RestRequest(baseApi, application.authId(), () -> Arrays.asList(
request = new RestRequest(baseApi, application.authId(), (r) -> Arrays.asList(
"Authorization", "token " + getInstallationToken().orElseThrow(),
"Accept", "application/vnd.github.machine-man-preview+json",
"Accept", "application/vnd.github.antiope-preview+json",
@@ -76,7 +76,7 @@ public GitHubHost(URI uri, GitHubApplication application, Pattern webUriPattern,
.appendSubDomain("api")
.setPath("/graphql")
.build();
graphQL = new RestRequest(graphQLAPI, application.authId(), () -> Arrays.asList(
graphQL = new RestRequest(graphQLAPI, application.authId(), (r) -> Arrays.asList(
"Authorization", "bearer " + getInstallationToken().orElseThrow(),
"Accept", "application/vnd.github.machine-man-preview+json",
"Accept", "application/vnd.github.antiope-preview+json",
@@ -106,7 +106,7 @@ public GitHubHost(URI uri, Credential pat, Pattern webUriPattern, String webUriR
.setPath("/")
.build();

request = new RestRequest(baseApi, pat.username(), () -> Arrays.asList(
request = new RestRequest(baseApi, pat.username(), (r) -> Arrays.asList(
"Authorization", "token " + getInstallationToken().orElseThrow(),
"Accept", "application/vnd.github.machine-man-preview+json",
"Accept", "application/vnd.github.antiope-preview+json",
@@ -117,7 +117,7 @@ public GitHubHost(URI uri, Credential pat, Pattern webUriPattern, String webUriR
.appendSubDomain("api")
.setPath("/graphql")
.build();
graphQL = new RestRequest(graphQLAPI, pat.username(), () -> Arrays.asList(
graphQL = new RestRequest(graphQLAPI, pat.username(), (r) -> Arrays.asList(
"Authorization", "bearer " + getInstallationToken().orElseThrow(),
"Accept", "application/vnd.github.machine-man-preview+json",
"Accept", "application/vnd.github.antiope-preview+json",
@@ -60,7 +60,7 @@ public class GitHubRepository implements HostedRepository {
.appendSubDomain("api")
.setPath("/repos/" + repository + "/")
.build();
request = new RestRequest(apiBase, gitHubHost.authId().orElse(null), () -> {
request = new RestRequest(apiBase, gitHubHost.authId().orElse(null), (r) -> {
var headers = new ArrayList<>(List.of(
"Accept", "application/vnd.github.machine-man-preview+json",
"Accept", "application/vnd.github.antiope-preview+json",
@@ -56,7 +56,7 @@ public GitLabHost(String name, URI uri, boolean useSsh, Credential pat, Set<Stri
var baseApi = URIBuilder.base(uri)
.setPath("/api/v4/")
.build();
request = new RestRequest(baseApi, pat.username(), () -> Arrays.asList("Private-Token", pat.password()));
request = new RestRequest(baseApi, pat.username(), (r) -> Arrays.asList("Private-Token", pat.password()));
}

GitLabHost(String name, URI uri, boolean useSsh, Set<String> groups) {
@@ -70,7 +70,7 @@ public GitLabRepository(GitLabHost gitLabHost, int id) {
.build();

request = gitLabHost.getPat()
.map(pat -> new RestRequest(baseApi, pat.username(), () -> Arrays.asList("Private-Token", pat.password())))
.map(pat -> new RestRequest(baseApi, pat.username(), (r) -> Arrays.asList("Private-Token", pat.password())))
.orElseGet(() -> new RestRequest(baseApi));

var urlPattern = URIBuilder.base(gitLabHost.getUri())
@@ -56,7 +56,7 @@ public class JiraHost implements IssueTracker {
var baseApi = URIBuilder.base(uri)
.setPath("/rest/api/2/")
.build();
request = new RestRequest(baseApi, jiraVault.authId(), () -> Arrays.asList("Cookie", jiraVault.getCookie()));
request = new RestRequest(baseApi, jiraVault.authId(), (r) -> Arrays.asList("Cookie", jiraVault.getCookie()));
}

JiraHost(URI uri, JiraVault jiraVault, String visibilityRole, String securityLevel) {
@@ -66,7 +66,7 @@ public class JiraHost implements IssueTracker {
var baseApi = URIBuilder.base(uri)
.setPath("/rest/api/2/")
.build();
request = new RestRequest(baseApi, jiraVault.authId(), () -> Arrays.asList("Cookie", jiraVault.getCookie()));
request = new RestRequest(baseApi, jiraVault.authId(), (r) -> Arrays.asList("Cookie", jiraVault.getCookie()));
}

URI getUri() {
@@ -51,15 +51,15 @@ private String checksum(String body) {

JiraVault(URI vaultUri, String vaultToken, URI jiraUri) {
authId = checksum(vaultToken);
request = new RestRequest(vaultUri, authId, () -> Arrays.asList(
request = new RestRequest(vaultUri, authId, (r) -> Arrays.asList(
"X-Vault-Token", vaultToken
));
this.authProbe = URIBuilder.base(jiraUri).setPath("/rest/api/2/myself").build();
}

String getCookie() {
if (cookie != null) {
var authProbeRequest = new RestRequest(authProbe, authId, () -> Arrays.asList("Cookie", cookie));
var authProbeRequest = new RestRequest(authProbe, authId, (r) -> Arrays.asList("Cookie", cookie));
var res = authProbeRequest.get()
.onError(error -> error.statusCode() >= 400 ? Optional.of(JSON.of("AUTH_ERROR")) : Optional.empty())
.execute();
@@ -28,6 +28,9 @@
import java.io.*;
import java.net.URI;
import java.net.http.*;
import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.time.*;
import java.util.*;
import java.util.logging.*;
@@ -55,7 +58,12 @@ private enum RequestType {

@FunctionalInterface
public interface AuthenticationGenerator {
List<String> getAuthHeaders();
List<String> getAuthHeaders(HttpRequest.Builder request);
}

@FunctionalInterface
public interface NextLinkExtractor {
Optional<HttpRequest.Builder> getNextLinkRequest(HttpResponse<String> response);
}

@FunctionalInterface
@@ -79,6 +87,7 @@ private class Param {
private String rawBody;
private int maxPages;
private ErrorTransform onError;
private String sha256Header;

private QueryBuilder(RequestType queryType, String endpoint) {
this.queryType = queryType;
@@ -184,6 +193,15 @@ public QueryBuilder header(String name, String value) {
return this;
}

/**
* Optionally name a header where a sha256 hash of the contents will be added.
* This is commonly used by cloud vendors as part of verifying requests.
*/
public QueryBuilder sha256Header(String name) {
sha256Header = name;
return this;
}

public JSONValue execute() {
try {
return RestRequest.this.execute(this);
@@ -207,18 +225,26 @@ public String toString() {
private final URI apiBase;
private final String authId;
private final AuthenticationGenerator authGen;
private final NextLinkExtractor nextLinkExtractor;
private final Logger log = Logger.getLogger("org.openjdk.skara.host.network");

public RestRequest(URI apiBase, String authId, AuthenticationGenerator authGen,
NextLinkExtractor nextLinkExtractor) {
this.apiBase = apiBase;
this.authId = authId;
this.authGen = authGen;
this.nextLinkExtractor = nextLinkExtractor;
}

public RestRequest(URI apiBase, String authId, AuthenticationGenerator authGen) {
this.apiBase = apiBase;
this.authId = authId;
this.authGen = authGen;
this.nextLinkExtractor = this::getNextLinkRequest;
}

public RestRequest(URI apiBase) {
this.apiBase = apiBase;
this.authId = null;
this.authGen = null;
this(apiBase, null, null);
}

/**
@@ -280,7 +306,7 @@ private HttpResponse<String> sendRequest(HttpRequest.Builder request) throws IOE
while (true) {
try {
if (authGen != null) {
request.headers(authGen.getAuthHeaders().toArray(new String[0]));
request.headers(authGen.getAuthHeaders(request).toArray(new String[0]));
}
response = cache.send(authId, request);
break;
@@ -330,7 +356,8 @@ private Optional<JSONValue> transformBadResponse(HttpResponse<String> response,
}

private HttpRequest.Builder createRequest(RequestType requestType, String endpoint, String body,
List<QueryBuilder.Param> params, Map<String, String> headers, boolean isJSON) {
List<QueryBuilder.Param> params, Map<String, String> headers,
boolean isJSON, String sha256Header) {
var uriBuilder = URIBuilder.base(apiBase);
if (endpoint != null && !endpoint.isEmpty()) {
uriBuilder = uriBuilder.appendPath(endpoint);
@@ -350,6 +377,16 @@ private HttpRequest.Builder createRequest(RequestType requestType, String endpoi

if (body != null) {
requestBuilder.method(requestType.name(), HttpRequest.BodyPublishers.ofString(body));
if (sha256Header != null) {
try {
var digest = MessageDigest.getInstance("SHA-256");
var hash = digest.digest(body.getBytes(StandardCharsets.UTF_8));
var encoded = new String(Base64.getEncoder().encode(hash), StandardCharsets.UTF_8);
requestBuilder.header(sha256Header, encoded);
} catch (NoSuchAlgorithmException e) {
throw new Error("SHA-256 algorithm not found");
}
}
}
headers.forEach(requestBuilder::header);
return requestBuilder;
@@ -395,9 +432,22 @@ private JSONValue combinePages(List<JSONValue> pages) {
}
}

private Optional<HttpRequest.Builder> getNextLinkRequest(HttpResponse<String> response) {
var link = response.headers().firstValue("Link");
if (link.isEmpty()) {
return Optional.empty();
}
var links = parseLink(link.get());
if (!links.containsKey("next")) {
return Optional.empty();
}
var uri = URI.create(links.get("next"));
return Optional.of(getHttpRequestBuilder(uri).GET());
}

private JSONValue execute(QueryBuilder queryBuilder) throws IOException {
var request = createRequest(queryBuilder.queryType, queryBuilder.endpoint, queryBuilder.composedBody(),
queryBuilder.params, queryBuilder.headers, queryBuilder.isJSON());
queryBuilder.params, queryBuilder.headers, queryBuilder.isJSON(), queryBuilder.sha256Header);
requestCounter.labels(queryBuilder.queryType.toString()).inc();
var response = sendRequest(request);
var errorTransform = transformBadResponse(response, queryBuilder);
@@ -406,8 +456,8 @@ private JSONValue execute(QueryBuilder queryBuilder) throws IOException {
return errorTransform.get();
}

var link = response.headers().firstValue("Link");
if (link.isEmpty() || queryBuilder.maxPages < 2) {
var nextRequest = nextLinkExtractor.getNextLinkRequest(response);
if (nextRequest.isEmpty() || queryBuilder.maxPages < 2) {
return parseResponse(response);
}

@@ -416,12 +466,9 @@ private JSONValue execute(QueryBuilder queryBuilder) throws IOException {
var parsedResponse = parseResponse(response);
ret.add(parsedResponse);

var links = parseLink(link.get());
while (links.containsKey("next") && ret.size() < queryBuilder.maxPages) {
var uri = URI.create(links.get("next"));
request = getHttpRequestBuilder(uri).GET();
while (nextRequest.isPresent() && ret.size() < queryBuilder.maxPages) {
requestCounter.labels(queryBuilder.queryType.toString()).inc();
response = sendRequest(request);
response = sendRequest(nextRequest.get());

// If an error occurs during paginated parsing, we have to discard all previous data
errorTransform = transformBadResponse(response, queryBuilder);
@@ -430,9 +477,7 @@ private JSONValue execute(QueryBuilder queryBuilder) throws IOException {
return errorTransform.get();
}

link = response.headers().firstValue("Link");
links = parseLink(link.orElseThrow(
() -> new UncheckedRestException("Initial paginated response no longer paginated for query: " + queryBuilder)));
nextRequest = nextLinkExtractor.getNextLinkRequest(response);

parsedResponse = parseResponse(response);
ret.add(parsedResponse);
@@ -442,7 +487,7 @@ private JSONValue execute(QueryBuilder queryBuilder) throws IOException {

private String executeUnparsed(QueryBuilder queryBuilder) throws IOException {
var request = createRequest(queryBuilder.queryType, queryBuilder.endpoint, queryBuilder.composedBody(),
queryBuilder.params, queryBuilder.headers, queryBuilder.isJSON());
queryBuilder.params, queryBuilder.headers, queryBuilder.isJSON(), queryBuilder.sha256Header);
requestCounter.labels(queryBuilder.queryType.toString()).inc();
var response = sendRequest(request);
responseCounter.labels(Integer.toString(response.statusCode()), Boolean.toString(false)).inc();
@@ -137,6 +137,11 @@ public URIBuilder setAuthentication(String authentication) {
return this;
}

public URIBuilder setQuery(String query) {
current.query = query;
return this;
}

public URIBuilder setQuery(Map<String, String> parameters) {
var query = parameters.entrySet().stream()
.map(p -> {
@@ -304,8 +304,8 @@ void cacheFlushPartial() throws IOException {
void cachedSeparateAuth() throws IOException {
try (var receiver = new RestReceiver()) {
var plainRequest = new RestRequest(receiver.getEndpoint());
var authRequest1 = new RestRequest(receiver.getEndpoint(), "id1", () -> List.of("user", "1"));
var authRequest2 = new RestRequest(receiver.getEndpoint(), "id2", () -> List.of("user", "2"));
var authRequest1 = new RestRequest(receiver.getEndpoint(), "id1", (r) -> List.of("user", "1"));
var authRequest2 = new RestRequest(receiver.getEndpoint(), "id2", (r) -> List.of("user", "2"));

plainRequest.get("/test").execute();
assertFalse(receiver.usedCached());