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

add gitlab and github token Auth support #14 #18

Merged
merged 23 commits into from
Feb 12, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
39631fb
use gitlab api request as preperation to support auth for private rep…
Feb 5, 2021
34d9a43
support self hosted gitlab hosts
Feb 5, 2021
1b65580
fix empty repoSubfolder
Feb 5, 2021
178bced
add github and gitlab token based auth support #14
Feb 5, 2021
6df64aa
First step of refactoring GitHub into a fluent interface.
nedtwigg Feb 5, 2021
4d1312d
GitHub can now handle auth with fluent interface.
nedtwigg Feb 5, 2021
f9e526f
GitLab can now handle auth with fluent interface, but not yet custom …
nedtwigg Feb 5, 2021
fa82f52
GitLab can now handle custom domains and protocols.
nedtwigg Feb 5, 2021
ea28cb4
Fix security vuln where GitHub and GitLab could have sent auth over H…
nedtwigg Feb 5, 2021
9287589
Fixup GitLab test.
nedtwigg Feb 5, 2021
9e3967c
Minor fixup.
nedtwigg Feb 5, 2021
f8f1c11
Refactor gitlab and auth around fluent configurator pattern.
nedtwigg Feb 5, 2021
9bcddff
extract authToken Support to interface
Feb 7, 2021
98a19e2
Revert "extract authToken Support to interface"
nedtwigg Feb 8, 2021
b2c7f35
Rework the "assertPluginNotSet()" state management for the fluent con…
nedtwigg Feb 8, 2021
f3edc22
AuthPlugin ought to return void, because `Request.Builder` is mutable…
nedtwigg Feb 8, 2021
d4717db
Update changelog.
nedtwigg Feb 12, 2021
a85a5e9
Remove the ben-manes.versions plugin.
nedtwigg Feb 12, 2021
938daaf
Re-add the tests that @vgropp added which I accidentally reverted.
nedtwigg Feb 12, 2021
12b2bba
Add gradle wrapper validation.
nedtwigg Feb 12, 2021
0cee4e8
Replace vgropp test repo with diffplug test repo.
nedtwigg Feb 12, 2021
a2769d2
Reduce the number of repositories to maintain.
nedtwigg Feb 12, 2021
e50316f
Fix test on windows.
nedtwigg Feb 12, 2021
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions .github/workflows/gradle-wrapper-validation.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
name: "Validate Gradle Wrapper"
on: [push, pull_request]

