Skip to content
This repository was archived by the owner on Dec 20, 2025. It is now read-only.

Commit 6a2024e

Browse files
fix(retrofit2): fix multiple slash issue in baseUrl of retrofit2 client (#1206) (#1207)
* test(github): add a test to demonstrate an error scenario where if the baseUrl of the Github client has multiple slashes and the end point start with a slash * fix(github): fix the error where if the baseUrl of the Github client has multiple slashes and the end point start with a slash, the resultant api url leaves that part the base url coming after first slash. * refactor(api): refactor retrofit2 client configuration for FiatService to fix any issues that may occur if the base url has multiple slashes * refactor(web): refactor retrofit2 client configuration for IgorApi, ClouddriverApi and Front50Api to fix any issues that may occur if the base url has multiple slashes (cherry picked from commit a7f7f9d) Co-authored-by: Kiran Godishala <53332225+kirangodishala@users.noreply.github.com>
1 parent 208d247 commit 6a2024e

File tree

12 files changed

+153
-24
lines changed

12 files changed

+153
-24
lines changed

fiat-api/src/main/java/com/netflix/spinnaker/fiat/shared/FiatAuthenticationConfig.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
import com.fasterxml.jackson.databind.ObjectMapper;
2323
import com.netflix.spinnaker.config.ErrorConfiguration;
2424
import com.netflix.spinnaker.config.OkHttp3ClientConfiguration;
25+
import com.netflix.spinnaker.fiat.util.RetrofitUtils;
2526
import com.netflix.spinnaker.kork.retrofit.ErrorHandlingExecutorCallAdapterFactory;
2627
import com.netflix.spinnaker.kork.web.exceptions.ExceptionMessageDecorator;
2728
import lombok.val;
@@ -61,7 +62,7 @@ public FiatService fiatService(
6162
objectMapper.disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES);
6263

6364
return new Retrofit.Builder()
64-
.baseUrl(fiatConfigurationProperties.getBaseUrl())
65+
.baseUrl(RetrofitUtils.getBaseUrl(fiatConfigurationProperties.getBaseUrl()))
6566
.client(okHttpClientConfig.createForRetrofit2().build())
6667
.addCallAdapterFactory(ErrorHandlingExecutorCallAdapterFactory.getInstance())
6768
.addConverterFactory(JacksonConverterFactory.create(objectMapper))

fiat-api/src/main/java/com/netflix/spinnaker/fiat/shared/FiatService.java

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ public interface FiatService {
3333
* @param userId The username of the user
3434
* @return The full UserPermission of the user.
3535
*/
36-
@GET("/authorize/{userId}")
36+
@GET("authorize/{userId}")
3737
Call<UserPermission.View> getUserPermission(@Path("userId") String userId);
3838

3939
/**
@@ -43,7 +43,7 @@ public interface FiatService {
4343
* @param authorization The authorization in question (read, write, etc)
4444
* @return True if the user has access to the specified resource.
4545
*/
46-
@GET("/authorize/{userId}/{resourceType}/{resourceName}/{authorization}")
46+
@GET("authorize/{userId}/{resourceType}/{resourceName}/{authorization}")
4747
Call<Void> hasAuthorization(
4848
@Path("userId") String userId,
4949
@Path("resourceType") String resourceType,
@@ -58,7 +58,7 @@ Call<Void> hasAuthorization(
5858
* @param resourceType The type of the resource
5959
* @param resource The resource to check
6060
*/
61-
@POST("/authorize/{userId}/{resourceType}/create")
61+
@POST("authorize/{userId}/{resourceType}/create")
6262
Call<Void> canCreate(
6363
@Path("userId") String userId,
6464
@Path("resourceType") String resourceType,
@@ -69,7 +69,7 @@ Call<Void> canCreate(
6969
*
7070
* @return The number of non-anonymous users synced.
7171
*/
72-
@POST("/roles/sync")
72+
@POST("roles/sync")
7373
Call<Long> sync();
7474

7575
/**
@@ -78,7 +78,7 @@ Call<Void> canCreate(
7878
* @param roles Users with any role listed should be updated.
7979
* @return The number of non-anonymous users synced.
8080
*/
81-
@POST("/roles/sync")
81+
@POST("roles/sync")
8282
Call<Long> sync(@Body List<String> roles);
8383

8484
/**
@@ -89,15 +89,15 @@ Call<Void> canCreate(
8989
* @param roles The roles allowed for this service account.
9090
* @return The number of non-anonymous users synced.
9191
*/
92-
@POST("/roles/sync/serviceAccount/{serviceAccountId}")
92+
@POST("roles/sync/serviceAccount/{serviceAccountId}")
9393
Call<Long> syncServiceAccount(
9494
@Path("serviceAccountId") String serviceAccountId, @Body List<String> roles);
9595

9696
/**
9797
* @param userId The user being logged in
9898
* @return ignored.
9999
*/
100-
@POST("/roles/{userId}")
100+
@POST("roles/{userId}")
101101
Call<Void> loginUser(@Path("userId") String userId);
102102

103103
/**
@@ -107,13 +107,13 @@ Call<Long> syncServiceAccount(
107107
* @param roles Collection of roles from the identity provider
108108
* @return ignored.
109109
*/
110-
@PUT("/roles/{userId}")
110+
@PUT("roles/{userId}")
111111
Call<Void> loginWithRoles(@Path("userId") String userId, @Body Collection<String> roles);
112112

113113
/**
114114
* @param userId The user being logged out
115115
* @return ignored.
116116
*/
117-
@DELETE("/roles/{userId}")
117+
@DELETE("roles/{userId}")
118118
Call<Void> logoutUser(@Path("userId") String userId);
119119
}

fiat-core/fiat-core.gradle

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ dependencies {
44

55
implementation "com.fasterxml.jackson.core:jackson-annotations"
66
implementation "com.google.code.findbugs:jsr305"
7+
implementation "io.spinnaker.kork:kork-retrofit"
78
implementation "org.slf4j:slf4j-api"
89
implementation "org.springframework:spring-core"
910
implementation "org.springframework.boot:spring-boot-starter-web"
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
/*
2+
* Copyright 2025 OpsMx, Inc.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package com.netflix.spinnaker.fiat.util;
18+
19+
import okhttp3.HttpUrl;
20+
21+
public class RetrofitUtils {
22+
23+
/**
24+
* Converts a given URL to a valid base URL for use in a {@link retrofit2.Retrofit} instance. If
25+
* the URL is invalid, an {@link IllegalArgumentException} is thrown. If the URL does not end with
26+
* a slash, a slash is appended to the end of the URL.
27+
*
28+
* @param suppliedBaseUrl the URL to convert
29+
* @return a valid base URL for use in a Retrofit instance
30+
*/
31+
public static String getBaseUrl(String suppliedBaseUrl) {
32+
HttpUrl parsedUrl = HttpUrl.parse(suppliedBaseUrl);
33+
if (parsedUrl == null) {
34+
throw new IllegalArgumentException("Invalid URL: " + suppliedBaseUrl);
35+
}
36+
String baseUrl = parsedUrl.newBuilder().build().toString();
37+
if (!baseUrl.endsWith("/")) {
38+
baseUrl += "/";
39+
}
40+
return baseUrl;
41+
}
42+
}

fiat-github/fiat-github.gradle

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,4 +9,6 @@ dependencies {
99
implementation "io.spinnaker.kork:kork-web"
1010
implementation "io.spinnaker.kork:kork-retrofit"
1111
implementation "javax.validation:validation-api"
12+
13+
testImplementation "com.github.tomakehurst:wiremock-jre8-standalone"
1214
}

fiat-github/src/main/java/com/netflix/spinnaker/fiat/config/GitHubConfig.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
import com.netflix.spinnaker.config.OkHttp3ClientConfiguration;
44
import com.netflix.spinnaker.fiat.roles.github.GitHubProperties;
55
import com.netflix.spinnaker.fiat.roles.github.client.GitHubClient;
6+
import com.netflix.spinnaker.fiat.util.RetrofitUtils;
67
import com.netflix.spinnaker.kork.retrofit.ErrorHandlingExecutorCallAdapterFactory;
78
import java.io.IOException;
89
import lombok.Setter;
@@ -35,7 +36,7 @@ public GitHubClient gitHubClient(OkHttp3ClientConfiguration okHttpClientConfig)
3536
new BasicAuthRequestInterceptor().setAccessToken(gitHubProperties.getAccessToken());
3637

3738
return new Retrofit.Builder()
38-
.baseUrl(gitHubProperties.getBaseUrl())
39+
.baseUrl(RetrofitUtils.getBaseUrl(gitHubProperties.getBaseUrl()))
3940
.client(okHttpClientConfig.createForRetrofit2().addInterceptor(interceptor).build())
4041
.addCallAdapterFactory(ErrorHandlingExecutorCallAdapterFactory.getInstance())
4142
.addConverterFactory(JacksonConverterFactory.create())

fiat-github/src/main/java/com/netflix/spinnaker/fiat/roles/github/client/GitHubClient.java

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -27,15 +27,15 @@
2727
/** Retrofit interface for interacting with a GitHub REST API. */
2828
public interface GitHubClient {
2929

30-
@GET("/orgs/{org}/teams")
30+
@GET("orgs/{org}/teams")
3131
Call<List<Team>> getOrgTeams(
3232
@Path("org") String org, @Query("page") int page, @Query("per_page") int paginationValue);
3333

34-
@GET("/orgs/{org}/members")
34+
@GET("orgs/{org}/members")
3535
Call<List<Member>> getOrgMembers(
3636
@Path("org") String org, @Query("page") int page, @Query("per_page") int paginationValue);
3737

38-
@GET("/orgs/{org}/teams/{teamSlug}/members")
38+
@GET("orgs/{org}/teams/{teamSlug}/members")
3939
Call<List<Member>> getMembersOfTeam(
4040
@Path("org") String org,
4141
@Path("teamSlug") String teamSlug,
Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
/*
2+
* Copyright 2025 OpsMx, Inc.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package com.netflix.spinnaker.fiat.roles.github;
18+
19+
import static com.github.tomakehurst.wiremock.client.WireMock.aResponse;
20+
import static com.github.tomakehurst.wiremock.client.WireMock.urlEqualTo;
21+
import static com.github.tomakehurst.wiremock.core.WireMockConfiguration.wireMockConfig;
22+
import static org.assertj.core.api.AssertionsForClassTypes.assertThat;
23+
24+
import com.github.tomakehurst.wiremock.client.WireMock;
25+
import com.github.tomakehurst.wiremock.junit5.WireMockExtension;
26+
import com.netflix.spinnaker.fiat.roles.github.client.GitHubClient;
27+
import com.netflix.spinnaker.fiat.roles.github.model.Member;
28+
import com.netflix.spinnaker.kork.retrofit.ErrorHandlingExecutorCallAdapterFactory;
29+
import com.netflix.spinnaker.kork.retrofit.Retrofit2SyncCall;
30+
import java.util.List;
31+
import okhttp3.OkHttpClient;
32+
import org.junit.jupiter.api.BeforeEach;
33+
import org.junit.jupiter.api.Test;
34+
import org.junit.jupiter.api.extension.RegisterExtension;
35+
import retrofit2.Retrofit;
36+
import retrofit2.converter.jackson.JacksonConverterFactory;
37+
38+
public class GithubTeamsUserRolesProviderTest {
39+
@RegisterExtension
40+
static WireMockExtension wmGithub =
41+
WireMockExtension.newInstance().options(wireMockConfig().dynamicPort()).build();
42+
43+
GitHubClient gitHubClient;
44+
int port;
45+
String baseUrl = "http://localhost:PORT/api/v3/";
46+
47+
@BeforeEach
48+
void setUp() {
49+
50+
port = wmGithub.getPort();
51+
52+
baseUrl = baseUrl.replaceFirst("PORT", String.valueOf(port));
53+
54+
gitHubClient =
55+
new Retrofit.Builder()
56+
.baseUrl(baseUrl)
57+
.client(new OkHttpClient())
58+
.addCallAdapterFactory(ErrorHandlingExecutorCallAdapterFactory.getInstance())
59+
.addConverterFactory(JacksonConverterFactory.create())
60+
.build()
61+
.create(GitHubClient.class);
62+
}
63+
64+
@Test
65+
void testBaseUrlWithMultipleSlashes() {
66+
wmGithub.stubFor(
67+
WireMock.get(urlEqualTo("/api/v3/orgs/org1/members?page=1&per_page=2"))
68+
.willReturn(
69+
aResponse()
70+
.withStatus(200)
71+
.withBody(
72+
"[{\"login\": \"foo\",\"id\": 18634546},{\"login\": \"bar\",\"id\": 202758929}]")));
73+
74+
List<Member> members = Retrofit2SyncCall.execute(gitHubClient.getOrgMembers("org1", 1, 2));
75+
76+
wmGithub.verify(
77+
1, WireMock.getRequestedFor(urlEqualTo("/api/v3/orgs/org1/members?page=1&per_page=2")));
78+
79+
assertThat(members.size()).isEqualTo(2);
80+
}
81+
}

fiat-roles/src/main/java/com/netflix/spinnaker/fiat/providers/internal/ClouddriverApi.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,9 +23,9 @@
2323
import retrofit2.http.GET;
2424

2525
public interface ClouddriverApi {
26-
@GET("/credentials")
26+
@GET("credentials")
2727
Call<List<Account>> getAccounts();
2828

29-
@GET("/applications?restricted=false&expand=false")
29+
@GET("applications?restricted=false&expand=false")
3030
Call<List<Application>> getApplications();
3131
}

fiat-roles/src/main/java/com/netflix/spinnaker/fiat/providers/internal/Front50Api.java

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -24,20 +24,20 @@
2424
import retrofit2.http.Query;
2525

2626
public interface Front50Api {
27-
@GET("/permissions/applications")
27+
@GET("permissions/applications")
2828
Call<List<Application>> getAllApplicationPermissions();
2929

3030
/**
3131
* @deprecated for fiat's usage this is always going to be called with restricted = false, use the
3232
* no arg method instead which has the same behavior.
3333
*/
34-
@GET("/v2/applications")
34+
@GET("v2/applications")
3535
@Deprecated
3636
Call<List<Application>> getAllApplications(@Query("restricted") boolean restricted);
3737

38-
@GET("/v2/applications?restricted=false")
38+
@GET("v2/applications?restricted=false")
3939
Call<List<Application>> getAllApplications();
4040

41-
@GET("/serviceAccounts")
41+
@GET("serviceAccounts")
4242
Call<List<ServiceAccount>> getAllServiceAccounts();
4343
}

0 commit comments

Comments
 (0)