Skip to content

Commit

Permalink
Add Repository Bearer Authentication
Browse files Browse the repository at this point in the history
  • Loading branch information
valentijnscholten committed Dec 20, 2024
1 parent ee5cbce commit ea83a6d
Show file tree
Hide file tree
Showing 14 changed files with 166 additions and 69 deletions.
14 changes: 14 additions & 0 deletions src/main/java/org/dependencytrack/model/Repository.java
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,10 @@ public class Repository implements Serializable {
@Column(name = "PASSWORD")
private String password;

@Persistent
@Column(name = "BEARERTOKEN")
private String bearerToken;

@Persistent(customValueStrategy = "uuid")
@Index(name = "REPOSITORY_UUID_IDX") // Cannot be @Unique. Microsoft SQL Server throws an exception
@Column(name = "UUID", jdbcType = "VARCHAR", length = 36, allowsNull = "true")
Expand Down Expand Up @@ -189,6 +193,16 @@ public void setPassword(String password) {
this.password = password;
}

@JsonIgnore
public String getBearerToken() {
return bearerToken;
}

@JsonProperty(value = "bearerToken")
public void setBearerToken(String bearerToken) {
this.bearerToken = bearerToken;
}

public UUID getUuid() {
return uuid;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -214,23 +214,23 @@ private List<Permission> getBadgesPermissions(final List<Permission> fullList) {
public void loadDefaultRepositories() {
try (QueryManager qm = new QueryManager()) {
LOGGER.info("Synchronizing default repositories to datastore");
qm.createRepository(RepositoryType.CPAN, "cpan-public-registry", "https://fastapi.metacpan.org/v1/", true, false, false, null, null);
qm.createRepository(RepositoryType.GEM, "rubygems.org", "https://rubygems.org/", true, false, false, null, null);
qm.createRepository(RepositoryType.HEX, "hex.pm", "https://hex.pm/", true, false, false, null, null);
qm.createRepository(RepositoryType.HACKAGE, "hackage.haskell.org", "https://hackage.haskell.org/", true, false, false, null, null);
qm.createRepository(RepositoryType.MAVEN, "central", "https://repo1.maven.org/maven2/", true, false, false, null, null);
qm.createRepository(RepositoryType.MAVEN, "atlassian-public", "https://packages.atlassian.com/content/repositories/atlassian-public/", true, false, false, null, null);
qm.createRepository(RepositoryType.MAVEN, "jboss-releases", "https://repository.jboss.org/nexus/content/repositories/releases/", true, false, false, null, null);
qm.createRepository(RepositoryType.MAVEN, "clojars", "https://repo.clojars.org/", true, false, false, null, null);
qm.createRepository(RepositoryType.MAVEN, "google-android", "https://maven.google.com/", true, false, false, null, null);
qm.createRepository(RepositoryType.NIXPKGS, "nixpkgs-unstable", "https://channels.nixos.org/nixpkgs-unstable/packages.json.br", true, false, false, null, null);
qm.createRepository(RepositoryType.NPM, "npm-public-registry", "https://registry.npmjs.org/", true, false, false, null, null);
qm.createRepository(RepositoryType.PYPI, "pypi.org", "https://pypi.org/", true, false, false, null, null);
qm.createRepository(RepositoryType.NUGET, "nuget-gallery", "https://api.nuget.org/", true, false, false, null, null);
qm.createRepository(RepositoryType.COMPOSER, "packagist", "https://repo.packagist.org/", true, false, false, null, null);
qm.createRepository(RepositoryType.CARGO, "crates.io", "https://crates.io", true, false, false, null, null);
qm.createRepository(RepositoryType.GO_MODULES, "proxy.golang.org", "https://proxy.golang.org", true, false, false, null, null);
qm.createRepository(RepositoryType.GITHUB, "github.com", "https://github.com", true, false, false, null, null);
qm.createRepository(RepositoryType.CPAN, "cpan-public-registry", "https://fastapi.metacpan.org/v1/", true, false, false, null, null, null);
qm.createRepository(RepositoryType.GEM, "rubygems.org", "https://rubygems.org/", true, false, false, null, null, null);
qm.createRepository(RepositoryType.HEX, "hex.pm", "https://hex.pm/", true, false, false, null, null, null);
qm.createRepository(RepositoryType.HACKAGE, "hackage.haskell.org", "https://hackage.haskell.org/", true, false, false, null, null, null);
qm.createRepository(RepositoryType.MAVEN, "central", "https://repo1.maven.org/maven2/", true, false, false, null, null, null);
qm.createRepository(RepositoryType.MAVEN, "atlassian-public", "https://packages.atlassian.com/content/repositories/atlassian-public/", true, false, false, null, null, null);
qm.createRepository(RepositoryType.MAVEN, "jboss-releases", "https://repository.jboss.org/nexus/content/repositories/releases/", true, false, false, null, null, null);
qm.createRepository(RepositoryType.MAVEN, "clojars", "https://repo.clojars.org/", true, false, false, null, null, null);
qm.createRepository(RepositoryType.MAVEN, "google-android", "https://maven.google.com/", true, false, false, null, null, null);
qm.createRepository(RepositoryType.NIXPKGS, "nixpkgs-unstable", "https://channels.nixos.org/nixpkgs-unstable/packages.json.br", true, false, false, null, null, null);
qm.createRepository(RepositoryType.NPM, "npm-public-registry", "https://registry.npmjs.org/", true, false, false, null, null, null);
qm.createRepository(RepositoryType.PYPI, "pypi.org", "https://pypi.org/", true, false, false, null, null, null);
qm.createRepository(RepositoryType.NUGET, "nuget-gallery", "https://api.nuget.org/", true, false, false, null, null, null);
qm.createRepository(RepositoryType.COMPOSER, "packagist", "https://repo.packagist.org/", true, false, false, null, null, null);
qm.createRepository(RepositoryType.CARGO, "crates.io", "https://crates.io", true, false, false, null, null, null);
qm.createRepository(RepositoryType.GO_MODULES, "proxy.golang.org", "https://proxy.golang.org", true, false, false, null, null, null);
qm.createRepository(RepositoryType.GITHUB, "github.com", "https://github.com", true, false, false, null, null, null);
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1237,12 +1237,12 @@ public boolean repositoryExist(RepositoryType type, String identifier) {
return getRepositoryQueryManager().repositoryExist(type, identifier);
}

public Repository createRepository(RepositoryType type, String identifier, String url, boolean enabled, boolean internal, boolean isAuthenticationRequired, String username, String password) {
return getRepositoryQueryManager().createRepository(type, identifier, url, enabled, internal, isAuthenticationRequired, username, password);
public Repository createRepository(RepositoryType type, String identifier, String url, boolean enabled, boolean internal, boolean isAuthenticationRequired, String username, String password, String bearerToken) {
return getRepositoryQueryManager().createRepository(type, identifier, url, enabled, internal, isAuthenticationRequired, username, password, bearerToken);
}

public Repository updateRepository(UUID uuid, String identifier, String url, boolean internal, boolean authenticationRequired, String username, String password, boolean enabled) {
return getRepositoryQueryManager().updateRepository(uuid, identifier, url, internal, authenticationRequired, username, password, enabled);
public Repository updateRepository(UUID uuid, String identifier, String url, boolean internal, boolean authenticationRequired, String username, String password, String bearerToken, boolean enabled) {
return getRepositoryQueryManager().updateRepository(uuid, identifier, url, internal, authenticationRequired, username, password, bearerToken, enabled);
}

public RepositoryMetaComponent getRepositoryMetaComponent(RepositoryType repositoryType, String namespace, String name) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -143,9 +143,10 @@ public boolean repositoryExist(RepositoryType type, String identifier) {
* @param isAuthenticationRequired if the repository needs authentication or not
* @param username the username to access the (authenticated) repository with
* @param password the password to access the (authenticated) repository with
* @param bearerToken the token to access the (authenticated) repository with
* @return the created Repository
*/
public Repository createRepository(RepositoryType type, String identifier, String url, boolean enabled, boolean internal, boolean isAuthenticationRequired, String username, String password) {
public Repository createRepository(RepositoryType type, String identifier, String url, boolean enabled, boolean internal, boolean isAuthenticationRequired, String username, String password, String bearerToken) {
if (repositoryExist(type, identifier)) {
return null;
}
Expand All @@ -166,15 +167,21 @@ public Repository createRepository(RepositoryType type, String identifier, Strin
repo.setEnabled(enabled);
repo.setInternal(internal);
repo.setAuthenticationRequired(isAuthenticationRequired);
if (Boolean.TRUE.equals(isAuthenticationRequired) && (username != null || password != null)) {
if (Boolean.TRUE.equals(isAuthenticationRequired) && (username != null || password != null || bearerToken != null)) {
repo.setUsername(StringUtils.trimToNull(username));
String msg = "password";
try {
if (password != null) {
repo.setPassword(DataEncryption.encryptAsString(password));
}
msg = "bearerToken";
if (bearerToken != null) {
repo.setBearerToken(DataEncryption.encryptAsString(bearerToken));
}
} catch (Exception e) {
LOGGER.error("An error occurred while saving password in encrypted state");
LOGGER.error("An error occurred while saving %s in encrypted state".formatted(msg));
}

}
return persist(repo);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -198,7 +198,8 @@ public Response createRepository(Repository jsonRepository) {
jsonRepository.isEnabled(),
jsonRepository.isInternal(),
jsonRepository.isAuthenticationRequired(),
jsonRepository.getUsername(), jsonRepository.getPassword());
jsonRepository.getUsername(), jsonRepository.getPassword(),
jsonRepository.getBearerToken());

return Response.status(Response.Status.CREATED).entity(repository).build();
} else {
Expand Down Expand Up @@ -240,8 +241,13 @@ public Response updateRepository(Repository jsonRepository) {
? DataEncryption.encryptAsString(jsonRepository.getPassword())
: repository.getPassword();

// The bearerToken is not passed to the front-end, so it should only be overwritten if it is not null or not set to default value coming from ui
final String updatedBearerToken = jsonRepository.getBearerToken()!=null && !jsonRepository.getBearerToken().equals(ENCRYPTED_PLACEHOLDER)
? DataEncryption.encryptAsString(jsonRepository.getBearerToken())
: repository.getBearerToken();

repository = qm.updateRepository(jsonRepository.getUuid(), repository.getIdentifier(), url,
jsonRepository.isInternal(), jsonRepository.isAuthenticationRequired(), jsonRepository.getUsername(), updatedPassword, jsonRepository.isEnabled());
jsonRepository.isInternal(), jsonRepository.isAuthenticationRequired(), jsonRepository.getUsername(), updatedPassword, updatedBearerToken, jsonRepository.isEnabled());
return Response.ok(repository).build();
} catch (Exception e) {
return Response.status(Response.Status.INTERNAL_SERVER_ERROR).entity("The specified repository password could not be encrypted.").build();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,8 @@ public abstract class AbstractMetaAnalyzer implements IMetaAnalyzer {

protected String password;

protected String bearerToken;

/**
* {@inheritDoc}
*/
Expand All @@ -66,9 +68,10 @@ public void setRepositoryBaseUrl(String baseUrl) {
this.baseUrl = baseUrl;
}

public void setRepositoryUsernameAndPassword(String username, String password) {
public void setCredentials(String username, String password, String bearerToken) {
this.username = StringUtils.trimToNull(username);
this.password = StringUtils.trimToNull(password);
this.bearerToken = StringUtils.trimToNull(bearerToken);
}

protected String urlEncode(final String value) {
Expand Down Expand Up @@ -105,8 +108,8 @@ protected CloseableHttpResponse processHttpRequest(String url) throws IOExceptio
URIBuilder uriBuilder = new URIBuilder(url);
final HttpUriRequest request = new HttpGet(uriBuilder.build().toString());
request.addHeader("accept", "application/json");
if (username != null || password != null) {
request.addHeader("Authorization", HttpUtil.basicAuthHeaderValue(username, password));
if (username != null || password != null || bearerToken != null) {
request.addHeader("Authorization", HttpUtil.constructAuthHeaderValue(username, password, bearerToken));
}
return HttpClientPool.getClient().execute(request);
}catch (URISyntaxException ex){
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -66,15 +66,6 @@ public void setRepositoryBaseUrl(String baseUrl) {
this.repositoryUrl = baseUrl;
}

/**
* {@inheritDoc}
*/
@Override
public void setRepositoryUsernameAndPassword(String username, String password) {
this.repositoryUser = username;
this.repositoryPassword = password;
}

/**
* {@inheritDoc}
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,12 +41,13 @@ public interface IMetaAnalyzer {

/**
* Sets the username and password (or access token) to use for authentication with the repository. Should not be used for repositories that do not
* use Basic authentication.
* use Basic or Bearer authentication.
* @param username the username for access to the repository.
* @param password the password or access token to be used for the repository.
* @param bearerToken the password or access token to be used for the repository.
* @since 4.6.0
*/
void setRepositoryUsernameAndPassword(String username, String password);
void setCredentials(String username, String password, String bearerToken);

/**
* Returns the type of repositry the analyzer supports.
Expand Down Expand Up @@ -154,7 +155,7 @@ public void setRepositoryBaseUrl(String baseUrl) {
}

@Override
public void setRepositoryUsernameAndPassword(String username, String password) {
public void setCredentials(String username, String password, String bearerToken) {

}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -180,11 +180,16 @@ private void analyze(final QueryManager qm, final Component component, final IMe

if (Boolean.TRUE.equals(repository.isAuthenticationRequired())) {
try {
LOGGER.error("decrypting credentials");
String decryptedPassword = null;
String decryptedBearerToken = null;
if (repository.getBearerToken() != null) {
decryptedBearerToken = DebugDataEncryption.decryptAsString(repository.getBearerToken());
}
if (repository.getPassword() != null) {
decryptedPassword = DebugDataEncryption.decryptAsString(repository.getPassword());
}
analyzer.setRepositoryUsernameAndPassword(repository.getUsername(), decryptedPassword);
analyzer.setCredentials(repository.getUsername(), decryptedPassword, decryptedBearerToken);
} catch (Exception e) {
LOGGER.error("Failed decrypting password for repository: " + repository.getIdentifier(), e);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ class UpgradeItems {
UPGRADE_ITEMS.add(org.dependencytrack.upgrade.v4110.v4110Updater.class);
UPGRADE_ITEMS.add(org.dependencytrack.upgrade.v4120.v4120Updater.class);
UPGRADE_ITEMS.add(org.dependencytrack.upgrade.v4122.v4122Updater.class);
UPGRADE_ITEMS.add(org.dependencytrack.upgrade.v4130.v4130Updater.class);
}

static List<Class<? extends UpgradeItem>> getUpgradeItems() {
Expand Down
23 changes: 19 additions & 4 deletions src/main/java/org/dependencytrack/util/HttpUtil.java
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@
import java.util.Base64;
import java.util.Objects;

import org.apache.commons.lang3.StringUtils;

import static org.apache.http.HttpHeaders.AUTHORIZATION;

public final class HttpUtil {
Expand All @@ -31,15 +33,28 @@ public final class HttpUtil {
private HttpUtil() {
}

public static String basicAuthHeader(final String username, final String password) {
return AUTHORIZATION + ": " + basicAuthHeaderValue(username, password);
}

public static String basicAuthHeaderValue(final String username, final String password) {
return "Basic " +
Base64.getEncoder().encodeToString(
String.format("%s:%s", Objects.toString(username, ""), Objects.toString(password, ""))
.getBytes()
);
}

public static String basicAuthHeader(final String username, final String password) {
return AUTHORIZATION + ": " + basicAuthHeaderValue(username, password);
}

public static String bearerAuthHeaderValue(final String bearerToken) {
return "Bearer " + bearerToken;
}

public static String constructAuthHeaderValue(final String username, final String password, final String bearerToken) {
if (StringUtils.isNotBlank(bearerToken)) {
return bearerAuthHeaderValue(bearerToken);
} else {
return basicAuthHeaderValue(username, password);
}
}

}
Loading

0 comments on commit ea83a6d

Please sign in to comment.