jobs:
validation:
name: "Validation"
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: gradle/[email protected]
5 changes: 4 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,12 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [Unreleased]
- Support for gitlab public repo #13
### Added
- Support for GitLab, self-hosted and `gitlab.com` ([#18](https://github.com/diffplug/blowdryer/pull/18)).
- Support for private GitHub and GitLab script repositories via auth tokens ([#18](https://github.com/diffplug/blowdryer/pull/18)).

## [1.0.0] - 2020-01-09
Same as `0.2.0`, just committing to API back-compat from here.

## [0.2.0] - 2020-01-09
### Added
Expand Down
1 change: 0 additions & 1 deletion build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ plugins {
id 'com.diffplug.blowdryer'
id 'com.diffplug.gradle.spotless'
id 'com.diffplug.spotless-changelog'
id 'com.github.ben-manes.versions'
id 'com.gradle.plugin-publish'
id 'com.jfrog.bintray'
}
Expand Down
25 changes: 22 additions & 3 deletions src/main/java/com/diffplug/blowdryer/Blowdryer.java
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.MalformedURLException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Path;
import java.util.Base64;
Expand Down Expand Up @@ -132,8 +133,9 @@ private static Map<String, String> loadPropertyFile(File file) throws IOExceptio

private static void download(String url, File dst) throws IOException {
OkHttpClient client = new OkHttpClient.Builder().build();
Request req = new Request.Builder().url(url).build();
try (Response response = client.newCall(req).execute()) {
Request.Builder req = new Request.Builder().url(url);
authPlugin.addAuthToken(url, req);
try (Response response = client.newCall(req.build()).execute()) {
if (!response.isSuccessful()) {
throw new IllegalArgumentException(url + "\nreceived http code " + response.code() + "\n" + response.body().string());
}
Expand Down Expand Up @@ -185,16 +187,26 @@ static void assertPluginNotSet(String errorMessage) {
}
}

static void assertPluginNotSet() {
assertPluginNotSet("You already initialized the `blowdryer` plugin, you can't do this twice.");
}

static void setResourcePluginNull() {
synchronized (Blowdryer.class) {
Blowdryer.plugin = null;
Blowdryer.authPlugin = authPluginNone;
}
}

static void setResourcePlugin(ResourcePlugin plugin) {
setResourcePlugin(plugin, null);
}

static void setResourcePlugin(ResourcePlugin plugin, AuthPlugin authPlugin) {
synchronized (Blowdryer.class) {
assertPluginNotSet("You already initialized the `blowdryer` plugin, you can't do this twice.");
assertPluginNotSet();
Blowdryer.plugin = plugin;
Blowdryer.authPlugin = authPlugin == null ? authPluginNone : authPlugin;
}
}

Expand All @@ -204,6 +216,13 @@ private static void assertInitialized() {
}
}

static interface AuthPlugin {
void addAuthToken(String url, Request.Builder builder) throws MalformedURLException;
}

private static final AuthPlugin authPluginNone = (url, builder) -> {};
private static AuthPlugin authPlugin = authPluginNone;

/** Returns the given resource as a File (as configured by {@link BlowdryerSetup}. */
public static File file(String resourcePath) {
synchronized (Blowdryer.class) {
Expand Down
110 changes: 100 additions & 10 deletions src/main/java/com/diffplug/blowdryer/BlowdryerSetup.java
Original file line number Diff line number Diff line change
Expand Up @@ -19,13 +19,20 @@
import com.diffplug.common.base.Errors;
import groovy.lang.Closure;
import java.io.File;
import java.io.UnsupportedEncodingException;
import java.net.URLEncoder;
import java.util.Objects;
import java.util.function.Function;
import javax.annotation.Nullable;
import org.jetbrains.annotations.NotNull;

/** Configures where {@link Blowdryer#file(String)} downloads files from. */
public class BlowdryerSetup {
static final String NAME = "blowdryerSetup";

private static final String GITHUB_HOST = "raw.githubusercontent.com";
private static final String GITLAB_HOST = "gitlab.com";

private final File referenceDirectory;

/** Pass in the directory that will be used to resolve string arguments to devLocal. */
Expand Down Expand Up @@ -54,19 +61,94 @@ public enum GitAnchorType {
}

/** Sets the source where we will grab these scripts. */
public void github(String repoOrg, GitAnchorType anchorType, String anchor) {
assertNoLeadingOrTrailingSlash(repoOrg);
assertNoLeadingOrTrailingSlash(anchor);
String root = "https://raw.githubusercontent.com/" + repoOrg + "/" + anchor + "/" + repoSubfolder + "/";
Blowdryer.setResourcePlugin(resource -> root + resource);
public GitHub github(String repoOrg, GitAnchorType anchorType, String anchor) {
// anchorType isn't used right now, but makes it easier to read what "anchor" is
return new GitHub(repoOrg, anchor);
}

public class GitHub {
private String repoOrg;
private String anchor;
private @Nullable String authToken;

private GitHub(String repoOrg, String anchor) {
Blowdryer.assertPluginNotSet();
this.repoOrg = assertNoLeadingOrTrailingSlash(repoOrg);
this.anchor = assertNoLeadingOrTrailingSlash(anchor);
setGlobals();
}

public GitHub authToken(String authToken) {
this.authToken = authToken;
return setGlobals();
}

private GitHub setGlobals() {
Blowdryer.setResourcePluginNull();
String root = "https://" + GITHUB_HOST + "/" + repoOrg + "/" + anchor + "/";
Blowdryer.setResourcePlugin(resource -> root + getFullResourcePath(resource), authToken == null ? null : (url, builder) -> {
if (url.startsWith(root)) {
builder.addHeader("Authorization", "Bearer " + authToken);
}
});
return this;
}
}

/** Sets the source where we will grab these scripts. */
public void gitlab(String repoOrg, GitAnchorType anchorType, String anchor) {
assertNoLeadingOrTrailingSlash(repoOrg);
assertNoLeadingOrTrailingSlash(anchor);
String root = "https://gitlab.com/" + repoOrg + "/-/raw/" + anchor + "/" + repoSubfolder + "/";
Blowdryer.setResourcePlugin(resource -> root + resource);
public GitLab gitlab(String repoOrg, GitAnchorType anchorType, String anchor) {
// anchorType isn't used right now, but makes it easier to read what "anchor" is
return new GitLab(repoOrg, anchor);
}

public class GitLab {
private String repoOrg;
private String anchor;
private @Nullable String authToken;
private String protocol, host;

private GitLab(String repoOrg, String anchor) {
Blowdryer.assertPluginNotSet();
this.repoOrg = assertNoLeadingOrTrailingSlash(repoOrg);
this.anchor = assertNoLeadingOrTrailingSlash(anchor);
customDomainHttps(GITLAB_HOST);
}

public GitLab authToken(String authToken) {
this.authToken = authToken;
return setGlobals();
}

public GitLab customDomainHttp(String domain) {
return customProtocolAndDomain("http://", domain);
}

public GitLab customDomainHttps(String domain) {
return customProtocolAndDomain("https://", domain);
}

private GitLab customProtocolAndDomain(String protocol, String domain) {
this.protocol = protocol;
this.host = domain;
return setGlobals();
}

private GitLab setGlobals() {
Blowdryer.setResourcePluginNull();
String urlStart = protocol + host + "/api/v4/projects/" + encodeUrlPart(repoOrg) + "/repository/files/";
String urlEnd = "/raw?ref=" + encodeUrlPart(anchor);
Blowdryer.setResourcePlugin(resource -> urlStart + encodeUrlPart(getFullResourcePath(resource)) + urlEnd, authToken == null ? null : (url, builder) -> {
if (url.startsWith(urlStart)) {
builder.addHeader("Authorization", "Bearer " + authToken);
}
});
return this;
}
}

@NotNull
private String getFullResourcePath(String resource) {
return (repoSubfolder.isEmpty() ? "" : repoSubfolder + "/") + resource;
}

/** Sets the mapping from `file(String)` to `immutableUrl(String)`. */
Expand Down Expand Up @@ -108,4 +190,12 @@ private static String assertNoLeadingOrTrailingSlash(String input) {
}
return input;
}

private static String encodeUrlPart(String part) {
try {
return URLEncoder.encode(part, "UTF-8");
} catch (UnsupportedEncodingException e) {
throw new IllegalArgumentException("error encoding part", e);
}
}
}
73 changes: 73 additions & 0 deletions src/test/java/com/diffplug/blowdryer/BlowdryerPluginAuthTest.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
/*
* Copyright 2020 DiffPlug
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.diffplug.blowdryer;


import java.io.File;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.util.Arrays;
import java.util.stream.Collectors;
import org.junit.Ignore;
import org.junit.Test;

@Ignore("has to be filled with prvate tokens and repos")
public class BlowdryerPluginAuthTest extends GradleHarness {

private void settingsGitlabAuth(String tag, String... extra) throws IOException {
write("settings.gradle",
"plugins { id 'com.diffplug.blowdryerSetup' }",
"blowdryerSetup { repoSubfolder(''); "
+ "gitlab('private/repo', 'tag', '" + tag + "').authToken('foobar');"
+ " }",
Arrays.stream(extra).collect(Collectors.joining("\n")));
}

private void settingsGithubAuth(String tag, String... extra) throws IOException {
write("settings.gradle",
"plugins { id 'com.diffplug.blowdryerSetup' }",
"blowdryerSetup { github('private/repo', 'tag', '" + tag + "').authToken('foobar');"
+ " }",
Arrays.stream(extra).collect(Collectors.joining("\n")));
}

@Test
public void githubAuthTag() throws IOException {
settingsGithubAuth("master");
write("build.gradle",
"apply plugin: 'com.diffplug.blowdryer'",
"assert 干.file('sample').text == 'a'");
gradleRunner().build();
}

@Test
public void gitlabAuthTag() throws IOException {
settingsGitlabAuth("init-test-for-auth");
write("build.gradle",
"apply plugin: 'com.diffplug.blowdryer'",
"assert 干.file('sample').text == 'a'");
gradleRunner().build();
}

/** Writes the given content to the given path. */
protected File write(String path, String... lines) throws IOException {
File file = file(path);
file.getParentFile().mkdirs();
Files.write(file.toPath(), Arrays.asList(lines), StandardCharsets.UTF_8);
return file;
}
}
71 changes: 69 additions & 2 deletions src/test/java/com/diffplug/blowdryer/BlowdryerPluginTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,21 @@ private void settingsGithub(String tag, String... extra) throws IOException {
private void settingsGitlab(String tag, String... extra) throws IOException {
write("settings.gradle",
"plugins { id 'com.diffplug.blowdryerSetup' }",
"blowdryerSetup { gitlab('vgropp/blowdryer-test', 'tag', '" + tag + "') }",
"blowdryerSetup { gitlab('diffplug/blowdryer', 'tag', '" + tag + "') }",
Arrays.stream(extra).collect(Collectors.joining("\n")));
}

private void settingsCustomGitlab(String tag, String... extra) throws IOException {
write("settings.gradle",
"plugins { id 'com.diffplug.blowdryerSetup' }",
"blowdryerSetup { gitlab('diffplug/blowdryer', 'tag', '" + tag + "').customDomainHttps('gitlab.com') }",
Arrays.stream(extra).collect(Collectors.joining("\n")));
}

private void settingsGitlabRootFolder(String tag, String... extra) throws IOException {
write("settings.gradle",
"plugins { id 'com.diffplug.blowdryerSetup' }",
"blowdryerSetup { repoSubfolder(''); gitlab('diffplug/blowdryer', 'tag', '" + tag + "') }",
Arrays.stream(extra).collect(Collectors.joining("\n")));
}

Expand Down Expand Up @@ -89,6 +103,58 @@ public void gitlabTag() throws IOException {
gradleRunner().buildAndFail();
}

@Test
public void customGitlabTag() throws IOException {
settingsCustomGitlab("test/2/a");
write("build.gradle",
"apply plugin: 'com.diffplug.blowdryer'",
"assert 干.file('sample').text == 'a'",
"assert 干.prop('sample', 'name') == 'test'",
"assert 干.prop('sample', 'ver_spotless') == '1.2.0'");
gradleRunner().build();

settingsCustomGitlab("test/2/b");
write("build.gradle",
"apply plugin: 'com.diffplug.blowdryer'",
"assert 干.file('sample').text == 'b'",
"assert 干.prop('sample', 'name') == 'testB'",
"assert 干.prop('sample', 'group') == 'com.diffplug.gradleB'");
gradleRunner().build();

// double-check that failures do fail
settingsCustomGitlab("test/2/b");
write("build.gradle",
"plugins { id 'com.diffplug.blowdryer' }",
"assert Blowdryer.file('sample').text == 'a'");
gradleRunner().buildAndFail();
}

@Test
public void rootfolderGitlabTag() throws IOException {
settingsGitlabRootFolder("test/2/a");
write("build.gradle",
"apply plugin: 'com.diffplug.blowdryer'",
"assert 干.file('src/main/resources/sample').text == 'a'",
"assert 干.prop('src/main/resources/sample', 'name') == 'test'",
"assert 干.prop('src/main/resources/sample', 'ver_spotless') == '1.2.0'");
gradleRunner().build();

settingsGitlabRootFolder("test/2/b");
write("build.gradle",
"apply plugin: 'com.diffplug.blowdryer'",
"assert 干.file('src/main/resources/sample').text == 'b'",
"assert 干.prop('src/main/resources/sample', 'name') == 'testB'",
"assert 干.prop('src/main/resources/sample', 'group') == 'com.diffplug.gradleB'");
gradleRunner().build();

// double-check that failures do fail
settingsGitlabRootFolder("test/2/b");
write("build.gradle",
"plugins { id 'com.diffplug.blowdryer' }",
"assert Blowdryer.file('src/main/resources/sample').text == 'a'");
gradleRunner().buildAndFail();
}

@Test
public void devLocal() throws IOException {
write("../blowdryer-script/src/main/resources/sample", "c");
Expand All @@ -100,7 +166,8 @@ public void devLocal() throws IOException {
"blowdryerSetup { devLocal('../blowdryer-script') }");
write("build.gradle",
"apply plugin: 'com.diffplug.blowdryer'",
"assert 干.file('sample').text == 'c\\n'",
// .replace('\\r', '') fixes test on windows
"assert 干.file('sample').text.replace('\\r', '') == 'c\\n'",
"assert 干.prop('sample', 'name') == 'test'",
"assert 干.prop('sample', 'group') == 'com.diffplug.gradle'");
gradleRunner().build();
Expand Down