Skip to content

Commit

Permalink
Support Basic authentication for devfile factory URL (#451)
Browse files Browse the repository at this point in the history
Add a parsing rule to detect credentials in factory URLs if it is in a format https://<username>:<pasword>@hostname.
Extract the credentials from factory URLs and pass them to the devfile content request.
  • Loading branch information
vinokurig committed Feb 27, 2023
1 parent f5856a7 commit d82a98a
Show file tree
Hide file tree
Showing 20 changed files with 226 additions and 49 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -163,7 +163,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);
}

@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)
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));
}
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://[email protected]/user/repo/", "token:"},
{"https://[email protected]/user/repo?a=&b=b&c=/.devfile.yaml&api-version=7.0", "token:"},
{"https://[email protected]/user/repo/", "token:"},
{"https://[email protected]/user/repo/", "token:"},
{
"https://[email protected]/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

0 comments on commit d82a98a

Please sign in to comment.