Skip to content

Commit 0fd2838

Browse files
authored
github: GitHub app installation auth support (#203)
1 parent c4c3959 commit 0fd2838

29 files changed

+1680
-241
lines changed

tasks/git/pom.xml

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,17 @@
5959
<artifactId>jackson-core</artifactId>
6060
<scope>provided</scope>
6161
</dependency>
62+
<dependency>
63+
<groupId>com.fasterxml.jackson.datatype</groupId>
64+
<artifactId>jackson-datatype-jsr310</artifactId>
65+
<scope>provided</scope>
66+
</dependency>
67+
<!-- Immutables -->
68+
<dependency>
69+
<groupId>org.immutables</groupId>
70+
<artifactId>value</artifactId>
71+
<scope>provided</scope>
72+
</dependency>
6273
<dependency>
6374
<groupId>com.jcraft</groupId>
6475
<artifactId>jsch</artifactId>
@@ -86,6 +97,25 @@
8697
<artifactId>gson</artifactId>
8798
<version>2.8.9</version>
8899
</dependency>
100+
<dependency>
101+
<groupId>com.nimbusds</groupId>
102+
<artifactId>nimbus-jose-jwt</artifactId>
103+
</dependency>
104+
<dependency>
105+
<groupId>org.bouncycastle</groupId>
106+
<artifactId>bcprov-ext-jdk15on</artifactId>
107+
<version>1.70</version> <!-- TODO use version in future targetplatform -->
108+
</dependency>
109+
<dependency>
110+
<groupId>org.bouncycastle</groupId>
111+
<artifactId>bcpkix-jdk15on</artifactId>
112+
<version>1.70</version> <!-- TODO use version in future targetplatform -->
113+
</dependency>
114+
<dependency>
115+
<groupId>org.bouncycastle</groupId>
116+
<artifactId>bcprov-jdk15on</artifactId>
117+
<version>1.70</version>
118+
</dependency>
89119

90120
<dependency>
91121
<groupId>org.junit.jupiter</groupId>
@@ -97,6 +127,12 @@
97127
<artifactId>mockito-core</artifactId>
98128
<scope>test</scope>
99129
</dependency>
130+
<dependency>
131+
<groupId>org.mockito</groupId>
132+
<artifactId>mockito-junit-jupiter</artifactId>
133+
<version>4.9.0</version>
134+
<scope>test</scope>
135+
</dependency>
100136
<dependency>
101137
<groupId>org.wiremock</groupId>
102138
<artifactId>wiremock-jetty12</artifactId>

tasks/git/src/main/java/com/walmartlabs/concord/plugins/git/GitHubTask.java

Lines changed: 171 additions & 170 deletions
Large diffs are not rendered by default.

tasks/git/src/main/java/com/walmartlabs/concord/plugins/git/GitSecretService.java

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,4 +25,7 @@
2525
public interface GitSecretService {
2626

2727
Path exportPrivateKeyAsFile(String orgName, String secretName, String pwd) throws Exception;
28+
29+
Path exportFile(String orgName, String secretName, String pwd) throws Exception;
30+
2831
}

tasks/git/src/main/java/com/walmartlabs/concord/plugins/git/Utils.java

Lines changed: 15 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,8 @@
2020
* =====
2121
*/
2222

23+
import com.fasterxml.jackson.databind.ObjectMapper;
24+
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
2325
import com.walmartlabs.concord.common.secret.UsernamePassword;
2426
import com.walmartlabs.concord.sdk.Secret;
2527

@@ -29,15 +31,18 @@
2931

3032
public final class Utils {
3133

34+
private static final ObjectMapper MAPPER = new ObjectMapper()
35+
.registerModule(new JavaTimeModule());
36+
3237
public static boolean getBoolean(Map<String, Object> in, String k, boolean fallback) {
3338
Object v = in.get(k);
3439

3540
if (v == null) {
3641
return fallback;
3742
}
3843

39-
if (v instanceof String) {
40-
return Boolean.parseBoolean((String) v);
44+
if (v instanceof String s) {
45+
return Boolean.parseBoolean(s);
4146
}
4247

4348
if (!(v instanceof Boolean)) {
@@ -60,20 +65,24 @@ public static String hideSensitiveData(String s, Secret secret) {
6065
return null;
6166
}
6267

63-
if (secret instanceof UsernamePassword) {
64-
char[] password = ((UsernamePassword) secret).getPassword();
68+
if (secret instanceof UsernamePassword up) {
69+
char[] password = up.getPassword();
6570
if (password != null && password.length != 0) {
6671
s = s.replace(new String(password), "***");
6772
}
68-
} else if (secret instanceof TokenSecret) {
69-
String token = ((TokenSecret) secret).getToken();
73+
} else if (secret instanceof TokenSecret ts) {
74+
String token = ts.getToken();
7075
if (token != null && !token.trim().isEmpty()) {
7176
s = s.replace(token, "***");
7277
}
7378
}
7479
return s;
7580
}
7681

82+
public static ObjectMapper getObjectMapper() {
83+
return MAPPER;
84+
}
85+
7786
private Utils() {
7887
}
7988
}
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
package com.walmartlabs.concord.plugins.git.model;
2+
3+
/*-
4+
* *****
5+
* Concord
6+
* -----
7+
* Copyright (C) 2017 - 2025 Walmart Inc., Concord Authors
8+
* -----
9+
* Licensed under the Apache License, Version 2.0 (the "License");
10+
* you may not use this file except in compliance with the License.
11+
* You may obtain a copy of the License at
12+
*
13+
* http://www.apache.org/licenses/LICENSE-2.0
14+
*
15+
* Unless required by applicable law or agreed to in writing, software
16+
* distributed under the License is distributed on an "AS IS" BASIS,
17+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
18+
* See the License for the specific language governing permissions and
19+
* limitations under the License.
20+
* =====
21+
*/
22+
23+
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
24+
import com.fasterxml.jackson.annotation.JsonProperty;
25+
import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
26+
import org.immutables.value.Value;
27+
28+
@Value.Immutable
29+
@Value.Style(jdkOnly = true)
30+
@JsonDeserialize(as = ImmutableAppInstallation.class)
31+
@JsonIgnoreProperties(ignoreUnknown = true)
32+
public interface AppInstallation {
33+
34+
/*
35+
This is all we **need**, even though there's other attributes. Some may differ
36+
between GitHub "cloud" and GitHub Enterprise. So, be care if/when adding more.
37+
*/
38+
@JsonProperty("access_tokens_url")
39+
String accessTokensUrl();
40+
41+
}
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
package com.walmartlabs.concord.plugins.git.model;
2+
3+
/*-
4+
* *****
5+
* Concord
6+
* -----
7+
* Copyright (C) 2017 - 2025 Walmart Inc., Concord Authors
8+
* -----
9+
* Licensed under the Apache License, Version 2.0 (the "License");
10+
* you may not use this file except in compliance with the License.
11+
* You may obtain a copy of the License at
12+
*
13+
* http://www.apache.org/licenses/LICENSE-2.0
14+
*
15+
* Unless required by applicable law or agreed to in writing, software
16+
* distributed under the License is distributed on an "AS IS" BASIS,
17+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
18+
* See the License for the specific language governing permissions and
19+
* limitations under the License.
20+
* =====
21+
*/
22+
23+
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
24+
import com.fasterxml.jackson.annotation.JsonProperty;
25+
import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
26+
import org.immutables.value.Value;
27+
28+
import java.time.OffsetDateTime;
29+
30+
@Value.Immutable
31+
@Value.Style(jdkOnly = true)
32+
@JsonDeserialize(as = ImmutableAppInstallationAccessToken.class)
33+
@JsonIgnoreProperties(ignoreUnknown = true)
34+
public interface AppInstallationAccessToken {
35+
36+
String token();
37+
38+
@JsonProperty("expires_at")
39+
OffsetDateTime expiresAt();
40+
41+
}
Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
package com.walmartlabs.concord.plugins.git.model;
2+
3+
/*-
4+
* *****
5+
* Concord
6+
* -----
7+
* Copyright (C) 2017 - 2025 Walmart Inc., Concord Authors
8+
* -----
9+
* Licensed under the Apache License, Version 2.0 (the "License");
10+
* you may not use this file except in compliance with the License.
11+
* You may obtain a copy of the License at
12+
*
13+
* http://www.apache.org/licenses/LICENSE-2.0
14+
*
15+
* Unless required by applicable law or agreed to in writing, software
16+
* distributed under the License is distributed on an "AS IS" BASIS,
17+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
18+
* See the License for the specific language governing permissions and
19+
* limitations under the License.
20+
* =====
21+
*/
22+
23+
import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
24+
import org.immutables.value.Value;
25+
26+
import javax.annotation.Nullable;
27+
28+
@Value.Immutable
29+
@Value.Style(jdkOnly = true)
30+
@JsonDeserialize(as = ImmutableAuth.class)
31+
public interface Auth {
32+
33+
@Nullable
34+
String accessToken();
35+
@Nullable
36+
AppInstallationAuth appInstallation();
37+
@Nullable
38+
AppInstallationSecretAuth appInstallationSecret();
39+
40+
static ImmutableAuth.Builder builder() {
41+
return ImmutableAuth.builder();
42+
}
43+
44+
@Value.Immutable
45+
@Value.Style(jdkOnly = true)
46+
@JsonDeserialize(as = ImmutableAppInstallationAuth.class)
47+
interface AppInstallationAuth {
48+
String privateKey();
49+
50+
String clientId();
51+
52+
@Value.Default
53+
default long refreshBufferSeconds() {
54+
return 60;
55+
}
56+
57+
static ImmutableAppInstallationAuth.Builder builder() {
58+
return ImmutableAppInstallationAuth.builder();
59+
}
60+
}
61+
62+
@Value.Immutable
63+
@Value.Style(jdkOnly = true)
64+
@JsonDeserialize(as = ImmutableAppInstallationSecretAuth.class)
65+
interface AppInstallationSecretAuth {
66+
String org();
67+
68+
String name();
69+
70+
@Nullable
71+
String password();
72+
73+
static ImmutableAppInstallationSecretAuth.Builder builder() {
74+
return ImmutableAppInstallationSecretAuth.builder();
75+
}
76+
}
77+
78+
}
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
package com.walmartlabs.concord.plugins.git.model;
2+
3+
/*-
4+
* *****
5+
* Concord
6+
* -----
7+
* Copyright (C) 2017 - 2025 Walmart Inc., Concord Authors
8+
* -----
9+
* Licensed under the Apache License, Version 2.0 (the "License");
10+
* you may not use this file except in compliance with the License.
11+
* You may obtain a copy of the License at
12+
*
13+
* http://www.apache.org/licenses/LICENSE-2.0
14+
*
15+
* Unless required by applicable law or agreed to in writing, software
16+
* distributed under the License is distributed on an "AS IS" BASIS,
17+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
18+
* See the License for the specific language governing permissions and
19+
* limitations under the License.
20+
* =====
21+
*/
22+
23+
import com.walmartlabs.concord.plugins.git.tokens.AccessTokenProvider;
24+
import org.immutables.value.Value;
25+
26+
@Value.Immutable
27+
@Value.Style(jdkOnly = true)
28+
public interface GitHubApiInfo {
29+
30+
String baseUrl();
31+
32+
AccessTokenProvider accessTokenProvider();
33+
34+
static ImmutableGitHubApiInfo.Builder builder() {
35+
return ImmutableGitHubApiInfo.builder();
36+
}
37+
}
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
package com.walmartlabs.concord.plugins.git.tokens;
2+
3+
/*-
4+
* *****
5+
* Concord
6+
* -----
7+
* Copyright (C) 2017 - 2025 Walmart Inc., Concord Authors
8+
* -----
9+
* Licensed under the Apache License, Version 2.0 (the "License");
10+
* you may not use this file except in compliance with the License.
11+
* You may obtain a copy of the License at
12+
*
13+
* http://www.apache.org/licenses/LICENSE-2.0
14+
*
15+
* Unless required by applicable law or agreed to in writing, software
16+
* distributed under the License is distributed on an "AS IS" BASIS,
17+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
18+
* See the License for the specific language governing permissions and
19+
* limitations under the License.
20+
* =====
21+
*/
22+
23+
import com.walmartlabs.concord.plugins.git.GitSecretService;
24+
import com.walmartlabs.concord.plugins.git.Utils;
25+
import com.walmartlabs.concord.plugins.git.model.Auth;
26+
27+
import java.nio.file.Files;
28+
import java.util.Objects;
29+
import java.util.stream.Stream;
30+
31+
public interface AccessTokenProvider {
32+
33+
String getToken();
34+
35+
static AccessTokenProvider fromAuth(Auth auth,
36+
String baseUrl,
37+
String installationRepo,
38+
GitSecretService secretService) {
39+
40+
Stream.of(auth.accessToken(), auth.appInstallation(), auth.appInstallationSecret())
41+
.filter(Objects::nonNull)
42+
.findFirst()
43+
.orElseThrow(() -> new IllegalArgumentException("Invalid 'auth' input."));
44+
45+
if (auth.accessToken() != null) {
46+
return new BasicTokenProvider(auth.accessToken());
47+
} else if (auth.appInstallation() != null) {
48+
var appInstallation = auth.appInstallation();
49+
return new AppInstallationTokenProvider(appInstallation, baseUrl, installationRepo);
50+
} else if (auth.appInstallationSecret() != null) {
51+
var sAuth = Objects.requireNonNull(auth.appInstallationSecret());
52+
try (var is = Files.newInputStream(secretService.exportFile(sAuth.org(), sAuth.name(), sAuth.password()))) {
53+
var a = Utils.getObjectMapper().readValue(is, Auth.AppInstallationAuth.class);
54+
return new AppInstallationTokenProvider(a, baseUrl, installationRepo);
55+
} catch (Exception e) {
56+
throw new RuntimeException("Failed to read the app installation token from secret '" + sAuth.name() + "'", e);
57+
}
58+
} else {
59+
throw new IllegalArgumentException("Unknown auth type");
60+
}
61+
}
62+
63+
}

0 commit comments

Comments
 (0)