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

Support Basic authentication for devfile factory URL #451

Merged
merged 5 commits into from
Feb 23, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
Original file line number Diff line number Diff line change
Expand Up @@ -169,7 +169,7 @@ public BitbucketServerUrl parse(String url) {
format(
"The given url %s is not a valid Bitbucket server URL. Check either URL or server configuration.",
url)));
return parse(matcher);
return parse(matcher).withUrl(url);
}

private BitbucketServerUrl parse(Matcher matcher) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,10 +19,10 @@
import java.util.Optional;
import java.util.StringJoiner;
import java.util.stream.Collectors;
import org.eclipse.che.api.factory.server.urlfactory.RemoteFactoryUrl;
import org.eclipse.che.api.factory.server.urlfactory.DefaultFactoryUrl;

/** Representation of a bitbucket Server URL, allowing to get details from it. */
public class BitbucketServerUrl implements RemoteFactoryUrl {
public class BitbucketServerUrl extends DefaultFactoryUrl {

private final String NAME = "bitbucket";

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@ public BitbucketUrl parse(String url) {
.withRepository(repoName)
.withBranch(branchName)
.withWorkspaceId(workspaceId)
.withDevfileFilenames(devfileFilenamesProvider.getConfiguredDevfileFilenames());
.withDevfileFilenames(devfileFilenamesProvider.getConfiguredDevfileFilenames())
.withUrl(url);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -19,14 +19,14 @@
import java.util.Optional;
import java.util.StringJoiner;
import java.util.stream.Collectors;
import org.eclipse.che.api.factory.server.urlfactory.RemoteFactoryUrl;
import org.eclipse.che.api.factory.server.urlfactory.DefaultFactoryUrl;

/**
* Representation of a bitbucket URL, allowing to get details from it.
*
* <p>like https://<your_username>@bitbucket.org/<workspace_ID>/<repo_name>.git
*/
public class BitbucketUrl implements RemoteFactoryUrl {
public class BitbucketUrl extends DefaultFactoryUrl {

private final String NAME = "bitbucket";

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -174,7 +174,8 @@ private GithubUrl parse(String url, boolean authenticationRequired) throws ApiEx
.withBranch(branchName)
.withLatestCommit(latestCommit)
.withSubfolder(matcher.group("subFolder"))
.withDevfileFilenames(devfileFilenamesProvider.getConfiguredDevfileFilenames());
.withDevfileFilenames(devfileFilenamesProvider.getConfiguredDevfileFilenames())
.withUrl(url);
}

private GithubPullRequest getPullRequest(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@
import java.util.Optional;
import java.util.StringJoiner;
import java.util.stream.Collectors;
import org.eclipse.che.api.factory.server.urlfactory.RemoteFactoryUrl;
import org.eclipse.che.api.factory.server.urlfactory.DefaultFactoryUrl;

/**
* Representation of a github URL, allowing to get details from it.
Expand All @@ -28,7 +28,7 @@
*
* @author Florent Benoit
*/
public class GithubUrl implements RemoteFactoryUrl {
public class GithubUrl extends DefaultFactoryUrl {

private final String NAME = "github";

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@
import java.util.Optional;
import java.util.StringJoiner;
import java.util.stream.Collectors;
import org.eclipse.che.api.factory.server.urlfactory.RemoteFactoryUrl;
import org.eclipse.che.api.factory.server.urlfactory.DefaultFactoryUrl;

/**
* Representation of a gitlab URL, allowing to get details from it.
Expand All @@ -30,7 +30,7 @@
*
* @author Max Shaposhnyk
*/
public class GitlabUrl implements RemoteFactoryUrl {
public class GitlabUrl extends DefaultFactoryUrl {

private final String NAME = "gitlab";

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -153,7 +153,7 @@ public GitlabUrl parse(String url) {
.findFirst()
.or(() -> getPatternMatcherByUrl(url));
if (matcherOptional.isPresent()) {
return parse(matcherOptional.get());
return parse(matcherOptional.get()).withUrl(url);
} else {
throw new UnsupportedOperationException(
"The gitlab integration is not configured properly and cannot be used at this moment."
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,9 @@ public FactoryMetaDto createFactory(@NotNull final Map<String, String> factoryPa
}
return urlFactoryBuilder
.createFactoryFromDevfile(
new DefaultFactoryUrl().withDevfileFileLocation(devfileLocation),
new DefaultFactoryUrl()
.withDevfileFileLocation(devfileLocation)
.withUrl(devfileLocation),
new URLFileContentProvider(devfileURI, urlFetcher),
extractOverrideParams(factoryParameters),
false)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,13 @@
*/
package org.eclipse.che.api.factory.server.scm;

import static com.google.common.base.Strings.isNullOrEmpty;

import java.io.FileNotFoundException;
import java.io.IOException;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.Base64;
import javax.net.ssl.SSLException;
import org.eclipse.che.api.factory.server.scm.exception.ScmCommunicationException;
import org.eclipse.che.api.factory.server.scm.exception.ScmConfigurationPersistenceException;
Expand All @@ -25,6 +28,7 @@
import org.eclipse.che.api.workspace.server.devfile.FileContentProvider;
import org.eclipse.che.api.workspace.server.devfile.URLFetcher;
import org.eclipse.che.api.workspace.server.devfile.exception.DevfileException;
import org.eclipse.che.commons.annotation.Nullable;

/**
* Common implementation of file content provider which is able to access content of private
Expand All @@ -48,26 +52,41 @@ public AuthorizingFileContentProvider(

@Override
public String fetchContent(String fileURL) throws IOException, DevfileException {
return fetchContent(fileURL, false);
return fetchContent(fileURL, false, null);
}

@Override
public String fetchContent(String fileURL, String credentials)
throws IOException, DevfileException {
return fetchContent(fileURL, false, credentials);
}
Comment on lines +59 to 62
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can we use Optional here also?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There is no need to use Optional or Nullable here. This is an overloaded method which is supposed to receive non null parameters.


@Override
public String fetchContentWithoutAuthentication(String fileURL)
throws IOException, DevfileException {
return fetchContent(fileURL, true);
return fetchContent(fileURL, true, null);
}

protected String fetchContent(String fileURL, boolean skipAuthentication)
private String fetchContent(
String fileURL, boolean skipAuthentication, @Nullable String credentials)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

would be nice to use Optional here also

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think that passing Optional as a parameter is a good idea: https://stackoverflow.com/questions/31922866/why-should-java-8s-optional-not-be-used-in-arguments
This is a private method so I don't think it will affect something.

throws IOException, DevfileException {
final String requestURL = formatUrl(fileURL);
try {
if (skipAuthentication) {
return urlFetcher.fetch(requestURL);
} else {
// try to authenticate for the given URL
PersonalAccessToken token =
personalAccessTokenManager.getAndStore(remoteFactoryUrl.getHostName());
return urlFetcher.fetch(requestURL, formatAuthorization(token.getToken()));
String authorization;
if (isNullOrEmpty(credentials)) {
authorization =
formatAuthorization(
personalAccessTokenManager
.getAndStore(remoteFactoryUrl.getHostName())
.getToken());
} else {
authorization = getCredentialsAuthorization(credentials);
}
return urlFetcher.fetch(requestURL, authorization);
}
} catch (UnknownScmProviderException e) {
return fetchContentWithoutToken(requestURL, e);
Expand Down Expand Up @@ -143,4 +162,8 @@ protected String formatUrl(String fileURL) throws DevfileException {
protected String formatAuthorization(String token) {
return "Bearer " + token;
}

private String getCredentialsAuthorization(String credentials) {
return "Basic " + new String(Base64.getEncoder().encode(credentials.getBytes()));
}
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright (c) 2012-2021 Red Hat, Inc.
* Copyright (c) 2012-2023 Red Hat, Inc.
* This program and the accompanying materials are made
* available under the terms of the Eclipse Public License 2.0
* which is available at https://www.eclipse.org/legal/epl-2.0/
Expand All @@ -11,9 +11,13 @@
*/
package org.eclipse.che.api.factory.server.urlfactory;

import static com.google.common.base.Strings.isNullOrEmpty;
import static java.lang.String.format;
import static java.util.Collections.singletonList;

import java.net.MalformedURLException;
import java.net.URI;
import java.net.URL;
import java.util.List;
import java.util.Optional;

Expand All @@ -24,6 +28,7 @@
public class DefaultFactoryUrl implements RemoteFactoryUrl {

private String devfileFileLocation;
private URL url;

@Override
public String getProviderName() {
Expand Down Expand Up @@ -61,6 +66,33 @@ public String getBranch() {
return null;
}

@Override
public Optional<String> getCredentials() {
if (url == null || isNullOrEmpty(url.getUserInfo())) {
return Optional.empty();
}
String userInfo = url.getUserInfo();
String[] credentials = userInfo.split(":");
String username = credentials[0];
String password = credentials.length == 2 ? credentials[1] : null;
if (!isNullOrEmpty(username) || !isNullOrEmpty(password)) {
return Optional.of(
format(
"%s:%s",
isNullOrEmpty(username) ? "" : username, isNullOrEmpty(password) ? "" : password));
}
Comment on lines +78 to +83
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm a bit lost, can we really handle a case from auth perspective if either username or password is null / blank? I thought we need both, no?

Copy link
Member

@ibuziuk ibuziuk Feb 22, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ok, this is probably for token: case

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actually we don't need both parts. If we have only username, we pass the credentials as username:. From the authentication prospective this case is considered as a token. I agree that it looks a bit weird, but it coms from the curl behaviour:

curl https://<token>@dev.azure.com/vinokurig/test/_apis/git/repositories/test/items?path=%2Fdevfile.yaml -v                                                                                        
*   Trying 185.199.109.133:443...
* Connected to raw.githubusercontent.com (185.199.109.133) port 443 (#0)
* ALPN: offers h2
* ALPN: offers http/1.1
*  CAfile: /etc/ssl/cert.pem
*  CApath: none
* (304) (OUT), TLS handshake, Client hello (1):
* (304) (IN), TLS handshake, Server hello (2):
* (304) (IN), TLS handshake, Unknown (8):
* (304) (IN), TLS handshake, Certificate (11):
* (304) (IN), TLS handshake, CERT verify (15):
* (304) (IN), TLS handshake, Finished (20):
* (304) (OUT), TLS handshake, Finished (20):
* SSL connection using TLSv1.3 / AEAD-AES128-GCM-SHA256
* ALPN: server accepted h2
* Server certificate:
*  subject: C=US; ST=California; L=San Francisco; O=GitHub, Inc.; CN=*.github.io
*  start date: Feb 21 00:00:00 2023 GMT
*  expire date: Mar 20 23:59:59 2024 GMT
*  subjectAltName: host "raw.githubusercontent.com" matched cert's "*.githubusercontent.com"
*  issuer: C=US; O=DigiCert Inc; CN=DigiCert TLS RSA SHA256 2020 CA1
*  SSL certificate verify ok.
* Using HTTP2, server supports multiplexing
* Copying HTTP/2 data in stream buffer to connection buffer after upgrade: len=0
* Server auth using Basic with user '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'
* h2h3 [:method: GET]
* h2h3 [:path: /vinokurig/private/master/devfile.yaml]
* h2h3 [:scheme: https]
* h2h3 [:authority: raw.githubusercontent.com]
* h2h3 [authorization: Basic <token plus : in Base 64> ]

So curl https://<token>@dev.azure.com/... equals curl https://dev.azure.com/... -H 'Authorization: Basic <token + : in base64>'

return Optional.empty();
}

public <U extends DefaultFactoryUrl> U withUrl(String url) {
try {
this.url = new URL(url);
} catch (MalformedURLException e) {
// Do nothing, wrong URL.
}
return (U) this;
}

public DefaultFactoryUrl withDevfileFileLocation(String devfileFileLocation) {
this.devfileFileLocation = devfileFileLocation;
return this;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright (c) 2012-2021 Red Hat, Inc.
* Copyright (c) 2012-2023 Red Hat, Inc.
* This program and the accompanying materials are made
* available under the terms of the Eclipse Public License 2.0
* which is available at https://www.eclipse.org/legal/epl-2.0/
Expand Down Expand Up @@ -42,6 +42,9 @@ public interface RemoteFactoryUrl {
/** Remote branch */
String getBranch();

/** Optional of credentials in format <username>:<password> or <username>: */
Optional<String> getCredentials();

/** Describes devfile location, including filename if any. */
interface DevfileLocation {
Optional<String> filename();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -111,17 +111,22 @@ public Optional<FactoryMetaDto> createFactoryFromDevfile(
}

for (DevfileLocation location : remoteFactoryUrl.devfileFileLocations()) {
String devfileLocation = location.location();
try {
devfileYamlContent =
skipAuthentication
? fileContentProvider.fetchContentWithoutAuthentication(location.location())
: fileContentProvider.fetchContent(location.location());
Optional<String> credentialsOptional = remoteFactoryUrl.getCredentials();
if (skipAuthentication) {
devfileYamlContent =
fileContentProvider.fetchContentWithoutAuthentication(devfileLocation);
} else if (credentialsOptional.isPresent()) {
devfileYamlContent =
fileContentProvider.fetchContent(devfileLocation, credentialsOptional.get());
} else {
devfileYamlContent = fileContentProvider.fetchContent(devfileLocation);
}
} catch (IOException ex) {
// try next location
LOG.debug(
"Unreachable devfile location met: {}. Error is: {}",
location.location(),
ex.getMessage());
"Unreachable devfile location met: {}. Error is: {}", devfileLocation, ex.getMessage());
continue;
} catch (DevfileException e) {
LOG.debug("Unexpected devfile exception: {}", e.getMessage());
Expand Down Expand Up @@ -182,7 +187,7 @@ private FactoryMetaDto createFactory(
/**
* Creates devfile with only `generateName` and no `name`. We take `generateName` with precedence.
* See doc of {@link URLFactoryBuilder#createFactoryFromDevfile(RemoteFactoryUrl,
* FileContentProvider, Map, Boolean)} for explanation why.
* FileContentProvider, Map, boolean)} for explanation why.
*/
private DevfileImpl ensureToUseGenerateName(DevfileImpl devfile) {
MetadataImpl devfileMetadata = new MetadataImpl(devfile.getMetadata());
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -91,14 +91,14 @@ public void shouldResolveRelativeFiles() throws Exception {
// set up our factory with the location of our devfile that is referencing our localfile
Map<String, String> factoryParameters = new HashMap<>();
factoryParameters.put(URL_PARAMETER_NAME, "http://myloc.com/aa/bb/devfile");
doReturn(DEVFILE).when(urlFetcher).fetch(eq("http://myloc.com/aa/bb/devfile"));
doReturn("localfile").when(urlFetcher).fetch("http://myloc.com/aa/localfile");
doReturn(DEVFILE).when(urlFetcher).fetch(eq("http://myloc.com/aa/bb/devfile"), eq(null));
doReturn("localfile").when(urlFetcher).fetch("http://myloc.com/aa/localfile", null);

// when
res.createFactory(factoryParameters);

// then
verify(urlFetcher).fetch(eq("http://myloc.com/aa/localfile"));
verify(urlFetcher).fetch(eq("http://myloc.com/aa/localfile"), eq(null));
}

@Test
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
/*
* Copyright (c) 2012-2023 Red Hat, Inc.
* This program and the accompanying materials are made
* available under the terms of the Eclipse Public License 2.0
* which is available at https://www.eclipse.org/legal/epl-2.0/
*
* SPDX-License-Identifier: EPL-2.0
*
* Contributors:
* Red Hat, Inc. - initial API and implementation
*/
package org.eclipse.che.api.factory.server.urlfactory;

import static org.testng.Assert.assertEquals;
import static org.testng.Assert.assertFalse;
import static org.testng.Assert.assertTrue;

import java.util.Optional;
import org.mockito.testng.MockitoTestNGListener;
import org.testng.annotations.DataProvider;
import org.testng.annotations.Listeners;
import org.testng.annotations.Test;

/** Testing {@link DefaultFactoryUrl} */
@Listeners(MockitoTestNGListener.class)
public class DefaultFactoryUrlTest {
@Test(dataProvider = "urlsProvider")
public void shouldGetCredentials(String url, String credentials) {
// given
DefaultFactoryUrl factoryUrl = new DefaultFactoryUrl().withUrl(url);
// when
Optional<String> credentialsOptional = factoryUrl.getCredentials();
// then
assertTrue(credentialsOptional.isPresent());
assertEquals(credentialsOptional.get(), credentials);
}

@DataProvider(name = "urlsProvider")
private Object[][] urlsProvider() {
return new Object[][] {
{"https://username:password@hostname/path", "username:password"},
{"https://token@hostname/path/user/repo/", "token:"},
{"http://token@hostname/path/user/repo/", "token:"},
{"https://token@dev.azure.com/user/repo/", "token:"},
vinokurig marked this conversation as resolved.
Show resolved Hide resolved
{"https://token@dev.azure.com/user/repo?a=&b=b&c=/.devfile.yaml&api-version=7.0", "token:"},
{"https://token@gitlub.com/user/repo/", "token:"},
{"https://token@bitbucket.org/user/repo/", "token:"},
{
"https://personal-access-token@raw.githubusercontent.com/user/repo/branch/.devfile.yaml",
"personal-access-token:"
}
};
}

@Test
public void shouldGetEmptyCredentials() {
// given
DefaultFactoryUrl factoryUrl = new DefaultFactoryUrl().withUrl("https://hostname/path");
// when
Optional<String> credentialsOptional = factoryUrl.getCredentials();
// then
assertFalse(credentialsOptional.isPresent());
}
}
Loading