Rework the Bitbucket Server oauth token validation
Signed-off-by: ivinokur <ivinokur@redhat.com>pull/673/head
parent
2e27c47f2f
commit
1baf3aff63
|
|
@ -1,5 +1,5 @@
|
||||||
/*
|
/*
|
||||||
* Copyright (c) 2012-2023 Red Hat, Inc.
|
* Copyright (c) 2012-2024 Red Hat, Inc.
|
||||||
* This program and the accompanying materials are made
|
* This program and the accompanying materials are made
|
||||||
* available under the terms of the Eclipse Public License 2.0
|
* available under the terms of the Eclipse Public License 2.0
|
||||||
* which is available at https://www.eclipse.org/legal/epl-2.0/
|
* which is available at https://www.eclipse.org/legal/epl-2.0/
|
||||||
|
|
@ -12,6 +12,7 @@
|
||||||
package org.eclipse.che.security.oauth;
|
package org.eclipse.che.security.oauth;
|
||||||
|
|
||||||
import static com.google.common.base.Strings.isNullOrEmpty;
|
import static com.google.common.base.Strings.isNullOrEmpty;
|
||||||
|
import static java.nio.charset.StandardCharsets.UTF_8;
|
||||||
|
|
||||||
import com.google.api.client.auth.oauth2.AuthorizationCodeRequestUrl;
|
import com.google.api.client.auth.oauth2.AuthorizationCodeRequestUrl;
|
||||||
import com.google.api.client.util.store.MemoryDataStoreFactory;
|
import com.google.api.client.util.store.MemoryDataStoreFactory;
|
||||||
|
|
@ -77,7 +78,7 @@ public class BitbucketOAuthAuthenticator extends OAuthAuthenticator {
|
||||||
private String getTestRequestUrl() {
|
private String getTestRequestUrl() {
|
||||||
return "https://bitbucket.org".equals(bitbucketEndpoint)
|
return "https://bitbucket.org".equals(bitbucketEndpoint)
|
||||||
? "https://api.bitbucket.org/2.0/user"
|
? "https://api.bitbucket.org/2.0/user"
|
||||||
: bitbucketEndpoint + "/rest/api/1.0/application-properties";
|
: bitbucketEndpoint + "/plugins/servlet/applinks/whoami";
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|
@ -89,11 +90,12 @@ public class BitbucketOAuthAuthenticator extends OAuthAuthenticator {
|
||||||
throws OAuthAuthenticationException {
|
throws OAuthAuthenticationException {
|
||||||
HttpURLConnection urlConnection = null;
|
HttpURLConnection urlConnection = null;
|
||||||
InputStream urlInputStream = null;
|
InputStream urlInputStream = null;
|
||||||
|
String result;
|
||||||
try {
|
try {
|
||||||
urlConnection = (HttpURLConnection) new URL(requestUrl).openConnection();
|
urlConnection = (HttpURLConnection) new URL(requestUrl).openConnection();
|
||||||
urlConnection.setRequestProperty("Authorization", "Bearer " + accessToken);
|
urlConnection.setRequestProperty("Authorization", "Bearer " + accessToken);
|
||||||
urlInputStream = urlConnection.getInputStream();
|
urlInputStream = urlConnection.getInputStream();
|
||||||
|
result = new String(urlInputStream.readAllBytes(), UTF_8);
|
||||||
} catch (IOException e) {
|
} catch (IOException e) {
|
||||||
throw new OAuthAuthenticationException(e.getMessage(), e);
|
throw new OAuthAuthenticationException(e.getMessage(), e);
|
||||||
} finally {
|
} finally {
|
||||||
|
|
@ -108,5 +110,8 @@ public class BitbucketOAuthAuthenticator extends OAuthAuthenticator {
|
||||||
urlConnection.disconnect();
|
urlConnection.disconnect();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if (isNullOrEmpty(result)) {
|
||||||
|
throw new OAuthAuthenticationException("Empty response from Bitbucket Server API");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -134,7 +134,8 @@ public class BitbucketServerPersonalAccessTokenFetcher implements PersonalAccess
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
BitbucketPersonalAccessToken bitbucketPersonalAccessToken =
|
BitbucketPersonalAccessToken bitbucketPersonalAccessToken =
|
||||||
bitbucketServerApiClient.getPersonalAccessToken(personalAccessToken.getScmTokenId());
|
bitbucketServerApiClient.getPersonalAccessToken(
|
||||||
|
personalAccessToken.getScmTokenId(), personalAccessToken.getToken());
|
||||||
return Optional.of(DEFAULT_TOKEN_SCOPE.equals(bitbucketPersonalAccessToken.getPermissions()));
|
return Optional.of(DEFAULT_TOKEN_SCOPE.equals(bitbucketPersonalAccessToken.getPermissions()));
|
||||||
} catch (ScmItemNotFoundException e) {
|
} catch (ScmItemNotFoundException e) {
|
||||||
return Optional.of(Boolean.FALSE);
|
return Optional.of(Boolean.FALSE);
|
||||||
|
|
@ -169,7 +170,8 @@ public class BitbucketServerPersonalAccessTokenFetcher implements PersonalAccess
|
||||||
}
|
}
|
||||||
// Token is added by OAuth. Token id is available.
|
// Token is added by OAuth. Token id is available.
|
||||||
BitbucketPersonalAccessToken bitbucketPersonalAccessToken =
|
BitbucketPersonalAccessToken bitbucketPersonalAccessToken =
|
||||||
bitbucketServerApiClient.getPersonalAccessToken(params.getScmTokenId());
|
bitbucketServerApiClient.getPersonalAccessToken(
|
||||||
|
params.getScmTokenId(), params.getToken());
|
||||||
return Optional.of(
|
return Optional.of(
|
||||||
Pair.of(
|
Pair.of(
|
||||||
DEFAULT_TOKEN_SCOPE.equals(bitbucketPersonalAccessToken.getPermissions())
|
DEFAULT_TOKEN_SCOPE.equals(bitbucketPersonalAccessToken.getPermissions())
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
/*
|
/*
|
||||||
* Copyright (c) 2012-2023 Red Hat, Inc.
|
* Copyright (c) 2012-2024 Red Hat, Inc.
|
||||||
* This program and the accompanying materials are made
|
* This program and the accompanying materials are made
|
||||||
* available under the terms of the Eclipse Public License 2.0
|
* available under the terms of the Eclipse Public License 2.0
|
||||||
* which is available at https://www.eclipse.org/legal/epl-2.0/
|
* which is available at https://www.eclipse.org/legal/epl-2.0/
|
||||||
|
|
@ -145,7 +145,9 @@ public class HttpBitbucketServerApiClient implements BitbucketServerApiClient {
|
||||||
@Override
|
@Override
|
||||||
public void deletePersonalAccessTokens(String tokenId)
|
public void deletePersonalAccessTokens(String tokenId)
|
||||||
throws ScmItemNotFoundException, ScmUnauthorizedException, ScmCommunicationException {
|
throws ScmItemNotFoundException, ScmUnauthorizedException, ScmCommunicationException {
|
||||||
URI uri = serverUri.resolve("./rest/access-tokens/1.0/users/" + getUserSlug() + "/" + tokenId);
|
URI uri =
|
||||||
|
serverUri.resolve(
|
||||||
|
"./rest/access-tokens/1.0/users/" + getUserSlug(Optional.empty()) + "/" + tokenId);
|
||||||
HttpRequest request =
|
HttpRequest request =
|
||||||
HttpRequest.newBuilder(uri)
|
HttpRequest.newBuilder(uri)
|
||||||
.DELETE()
|
.DELETE()
|
||||||
|
|
@ -185,7 +187,7 @@ public class HttpBitbucketServerApiClient implements BitbucketServerApiClient {
|
||||||
ScmItemNotFoundException {
|
ScmItemNotFoundException {
|
||||||
BitbucketPersonalAccessToken token =
|
BitbucketPersonalAccessToken token =
|
||||||
new BitbucketPersonalAccessToken(tokenName, permissions, 90);
|
new BitbucketPersonalAccessToken(tokenName, permissions, 90);
|
||||||
URI uri = serverUri.resolve("./rest/access-tokens/1.0/users/" + getUserSlug());
|
URI uri = serverUri.resolve("./rest/access-tokens/1.0/users/" + getUserSlug(Optional.empty()));
|
||||||
|
|
||||||
try {
|
try {
|
||||||
HttpRequest request =
|
HttpRequest request =
|
||||||
|
|
@ -229,7 +231,7 @@ public class HttpBitbucketServerApiClient implements BitbucketServerApiClient {
|
||||||
return doGetItems(
|
return doGetItems(
|
||||||
Optional.empty(),
|
Optional.empty(),
|
||||||
BitbucketPersonalAccessToken.class,
|
BitbucketPersonalAccessToken.class,
|
||||||
"./rest/access-tokens/1.0/users/" + getUserSlug(),
|
"./rest/access-tokens/1.0/users/" + getUserSlug(Optional.empty()),
|
||||||
null);
|
null);
|
||||||
} catch (ScmBadRequestException e) {
|
} catch (ScmBadRequestException e) {
|
||||||
throw new ScmCommunicationException(e.getMessage(), e);
|
throw new ScmCommunicationException(e.getMessage(), e);
|
||||||
|
|
@ -237,14 +239,19 @@ public class HttpBitbucketServerApiClient implements BitbucketServerApiClient {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public BitbucketPersonalAccessToken getPersonalAccessToken(String tokenId)
|
public BitbucketPersonalAccessToken getPersonalAccessToken(String tokenId, String oauthToken)
|
||||||
throws ScmItemNotFoundException, ScmUnauthorizedException, ScmCommunicationException {
|
throws ScmItemNotFoundException, ScmUnauthorizedException, ScmCommunicationException {
|
||||||
URI uri = serverUri.resolve("./rest/access-tokens/1.0/users/" + getUserSlug() + "/" + tokenId);
|
URI uri =
|
||||||
|
serverUri.resolve(
|
||||||
|
"./rest/access-tokens/1.0/users/"
|
||||||
|
+ getUserSlug(Optional.of(oauthToken))
|
||||||
|
+ "/"
|
||||||
|
+ tokenId);
|
||||||
HttpRequest request =
|
HttpRequest request =
|
||||||
HttpRequest.newBuilder(uri)
|
HttpRequest.newBuilder(uri)
|
||||||
.headers(
|
.headers(
|
||||||
"Authorization",
|
"Authorization",
|
||||||
computeAuthorizationHeader("GET", uri.toString()),
|
"Bearer " + oauthToken,
|
||||||
HttpHeaders.ACCEPT,
|
HttpHeaders.ACCEPT,
|
||||||
MediaType.APPLICATION_JSON)
|
MediaType.APPLICATION_JSON)
|
||||||
.timeout(DEFAULT_HTTP_TIMEOUT)
|
.timeout(DEFAULT_HTTP_TIMEOUT)
|
||||||
|
|
@ -269,9 +276,9 @@ public class HttpBitbucketServerApiClient implements BitbucketServerApiClient {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private String getUserSlug()
|
private String getUserSlug(Optional<String> token)
|
||||||
throws ScmItemNotFoundException, ScmCommunicationException, ScmUnauthorizedException {
|
throws ScmItemNotFoundException, ScmCommunicationException, ScmUnauthorizedException {
|
||||||
return getUser(Optional.empty()).getSlug();
|
return getUser(token).getSlug();
|
||||||
}
|
}
|
||||||
|
|
||||||
private BitbucketUser getUser(Optional<String> token)
|
private BitbucketUser getUser(Optional<String> token)
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
/*
|
/*
|
||||||
* Copyright (c) 2012-2023 Red Hat, Inc.
|
* Copyright (c) 2012-2024 Red Hat, Inc.
|
||||||
* This program and the accompanying materials are made
|
* This program and the accompanying materials are made
|
||||||
* available under the terms of the Eclipse Public License 2.0
|
* available under the terms of the Eclipse Public License 2.0
|
||||||
* which is available at https://www.eclipse.org/legal/epl-2.0/
|
* which is available at https://www.eclipse.org/legal/epl-2.0/
|
||||||
|
|
@ -100,9 +100,10 @@ public interface BitbucketServerApiClient {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param tokenId - bitbucket personal access token id.
|
* @param tokenId - bitbucket personal access token id.
|
||||||
|
* @param oauthToken - bitbucket oauth token.
|
||||||
* @return - Bitbucket personal access token.
|
* @return - Bitbucket personal access token.
|
||||||
* @throws ScmCommunicationException
|
* @throws ScmCommunicationException
|
||||||
*/
|
*/
|
||||||
BitbucketPersonalAccessToken getPersonalAccessToken(String tokenId)
|
BitbucketPersonalAccessToken getPersonalAccessToken(String tokenId, String oauthToken)
|
||||||
throws ScmItemNotFoundException, ScmUnauthorizedException, ScmCommunicationException;
|
throws ScmItemNotFoundException, ScmUnauthorizedException, ScmCommunicationException;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
/*
|
/*
|
||||||
* Copyright (c) 2012-2023 Red Hat, Inc.
|
* Copyright (c) 2012-2024 Red Hat, Inc.
|
||||||
* This program and the accompanying materials are made
|
* This program and the accompanying materials are made
|
||||||
* available under the terms of the Eclipse Public License 2.0
|
* available under the terms of the Eclipse Public License 2.0
|
||||||
* which is available at https://www.eclipse.org/legal/epl-2.0/
|
* which is available at https://www.eclipse.org/legal/epl-2.0/
|
||||||
|
|
@ -77,7 +77,7 @@ public class NoopBitbucketServerApiClient implements BitbucketServerApiClient {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public BitbucketPersonalAccessToken getPersonalAccessToken(String tokenId)
|
public BitbucketPersonalAccessToken getPersonalAccessToken(String tokenId, String oauthToken)
|
||||||
throws ScmItemNotFoundException, ScmUnauthorizedException, ScmCommunicationException {
|
throws ScmItemNotFoundException, ScmUnauthorizedException, ScmCommunicationException {
|
||||||
throw new RuntimeException("Invalid usage of BitbucketServerApi");
|
throw new RuntimeException("Invalid usage of BitbucketServerApi");
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
/*
|
/*
|
||||||
* Copyright (c) 2012-2023 Red Hat, Inc.
|
* Copyright (c) 2012-2024 Red Hat, Inc.
|
||||||
* This program and the accompanying materials are made
|
* This program and the accompanying materials are made
|
||||||
* available under the terms of the Eclipse Public License 2.0
|
* available under the terms of the Eclipse Public License 2.0
|
||||||
* which is available at https://www.eclipse.org/legal/epl-2.0/
|
* which is available at https://www.eclipse.org/legal/epl-2.0/
|
||||||
|
|
@ -221,10 +221,12 @@ public class BitbucketServerPersonalAccessTokenFetcherTest {
|
||||||
throws ScmUnauthorizedException, ScmCommunicationException, ScmItemNotFoundException {
|
throws ScmUnauthorizedException, ScmCommunicationException, ScmItemNotFoundException {
|
||||||
// given
|
// given
|
||||||
when(personalAccessTokenParams.getScmProviderUrl()).thenReturn(someBitbucketURL);
|
when(personalAccessTokenParams.getScmProviderUrl()).thenReturn(someBitbucketURL);
|
||||||
|
when(personalAccessTokenParams.getToken()).thenReturn(bitbucketPersonalAccessToken.getToken());
|
||||||
when(personalAccessTokenParams.getScmTokenId())
|
when(personalAccessTokenParams.getScmTokenId())
|
||||||
.thenReturn(bitbucketPersonalAccessToken.getId());
|
.thenReturn(bitbucketPersonalAccessToken.getId());
|
||||||
when(bitbucketServerApiClient.isConnected(eq(someBitbucketURL))).thenReturn(true);
|
when(bitbucketServerApiClient.isConnected(eq(someBitbucketURL))).thenReturn(true);
|
||||||
when(bitbucketServerApiClient.getPersonalAccessToken(eq(bitbucketPersonalAccessToken.getId())))
|
when(bitbucketServerApiClient.getPersonalAccessToken(
|
||||||
|
eq(bitbucketPersonalAccessToken.getId()), eq(bitbucketPersonalAccessToken.getToken())))
|
||||||
.thenReturn(bitbucketPersonalAccessToken);
|
.thenReturn(bitbucketPersonalAccessToken);
|
||||||
// when
|
// when
|
||||||
Optional<Pair<Boolean, String>> result = fetcher.isValid(personalAccessTokenParams);
|
Optional<Pair<Boolean, String>> result = fetcher.isValid(personalAccessTokenParams);
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
/*
|
/*
|
||||||
* Copyright (c) 2012-2023 Red Hat, Inc.
|
* Copyright (c) 2012-2024 Red Hat, Inc.
|
||||||
* This program and the accompanying materials are made
|
* This program and the accompanying materials are made
|
||||||
* available under the terms of the Eclipse Public License 2.0
|
* available under the terms of the Eclipse Public License 2.0
|
||||||
* which is available at https://www.eclipse.org/legal/epl-2.0/
|
* which is available at https://www.eclipse.org/legal/epl-2.0/
|
||||||
|
|
@ -312,14 +312,14 @@ public class HttpBitbucketServerApiClientTest {
|
||||||
// given
|
// given
|
||||||
stubFor(
|
stubFor(
|
||||||
get(urlPathEqualTo("/rest/access-tokens/1.0/users/ksmster/5"))
|
get(urlPathEqualTo("/rest/access-tokens/1.0/users/ksmster/5"))
|
||||||
.withHeader(HttpHeaders.AUTHORIZATION, equalTo(AUTHORIZATION_TOKEN))
|
.withHeader(HttpHeaders.AUTHORIZATION, equalTo("Bearer token"))
|
||||||
.withHeader(HttpHeaders.ACCEPT, equalTo(MediaType.APPLICATION_JSON))
|
.withHeader(HttpHeaders.ACCEPT, equalTo(MediaType.APPLICATION_JSON))
|
||||||
.willReturn(
|
.willReturn(
|
||||||
ok().withBodyFile("bitbucket/rest/access-tokens/1.0/users/ksmster/newtoken.json")));
|
ok().withBodyFile("bitbucket/rest/access-tokens/1.0/users/ksmster/newtoken.json")));
|
||||||
|
|
||||||
stubFor(
|
stubFor(
|
||||||
get(urlPathEqualTo("/rest/api/1.0/users"))
|
get(urlPathEqualTo("/rest/api/1.0/users"))
|
||||||
.withHeader(HttpHeaders.AUTHORIZATION, equalTo(AUTHORIZATION_TOKEN))
|
.withHeader(HttpHeaders.AUTHORIZATION, equalTo("Bearer token"))
|
||||||
.withQueryParam("start", equalTo("0"))
|
.withQueryParam("start", equalTo("0"))
|
||||||
.withQueryParam("limit", equalTo("25"))
|
.withQueryParam("limit", equalTo("25"))
|
||||||
.willReturn(
|
.willReturn(
|
||||||
|
|
@ -328,7 +328,7 @@ public class HttpBitbucketServerApiClientTest {
|
||||||
.withBodyFile("bitbucket/rest/api/1.0/users/filtered/response.json")));
|
.withBodyFile("bitbucket/rest/api/1.0/users/filtered/response.json")));
|
||||||
|
|
||||||
// when
|
// when
|
||||||
BitbucketPersonalAccessToken result = bitbucketServer.getPersonalAccessToken("5");
|
BitbucketPersonalAccessToken result = bitbucketServer.getPersonalAccessToken("5", "token");
|
||||||
// then
|
// then
|
||||||
assertNotNull(result);
|
assertNotNull(result);
|
||||||
assertEquals(result.getToken(), "MTU4OTEwNTMyOTA5Ohc88HcY8k7gWOzl2mP5TtdtY5Qs");
|
assertEquals(result.getToken(), "MTU4OTEwNTMyOTA5Ohc88HcY8k7gWOzl2mP5TtdtY5Qs");
|
||||||
|
|
@ -346,7 +346,7 @@ public class HttpBitbucketServerApiClientTest {
|
||||||
.willReturn(notFound()));
|
.willReturn(notFound()));
|
||||||
|
|
||||||
// when
|
// when
|
||||||
bitbucketServer.getPersonalAccessToken("5");
|
bitbucketServer.getPersonalAccessToken("5", "token");
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test(expectedExceptions = ScmUnauthorizedException.class)
|
@Test(expectedExceptions = ScmUnauthorizedException.class)
|
||||||
|
|
@ -361,18 +361,18 @@ public class HttpBitbucketServerApiClientTest {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test(expectedExceptions = ScmUnauthorizedException.class)
|
@Test(expectedExceptions = ScmUnauthorizedException.class)
|
||||||
public void shouldBeAbleToThrowScmUnauthorizedExceptionOnGePAT()
|
public void shouldBeAbleToThrowScmUnauthorizedExceptionOnGetPAT()
|
||||||
throws ScmCommunicationException, ScmUnauthorizedException, ScmItemNotFoundException {
|
throws ScmCommunicationException, ScmUnauthorizedException, ScmItemNotFoundException {
|
||||||
|
|
||||||
// given
|
// given
|
||||||
stubFor(
|
stubFor(
|
||||||
get(urlPathEqualTo("/rest/access-tokens/1.0/users/ksmster/5"))
|
get(urlPathEqualTo("/rest/access-tokens/1.0/users/ksmster/5"))
|
||||||
.withHeader(HttpHeaders.AUTHORIZATION, equalTo(AUTHORIZATION_TOKEN))
|
.withHeader(HttpHeaders.AUTHORIZATION, equalTo("Bearer token"))
|
||||||
.withHeader(HttpHeaders.ACCEPT, equalTo(MediaType.APPLICATION_JSON))
|
.withHeader(HttpHeaders.ACCEPT, equalTo(MediaType.APPLICATION_JSON))
|
||||||
.willReturn(unauthorized()));
|
.willReturn(unauthorized()));
|
||||||
stubFor(
|
stubFor(
|
||||||
get(urlPathEqualTo("/rest/api/1.0/users"))
|
get(urlPathEqualTo("/rest/api/1.0/users"))
|
||||||
.withHeader(HttpHeaders.AUTHORIZATION, equalTo(AUTHORIZATION_TOKEN))
|
.withHeader(HttpHeaders.AUTHORIZATION, equalTo("Bearer token"))
|
||||||
.withQueryParam("start", equalTo("0"))
|
.withQueryParam("start", equalTo("0"))
|
||||||
.withQueryParam("limit", equalTo("25"))
|
.withQueryParam("limit", equalTo("25"))
|
||||||
.willReturn(
|
.willReturn(
|
||||||
|
|
@ -381,7 +381,7 @@ public class HttpBitbucketServerApiClientTest {
|
||||||
.withBodyFile("bitbucket/rest/api/1.0/users/filtered/response.json")));
|
.withBodyFile("bitbucket/rest/api/1.0/users/filtered/response.json")));
|
||||||
|
|
||||||
// when
|
// when
|
||||||
bitbucketServer.getPersonalAccessToken("5");
|
bitbucketServer.getPersonalAccessToken("5", "token");
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test(
|
@Test(
|
||||||
|
|
@ -400,7 +400,7 @@ public class HttpBitbucketServerApiClientTest {
|
||||||
wireMockServer.url("/"), new NoopOAuthAuthenticator(), oAuthAPI, apiEndpoint);
|
wireMockServer.url("/"), new NoopOAuthAuthenticator(), oAuthAPI, apiEndpoint);
|
||||||
|
|
||||||
// when
|
// when
|
||||||
localServer.getPersonalAccessToken("5");
|
localServer.getUser();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue