Skip to content

Commit

Permalink
fix(concourse): support old and new concourse auth (#762) (#823)
Browse files Browse the repository at this point in the history
* fix(concourse): support old and new concourse auth

Fixes concourse authentication for clusters >= v6.1.0 by detecting version and providing different implementation accordingly.

Addresses issue: spinnaker/spinnaker#5797

* fix(concourse): support old and new concourse auth

Sending basic auth header to /info endpoint causes 401

Addresses issue: spinnaker/spinnaker#5797

Co-authored-by: Jared Stehler <jared.stehler@edgenuity.com>

Co-authored-by: Jared Stehler <jared.stehler@gmail.com>
Co-authored-by: Jared Stehler <jared.stehler@edgenuity.com>
  • Loading branch information
3 people authored Jul 22, 2020
1 parent 9298b39 commit 4fe1326
Show file tree
Hide file tree
Showing 11 changed files with 322 additions and 15 deletions.
2 changes: 2 additions & 0 deletions igor-web/igor-web.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,8 @@ dependencies {
implementation "com.sun.xml.bind:jaxb-core:2.3.0.1"
implementation "com.sun.xml.bind:jaxb-impl:2.3.2"

implementation "com.vdurmont:semver4j:3.1.0"

testImplementation "org.springframework.boot:spring-boot-starter-test"
testImplementation "org.spockframework:spock-core"
testImplementation "org.spockframework:spock-spring"
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
/*
* Copyright 2019 Pivotal, Inc.
*
* 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
*
* http://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.netflix.spinnaker.igor.concourse.client;

import com.netflix.spinnaker.igor.concourse.client.model.ClusterInfo;
import retrofit.http.GET;

public interface ClusterInfoService {
@GET("/api/v1/info")
ClusterInfo clusterInfo();
}
Original file line number Diff line number Diff line change
Expand Up @@ -20,14 +20,17 @@
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.PropertyNamingStrategy;
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
import com.netflix.spinnaker.igor.concourse.client.model.ClusterInfo;
import com.netflix.spinnaker.igor.concourse.client.model.Token;
import com.netflix.spinnaker.retrofit.Slf4jRetrofitLogger;
import com.squareup.okhttp.OkHttpClient;
import com.vdurmont.semver4j.Semver;
import java.time.ZonedDateTime;
import lombok.Getter;
import retrofit.RequestInterceptor;
import retrofit.RestAdapter;
import retrofit.client.OkClient;
import retrofit.client.Response;
import retrofit.converter.JacksonConverter;

public class ConcourseClient {
Expand All @@ -36,7 +39,12 @@ public class ConcourseClient {
private final String password;
private final OkHttpClient okHttpClient;

@Getter private TokenService tokenService;
private final SkyService skyServiceV1;
private final SkyServiceV2 skyServiceV2;
private final TokenService tokenServiceV1;
private final TokenServiceV2 tokenServiceV2;

@Getter private ClusterInfoService clusterInfoService;

@Getter private BuildService buildService;

Expand All @@ -48,11 +56,9 @@ public class ConcourseClient {

@Getter private PipelineService pipelineService;

@Getter private SkyService skyService;

@Getter private ResourceService resourceService;

private volatile ZonedDateTime tokenExpiration = ZonedDateTime.now();
private volatile ZonedDateTime tokenExpiration = ZonedDateTime.now().minusSeconds(1);
private volatile Token token;

private JacksonConverter jacksonConverter;
Expand Down Expand Up @@ -80,25 +86,55 @@ public ConcourseClient(String host, String user, String password) {
this.okHttpClient = OkHttpClientBuilder.retryingClient(this::refreshToken);
this.jacksonConverter = new JacksonConverter(mapper);

this.tokenService =
RestAdapter.Builder tokenRestBuilder =
new RestAdapter.Builder()
.setEndpoint(host)
.setClient(new OkClient(okHttpClient))
.setConverter(jacksonConverter)
.setRequestInterceptor(
request -> {
request.addHeader("Authorization", "Basic Zmx5OlpteDU=");
})
.setLog(new Slf4jRetrofitLogger(TokenService.class))
});

this.clusterInfoService =
new RestAdapter.Builder()
.setEndpoint(host)
.setClient(new OkClient(okHttpClient))
.setConverter(jacksonConverter)
.setLog(new Slf4jRetrofitLogger(ClusterInfoService.class))
.build()
.create(TokenService.class);
.create(ClusterInfoService.class);

ClusterInfo clusterInfo = this.clusterInfoService.clusterInfo();

Semver ver = new Semver("6.1.0");
if (ver.isGreaterThan(clusterInfo.getVersion())) {
this.tokenServiceV1 =
tokenRestBuilder
.setLog(new Slf4jRetrofitLogger(TokenService.class))
.build()
.create(TokenService.class);
this.tokenServiceV2 = null;

this.skyServiceV1 = createService(SkyService.class);
this.skyServiceV2 = null;
} else {
this.tokenServiceV1 = null;
this.tokenServiceV2 =
tokenRestBuilder
.setLog(new Slf4jRetrofitLogger(TokenServiceV2.class))
.build()
.create(TokenServiceV2.class);

this.skyServiceV1 = null;
this.skyServiceV2 = createService(SkyServiceV2.class);
}

this.buildService = createService(BuildService.class);
this.jobService = createService(JobService.class);
this.teamService = createService(TeamService.class);
this.pipelineService = createService(PipelineService.class);
this.resourceService = createService(ResourceService.class);
this.skyService = createService(SkyService.class);
this.eventService = new EventService(host, this::refreshToken, mapper);
}

Expand All @@ -110,12 +146,20 @@ private void refreshTokenIfNecessary() {

private Token refreshToken() {
token =
tokenService.passwordToken(
"password", user, password, "openid profile email federated:id groups");
tokenServiceV1 != null
? tokenServiceV1.passwordToken(
"password", user, password, "openid profile email federated:id groups")
: tokenServiceV2.passwordToken(
"password", user, password, "openid profile email federated:id groups");

tokenExpiration = token.getExpiry();
return token;
}

public Response userInfo() {
return skyServiceV1 != null ? skyServiceV1.userInfo() : skyServiceV2.userInfo();
}

private <S> S createService(Class<S> serviceClass) {
return new RestAdapter.Builder()
.setEndpoint(host)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
/*
* Copyright 2019 Pivotal, Inc.
*
* 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
*
* http://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.netflix.spinnaker.igor.concourse.client;

import retrofit.client.Response;
import retrofit.http.GET;

public interface SkyServiceV2 {
@GET("/api/v1/user")
Response userInfo();
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
/*
* Copyright 2019 Pivotal, Inc.
*
* 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
*
* http://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.netflix.spinnaker.igor.concourse.client;

import com.netflix.spinnaker.igor.concourse.client.model.TokenV2;
import retrofit.http.Field;
import retrofit.http.FormUrlEncoded;
import retrofit.http.POST;

public interface TokenServiceV2 {
@FormUrlEncoded
@POST("/sky/issuer/token")
TokenV2 passwordToken(
@Field("grant_type") String grantType,
@Field("username") String username,
@Field("password") String password,
@Field("scope") String scope);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
/*
* Copyright 2019 Pivotal, Inc.
*
* 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
*
* http://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.netflix.spinnaker.igor.concourse.client.model;

import lombok.Data;

@Data
public class ClusterInfo {
private String clusterName;
private String externalUrl;
private String version;
private String workerVersion;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
/*
* Copyright 2019 Pivotal, Inc.
*
* 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
*
* http://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.netflix.spinnaker.igor.concourse.client.model;

import com.fasterxml.jackson.annotation.JsonProperty;
import java.time.ZonedDateTime;

public class TokenV2 extends Token {

@Override
@JsonProperty("id_token")
public void setAccessToken(String idToken) {
super.setAccessToken(idToken);
}

@Override
public ZonedDateTime getExpiry() {
return ZonedDateTime.now();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -363,6 +363,6 @@ private Job toJob(String jobPath) {
private void refreshTokenIfNecessary() {
// returns a 401 on expired/invalid token, which because of retry logic causes the token to be
// refreshed.
client.getSkyService().userInfo();
client.userInfo();
}
}
Loading

0 comments on commit 4fe1326

Please sign in to comment.