From 318cb5854062647939e2dff0d193eb1cbef53ec6 Mon Sep 17 00:00:00 2001 From: Sahiba Mittal Date: Tue, 1 Oct 2024 16:44:35 +0100 Subject: [PATCH] Enhance badge API to require authorization Co-Authored-By: SaberStrat <11158273+saberstrat@users.noreply.github.com> --- .../org/dependencytrack/auth/Permissions.java | 4 +- .../model/ConfigPropertyConstants.java | 2 +- .../persistence/DefaultObjectGenerator.java | 14 + .../resources/v1/BadgeResource.java | 222 ++++++-- src/main/resources/openapi-configuration.yaml | 4 + .../org/dependencytrack/ResourceTest.java | 1 + .../dependencytrack/auth/PermissionsTest.java | 51 +- .../DefaultObjectGeneratorTest.java | 2 +- .../resources/v1/BadgeResourceTest.java | 499 +++++++++++++++++- .../resources/v1/PermissionResourceTest.java | 2 +- 10 files changed, 700 insertions(+), 101 deletions(-) diff --git a/src/main/java/org/dependencytrack/auth/Permissions.java b/src/main/java/org/dependencytrack/auth/Permissions.java index 7bc72fb00..982eefe1a 100644 --- a/src/main/java/org/dependencytrack/auth/Permissions.java +++ b/src/main/java/org/dependencytrack/auth/Permissions.java @@ -62,7 +62,8 @@ public enum Permissions { POLICY_MANAGEMENT_UPDATE("Allows the modification of a policy"), POLICY_MANAGEMENT_DELETE("Allows the deletion of a policy"), TAG_MANAGEMENT("Allows the modification and deletion of tags"), - TAG_MANAGEMENT_DELETE("Allows the deletion of a tag"); + TAG_MANAGEMENT_DELETE("Allows the deletion of a tag"), + VIEW_BADGES("Provides the ability to view badges"); private final String description; @@ -112,6 +113,7 @@ public static class Constants { public static final String POLICY_MANAGEMENT_DELETE = "POLICY_MANAGEMENT_DELETE"; public static final String TAG_MANAGEMENT = "TAG_MANAGEMENT"; public static final String TAG_MANAGEMENT_DELETE = "TAG_MANAGEMENT_DELETE"; + public static final String VIEW_BADGES = "VIEW_BADGES"; } } diff --git a/src/main/java/org/dependencytrack/model/ConfigPropertyConstants.java b/src/main/java/org/dependencytrack/model/ConfigPropertyConstants.java index ca74fea14..3286f6745 100644 --- a/src/main/java/org/dependencytrack/model/ConfigPropertyConstants.java +++ b/src/main/java/org/dependencytrack/model/ConfigPropertyConstants.java @@ -30,7 +30,7 @@ public enum ConfigPropertyConstants { INTERNAL_CLUSTER_ID("internal", "cluster.id", UUID.randomUUID().toString(), PropertyType.STRING, "Unique identifier of the cluster", ConfigPropertyAccessMode.READ_ONLY), INTERNAL_DEFAULT_OBJECTS_VERSION("internal", "default.objects.version", null, PropertyType.STRING, "Version of the default objects in the database", ConfigPropertyAccessMode.READ_ONLY), GENERAL_BASE_URL("general", "base.url", null, PropertyType.URL, "URL used to construct links back to Dependency-Track from external systems", ConfigPropertyAccessMode.READ_WRITE), - GENERAL_BADGE_ENABLED("general", "badge.enabled", "false", PropertyType.BOOLEAN, "Flag to enable/disable SVG badge support from metrics", ConfigPropertyAccessMode.READ_WRITE), + GENERAL_BADGE_ENABLED("general", "badge.enabled", "false", PropertyType.BOOLEAN, "Flag to enable/disable unauthenticated access to SVG badge from metrics", ConfigPropertyAccessMode.READ_WRITE), EMAIL_SMTP_ENABLED("email", "smtp.enabled", "false", PropertyType.BOOLEAN, "Flag to enable/disable SMTP", ConfigPropertyAccessMode.READ_WRITE), EMAIL_SMTP_FROM_ADDR("email", "smtp.from.address", null, PropertyType.STRING, "The from email address to use to send output SMTP mail", ConfigPropertyAccessMode.READ_WRITE), EMAIL_SMTP_SERVER_HOSTNAME("email", "smtp.server.hostname", null, PropertyType.STRING, "The hostname or IP address of the SMTP mail server", ConfigPropertyAccessMode.READ_WRITE), diff --git a/src/main/java/org/dependencytrack/persistence/DefaultObjectGenerator.java b/src/main/java/org/dependencytrack/persistence/DefaultObjectGenerator.java index 3b910c9c5..a22673518 100644 --- a/src/main/java/org/dependencytrack/persistence/DefaultObjectGenerator.java +++ b/src/main/java/org/dependencytrack/persistence/DefaultObjectGenerator.java @@ -232,6 +232,8 @@ private void loadDefaultPersonas() { final Team managers = qm.createTeam("Portfolio Managers", false); LOGGER.debug("Creating team: Automation"); final Team automation = qm.createTeam("Automation", true); + LOGGER.debug("Creating team: Badge Viewers"); + final Team badges = qm.createTeam("Badge Viewers", true); final List fullList = qm.getPermissions(); @@ -239,10 +241,12 @@ private void loadDefaultPersonas() { sysadmins.setPermissions(fullList); managers.setPermissions(getPortfolioManagersPermissions(fullList)); automation.setPermissions(getAutomationPermissions(fullList)); + badges.setPermissions(getBadgesPermissions(fullList)); qm.persist(sysadmins); qm.persist(managers); qm.persist(automation); + qm.persist(badges); LOGGER.debug("Adding admin user to System Administrators"); qm.addUserToTeam(admin, sysadmins); @@ -279,6 +283,16 @@ private List getAutomationPermissions(final List fullLis return permissions; } + private List getBadgesPermissions(final List fullList) { + final List permissions = new ArrayList<>(); + for (final Permission permission : fullList) { + if (permission.getName().equals(Permissions.Constants.VIEW_BADGES)) { + permissions.add(permission); + } + } + return permissions; + } + /** * Loads the default repositories */ diff --git a/src/main/java/org/dependencytrack/resources/v1/BadgeResource.java b/src/main/java/org/dependencytrack/resources/v1/BadgeResource.java index 41e8db3aa..e68cd03ae 100644 --- a/src/main/java/org/dependencytrack/resources/v1/BadgeResource.java +++ b/src/main/java/org/dependencytrack/resources/v1/BadgeResource.java @@ -18,9 +18,18 @@ */ package org.dependencytrack.resources.v1; +import alpine.common.logging.Logger; import alpine.common.util.BooleanUtil; +import alpine.model.ApiKey; import alpine.model.ConfigProperty; +import alpine.model.LdapUser; +import alpine.model.ManagedUser; +import alpine.model.OidcUser; +import alpine.model.UserPrincipal; +import alpine.server.auth.ApiKeyAuthenticationService; import alpine.server.auth.AuthenticationNotRequired; +import alpine.server.auth.JwtAuthenticationService; +import alpine.server.filters.AuthenticationFilter; import alpine.server.resources.AlpineResource; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Parameter; @@ -28,17 +37,26 @@ import io.swagger.v3.oas.annotations.media.Schema; import io.swagger.v3.oas.annotations.responses.ApiResponse; import io.swagger.v3.oas.annotations.responses.ApiResponses; +import io.swagger.v3.oas.annotations.security.SecurityRequirement; +import io.swagger.v3.oas.annotations.security.SecurityRequirements; import io.swagger.v3.oas.annotations.tags.Tag; import jakarta.ws.rs.GET; +import jakarta.ws.rs.HttpMethod; import jakarta.ws.rs.Path; import jakarta.ws.rs.PathParam; import jakarta.ws.rs.Produces; import jakarta.ws.rs.core.Response; +import org.dependencytrack.auth.Permissions; import org.dependencytrack.model.Project; import org.dependencytrack.model.ProjectMetrics; import org.dependencytrack.model.validation.ValidUuid; import org.dependencytrack.persistence.QueryManager; import org.dependencytrack.resources.v1.misc.Badger; +import org.glassfish.jersey.server.ContainerRequest; +import org.owasp.security.logging.SecurityMarkers; + +import javax.naming.AuthenticationException; +import java.security.Principal; import static org.dependencytrack.model.ConfigPropertyConstants.GENERAL_BADGE_ENABLED; @@ -50,21 +68,112 @@ */ @Path("/v1/badge") @Tag(name = "badge") +@SecurityRequirements({ + @SecurityRequirement(name = "ApiKeyAuth"), + @SecurityRequirement(name = "BearerAuth"), + @SecurityRequirement(name = "ApiKeyQueryAuth") +}) public class BadgeResource extends AlpineResource { private static final String SVG_MEDIA_TYPE = "image/svg+xml"; - private boolean isBadgeSupportEnabled(final QueryManager qm) { + private final Logger LOGGER = Logger.getLogger(AuthenticationFilter.class); + + private boolean isUnauthenticatedBadgeAccessEnabled(final QueryManager qm) { ConfigProperty property = qm.getConfigProperty( GENERAL_BADGE_ENABLED.getGroupName(), GENERAL_BADGE_ENABLED.getPropertyName()); return BooleanUtil.valueOf(property.getPropertyValue()); } + // Stand-in methods for alpine.server.filters.AuthenticationFilter and + // alpine.server.filters.AuthorizationFilter to allow enabling and disabling of + // unauthenticated access to the badges API during runtime, used solely to offer + // a deprecation period for unauthenticated access to badges. + private boolean passesAuthentication() { + ContainerRequest request = (ContainerRequest) super.getRequestContext().getRequest(); + + if (HttpMethod.OPTIONS.equals(request.getMethod())) { + return true; + } + + Principal principal = null; + + final ApiKeyAuthenticationService apiKeyAuthService = new ApiKeyAuthenticationService(request, true); + if (apiKeyAuthService.isSpecified()) { + try { + principal = apiKeyAuthService.authenticate(); + } catch (AuthenticationException e) { + LOGGER.info(SecurityMarkers.SECURITY_FAILURE, "Invalid API key asserted"); + return false; + } + } + + final JwtAuthenticationService jwtAuthService = new JwtAuthenticationService(request); + if (jwtAuthService.isSpecified()) { + try { + principal = jwtAuthService.authenticate(); + } catch (AuthenticationException e) { + LOGGER.info(SecurityMarkers.SECURITY_FAILURE, "Invalid JWT asserted"); + return false; + } + } + + if (principal == null) { + return false; + } else { + super.getRequestContext().setProperty("Principal", principal); + return true; + } + } + + private boolean passesAuthorization(final QueryManager qm) { + final Principal principal = (Principal) super.getRequestContext().getProperty("Principal"); + if (principal == null) { + LOGGER.info(SecurityMarkers.SECURITY_FAILURE, "A request was made without the assertion of a valid user principal"); + return false; + } + + final String[] permissions = { Permissions.Constants.VIEW_BADGES }; + + if (principal instanceof ApiKey) { + final ApiKey apiKey = (ApiKey)principal; + for (final String permission: permissions) { + if (qm.hasPermission(apiKey, permission)) { + return true; + } + } + LOGGER.info(SecurityMarkers.SECURITY_FAILURE, "Unauthorized access attempt made by API Key " + + apiKey.getMaskedKey() + " to " + ((ContainerRequest) super.getRequestContext()).getRequestUri().toString()); + } else { + UserPrincipal user = null; + if (principal instanceof ManagedUser) { + user = qm.getManagedUser(((ManagedUser) principal).getUsername()); + } else if (principal instanceof LdapUser) { + user = qm.getLdapUser(((LdapUser) principal).getUsername()); + } else if (principal instanceof OidcUser) { + user = qm.getOidcUser(((OidcUser) principal).getUsername()); + } + if (user == null) { + LOGGER.info(SecurityMarkers.SECURITY_FAILURE, "A request was made but the system in unable to find the user principal"); + return false; + } + for (final String permission : permissions) { + if (qm.hasPermission(user, permission, true)) { + return true; + } + } + LOGGER.info(SecurityMarkers.SECURITY_FAILURE, "Unauthorized access attempt made by " + + user.getUsername() + " to " + ((ContainerRequest) super.getRequestContext()).getRequestUri().toString()); + } + return false; + } + @GET @Path("/vulns/project/{uuid}") @Produces(SVG_MEDIA_TYPE) @Operation( - summary = "Returns current metrics for a specific project" + summary = "Returns current metrics for a specific project", + description = "

Requires permission VIEW_BADGES

" ) @ApiResponses(value = { @ApiResponse( @@ -72,8 +181,8 @@ private boolean isBadgeSupportEnabled(final QueryManager qm) { description = "A badge displaying current vulnerability metrics for a project in SVG format", content = @Content(schema = @Schema(type = "string")) ), - @ApiResponse(responseCode = "204", description = "Badge support is disabled. No content will be returned."), @ApiResponse(responseCode = "401", description = "Unauthorized"), + @ApiResponse(responseCode = "403", description = "Forbidden"), @ApiResponse(responseCode = "404", description = "The project could not be found") }) @AuthenticationNotRequired @@ -81,17 +190,22 @@ public Response getProjectVulnerabilitiesBadge( @Parameter(description = "The UUID of the project to retrieve metrics for", schema = @Schema(type = "string", format = "uuid"), required = true) @PathParam("uuid") @ValidUuid String uuid) { try (QueryManager qm = new QueryManager()) { - if (isBadgeSupportEnabled(qm)) { - final Project project = qm.getObjectByUuid(Project.class, uuid); - if (project != null) { - final ProjectMetrics metrics = qm.getMostRecentProjectMetrics(project); - final Badger badger = new Badger(); - return Response.ok(badger.generateVulnerabilities(metrics)).build(); - } else { - return Response.status(Response.Status.NOT_FOUND).entity("The project could not be found.").build(); + if (!isUnauthenticatedBadgeAccessEnabled(qm) && !passesAuthentication()) { + return Response.status(Response.Status.UNAUTHORIZED).build(); + } + if (!isUnauthenticatedBadgeAccessEnabled(qm) && !passesAuthorization(qm)) { + return Response.status(Response.Status.FORBIDDEN).build(); + } + final Project project = qm.getObjectByUuid(Project.class, uuid); + if (project != null) { + if (!isUnauthenticatedBadgeAccessEnabled(qm) && !qm.hasAccess(super.getPrincipal(), project)) { + return Response.status(Response.Status.FORBIDDEN).entity("Access to the specified project is forbidden").build(); } + final ProjectMetrics metrics = qm.getMostRecentProjectMetrics(project); + final Badger badger = new Badger(); + return Response.ok(badger.generateVulnerabilities(metrics)).build(); } else { - return Response.status(Response.Status.NO_CONTENT).build(); + return Response.status(Response.Status.NOT_FOUND).entity("The project could not be found.").build(); } } } @@ -100,7 +214,8 @@ public Response getProjectVulnerabilitiesBadge( @Path("/vulns/project/{name}/{version}") @Produces(SVG_MEDIA_TYPE) @Operation( - summary = "Returns current metrics for a specific project" + summary = "Returns current metrics for a specific project", + description = "

Requires permission VIEW_BADGES

" ) @ApiResponses(value = { @ApiResponse( @@ -108,8 +223,8 @@ public Response getProjectVulnerabilitiesBadge( description = "A badge displaying current vulnerability metrics for a project in SVG format", content = @Content(schema = @Schema(type = "string")) ), - @ApiResponse(responseCode = "204", description = "Badge support is disabled. No content will be returned."), @ApiResponse(responseCode = "401", description = "Unauthorized"), + @ApiResponse(responseCode = "403", description = "Forbidden"), @ApiResponse(responseCode = "404", description = "The project could not be found") }) @AuthenticationNotRequired @@ -119,17 +234,22 @@ public Response getProjectVulnerabilitiesBadge( @Parameter(description = "The version of the project to query on", required = true) @PathParam("version") String version) { try (QueryManager qm = new QueryManager()) { - if (isBadgeSupportEnabled(qm)) { - final Project project = qm.getProject(name, version); - if (project != null) { - final ProjectMetrics metrics = qm.getMostRecentProjectMetrics(project); - final Badger badger = new Badger(); - return Response.ok(badger.generateVulnerabilities(metrics)).build(); - } else { - return Response.status(Response.Status.NOT_FOUND).entity("The project could not be found.").build(); + if (!isUnauthenticatedBadgeAccessEnabled(qm) && !passesAuthentication()) { + return Response.status(Response.Status.UNAUTHORIZED).build(); + } + if (!isUnauthenticatedBadgeAccessEnabled(qm) && !passesAuthorization(qm)) { + return Response.status(Response.Status.FORBIDDEN).build(); + } + final Project project = qm.getProject(name, version); + if (project != null) { + if (!isUnauthenticatedBadgeAccessEnabled(qm) && !qm.hasAccess(super.getPrincipal(), project)) { + return Response.status(Response.Status.FORBIDDEN).entity("Access to the specified project is forbidden").build(); } + final ProjectMetrics metrics = qm.getMostRecentProjectMetrics(project); + final Badger badger = new Badger(); + return Response.ok(badger.generateVulnerabilities(metrics)).build(); } else { - return Response.status(Response.Status.NO_CONTENT).build(); + return Response.status(Response.Status.NOT_FOUND).entity("The project could not be found.").build(); } } } @@ -138,7 +258,8 @@ public Response getProjectVulnerabilitiesBadge( @Path("/violations/project/{uuid}") @Produces(SVG_MEDIA_TYPE) @Operation( - summary = "Returns a policy violations badge for a specific project" + summary = "Returns a policy violations badge for a specific project", + description = "

Requires permission VIEW_BADGES

" ) @ApiResponses(value = { @ApiResponse( @@ -146,8 +267,8 @@ public Response getProjectVulnerabilitiesBadge( description = "A badge displaying current policy violation metrics of a project in SVG format", content = @Content(schema = @Schema(type = "string")) ), - @ApiResponse(responseCode = "204", description = "Badge support is disabled. No content will be returned."), @ApiResponse(responseCode = "401", description = "Unauthorized"), + @ApiResponse(responseCode = "403", description = "Forbidden"), @ApiResponse(responseCode = "404", description = "The project could not be found") }) @AuthenticationNotRequired @@ -155,17 +276,22 @@ public Response getProjectPolicyViolationsBadge( @Parameter(description = "The UUID of the project to retrieve a badge for", schema = @Schema(type = "string", format = "uuid"), required = true) @PathParam("uuid") @ValidUuid String uuid) { try (QueryManager qm = new QueryManager()) { - if (isBadgeSupportEnabled(qm)) { - final Project project = qm.getObjectByUuid(Project.class, uuid); - if (project != null) { - final ProjectMetrics metrics = qm.getMostRecentProjectMetrics(project); - final Badger badger = new Badger(); - return Response.ok(badger.generateViolations(metrics)).build(); - } else { - return Response.status(Response.Status.NOT_FOUND).entity("The project could not be found.").build(); + if (!isUnauthenticatedBadgeAccessEnabled(qm) && !passesAuthentication()) { + return Response.status(Response.Status.UNAUTHORIZED).build(); + } + if (!isUnauthenticatedBadgeAccessEnabled(qm) && !passesAuthorization(qm)) { + return Response.status(Response.Status.FORBIDDEN).build(); + } + final Project project = qm.getObjectByUuid(Project.class, uuid); + if (project != null) { + if (!isUnauthenticatedBadgeAccessEnabled(qm) && !qm.hasAccess(super.getPrincipal(), project)) { + return Response.status(Response.Status.FORBIDDEN).entity("Access to the specified project is forbidden").build(); } + final ProjectMetrics metrics = qm.getMostRecentProjectMetrics(project); + final Badger badger = new Badger(); + return Response.ok(badger.generateViolations(metrics)).build(); } else { - return Response.status(Response.Status.NO_CONTENT).build(); + return Response.status(Response.Status.NOT_FOUND).entity("The project could not be found.").build(); } } } @@ -174,7 +300,8 @@ public Response getProjectPolicyViolationsBadge( @Path("/violations/project/{name}/{version}") @Produces(SVG_MEDIA_TYPE) @Operation( - summary = "Returns a policy violations badge for a specific project" + summary = "Returns a policy violations badge for a specific project", + description = "

Requires permission VIEW_BADGES

" ) @ApiResponses(value = { @ApiResponse( @@ -182,8 +309,8 @@ public Response getProjectPolicyViolationsBadge( description = "A badge displaying current policy violation metrics of a project in SVG format", content = @Content(schema = @Schema(type = "string")) ), - @ApiResponse(responseCode = "204", description = "Badge support is disabled. No content will be returned."), @ApiResponse(responseCode = "401", description = "Unauthorized"), + @ApiResponse(responseCode = "403", description = "Forbidden"), @ApiResponse(responseCode = "404", description = "The project could not be found") }) @AuthenticationNotRequired @@ -193,17 +320,22 @@ public Response getProjectPolicyViolationsBadge( @Parameter(description = "The version of the project to query on", required = true) @PathParam("version") String version) { try (QueryManager qm = new QueryManager()) { - if (isBadgeSupportEnabled(qm)) { - final Project project = qm.getProject(name, version); - if (project != null) { - final ProjectMetrics metrics = qm.getMostRecentProjectMetrics(project); - final Badger badger = new Badger(); - return Response.ok(badger.generateViolations(metrics)).build(); - } else { - return Response.status(Response.Status.NOT_FOUND).entity("The project could not be found.").build(); + if (!isUnauthenticatedBadgeAccessEnabled(qm) && !passesAuthentication()) { + return Response.status(Response.Status.UNAUTHORIZED).build(); + } + if (!isUnauthenticatedBadgeAccessEnabled(qm) && !passesAuthorization(qm)) { + return Response.status(Response.Status.FORBIDDEN).build(); + } + final Project project = qm.getProject(name, version); + if (project != null) { + if (!isUnauthenticatedBadgeAccessEnabled(qm) && !qm.hasAccess(super.getPrincipal(), project)) { + return Response.status(Response.Status.FORBIDDEN).entity("Access to the specified project is forbidden").build(); } + final ProjectMetrics metrics = qm.getMostRecentProjectMetrics(project); + final Badger badger = new Badger(); + return Response.ok(badger.generateViolations(metrics)).build(); } else { - return Response.status(Response.Status.NO_CONTENT).build(); + return Response.status(Response.Status.NOT_FOUND).entity("The project could not be found.").build(); } } } diff --git a/src/main/resources/openapi-configuration.yaml b/src/main/resources/openapi-configuration.yaml index dfc491c49..f4574f8ce 100644 --- a/src/main/resources/openapi-configuration.yaml +++ b/src/main/resources/openapi-configuration.yaml @@ -23,6 +23,10 @@ openAPI: BearerAuth: type: http scheme: Bearer + ApiKeyQueryAuth: + name: apiKey + type: apiKey + in: query prettyPrint: true resourcePackages: - alpine.server.resources diff --git a/src/test/java/org/dependencytrack/ResourceTest.java b/src/test/java/org/dependencytrack/ResourceTest.java index 5fe3d1627..b960f62a1 100644 --- a/src/test/java/org/dependencytrack/ResourceTest.java +++ b/src/test/java/org/dependencytrack/ResourceTest.java @@ -89,6 +89,7 @@ public abstract class ResourceTest { protected final String SIZE = "size"; protected final String TOTAL_COUNT_HEADER = "X-Total-Count"; protected final String X_API_KEY = "X-Api-Key"; + protected final String API_KEY = "apiKey"; protected final String V1_TAG = "/v1/tag"; // Hashing is expensive. Do it once and re-use across tests as much as possible. diff --git a/src/test/java/org/dependencytrack/auth/PermissionsTest.java b/src/test/java/org/dependencytrack/auth/PermissionsTest.java index 7560ca2e6..15c556019 100644 --- a/src/test/java/org/dependencytrack/auth/PermissionsTest.java +++ b/src/test/java/org/dependencytrack/auth/PermissionsTest.java @@ -21,49 +21,50 @@ import org.junit.Assert; import org.junit.Test; +import static org.dependencytrack.auth.Permissions.Constants.ACCESS_MANAGEMENT; +import static org.dependencytrack.auth.Permissions.Constants.ACCESS_MANAGEMENT_CREATE; +import static org.dependencytrack.auth.Permissions.Constants.ACCESS_MANAGEMENT_DELETE; +import static org.dependencytrack.auth.Permissions.Constants.ACCESS_MANAGEMENT_READ; +import static org.dependencytrack.auth.Permissions.Constants.ACCESS_MANAGEMENT_UPDATE; import static org.dependencytrack.auth.Permissions.Constants.BOM_UPLOAD; -import static org.dependencytrack.auth.Permissions.Constants.VIEW_PORTFOLIO; +import static org.dependencytrack.auth.Permissions.Constants.POLICY_MANAGEMENT; +import static org.dependencytrack.auth.Permissions.Constants.POLICY_MANAGEMENT_CREATE; +import static org.dependencytrack.auth.Permissions.Constants.POLICY_MANAGEMENT_DELETE; +import static org.dependencytrack.auth.Permissions.Constants.POLICY_MANAGEMENT_READ; +import static org.dependencytrack.auth.Permissions.Constants.POLICY_MANAGEMENT_UPDATE; +import static org.dependencytrack.auth.Permissions.Constants.POLICY_VIOLATION_ANALYSIS; import static org.dependencytrack.auth.Permissions.Constants.PORTFOLIO_MANAGEMENT; import static org.dependencytrack.auth.Permissions.Constants.PORTFOLIO_MANAGEMENT_CREATE; +import static org.dependencytrack.auth.Permissions.Constants.PORTFOLIO_MANAGEMENT_DELETE; import static org.dependencytrack.auth.Permissions.Constants.PORTFOLIO_MANAGEMENT_READ; import static org.dependencytrack.auth.Permissions.Constants.PORTFOLIO_MANAGEMENT_UPDATE; -import static org.dependencytrack.auth.Permissions.Constants.PORTFOLIO_MANAGEMENT_DELETE; +import static org.dependencytrack.auth.Permissions.Constants.PROJECT_CREATION_UPLOAD; +import static org.dependencytrack.auth.Permissions.Constants.SYSTEM_CONFIGURATION; +import static org.dependencytrack.auth.Permissions.Constants.SYSTEM_CONFIGURATION_CREATE; +import static org.dependencytrack.auth.Permissions.Constants.SYSTEM_CONFIGURATION_DELETE; +import static org.dependencytrack.auth.Permissions.Constants.SYSTEM_CONFIGURATION_READ; +import static org.dependencytrack.auth.Permissions.Constants.SYSTEM_CONFIGURATION_UPDATE; +import static org.dependencytrack.auth.Permissions.Constants.TAG_MANAGEMENT; +import static org.dependencytrack.auth.Permissions.Constants.TAG_MANAGEMENT_DELETE; +import static org.dependencytrack.auth.Permissions.Constants.VIEW_POLICY_VIOLATION; +import static org.dependencytrack.auth.Permissions.Constants.VIEW_PORTFOLIO; import static org.dependencytrack.auth.Permissions.Constants.VIEW_VULNERABILITY; import static org.dependencytrack.auth.Permissions.Constants.VULNERABILITY_ANALYSIS; import static org.dependencytrack.auth.Permissions.Constants.VULNERABILITY_ANALYSIS_CREATE; import static org.dependencytrack.auth.Permissions.Constants.VULNERABILITY_ANALYSIS_READ; import static org.dependencytrack.auth.Permissions.Constants.VULNERABILITY_ANALYSIS_UPDATE; -import static org.dependencytrack.auth.Permissions.Constants.VIEW_POLICY_VIOLATION; import static org.dependencytrack.auth.Permissions.Constants.VULNERABILITY_MANAGEMENT; import static org.dependencytrack.auth.Permissions.Constants.VULNERABILITY_MANAGEMENT_CREATE; +import static org.dependencytrack.auth.Permissions.Constants.VULNERABILITY_MANAGEMENT_DELETE; import static org.dependencytrack.auth.Permissions.Constants.VULNERABILITY_MANAGEMENT_READ; import static org.dependencytrack.auth.Permissions.Constants.VULNERABILITY_MANAGEMENT_UPDATE; -import static org.dependencytrack.auth.Permissions.Constants.VULNERABILITY_MANAGEMENT_DELETE; -import static org.dependencytrack.auth.Permissions.Constants.POLICY_VIOLATION_ANALYSIS; -import static org.dependencytrack.auth.Permissions.Constants.ACCESS_MANAGEMENT; -import static org.dependencytrack.auth.Permissions.Constants.ACCESS_MANAGEMENT_CREATE; -import static org.dependencytrack.auth.Permissions.Constants.ACCESS_MANAGEMENT_READ; -import static org.dependencytrack.auth.Permissions.Constants.ACCESS_MANAGEMENT_UPDATE; -import static org.dependencytrack.auth.Permissions.Constants.ACCESS_MANAGEMENT_DELETE; -import static org.dependencytrack.auth.Permissions.Constants.SYSTEM_CONFIGURATION; -import static org.dependencytrack.auth.Permissions.Constants.SYSTEM_CONFIGURATION_CREATE; -import static org.dependencytrack.auth.Permissions.Constants.SYSTEM_CONFIGURATION_READ; -import static org.dependencytrack.auth.Permissions.Constants.SYSTEM_CONFIGURATION_UPDATE; -import static org.dependencytrack.auth.Permissions.Constants.SYSTEM_CONFIGURATION_DELETE; -import static org.dependencytrack.auth.Permissions.Constants.PROJECT_CREATION_UPLOAD; -import static org.dependencytrack.auth.Permissions.Constants.POLICY_MANAGEMENT; -import static org.dependencytrack.auth.Permissions.Constants.POLICY_MANAGEMENT_CREATE; -import static org.dependencytrack.auth.Permissions.Constants.POLICY_MANAGEMENT_READ; -import static org.dependencytrack.auth.Permissions.Constants.POLICY_MANAGEMENT_UPDATE; -import static org.dependencytrack.auth.Permissions.Constants.POLICY_MANAGEMENT_DELETE; -import static org.dependencytrack.auth.Permissions.Constants.TAG_MANAGEMENT; -import static org.dependencytrack.auth.Permissions.Constants.TAG_MANAGEMENT_DELETE; +import static org.dependencytrack.auth.Permissions.Constants.VIEW_BADGES; public class PermissionsTest { @Test public void testPermissionEnums() { - Assert.assertEquals(37, Permissions.values().length); + Assert.assertEquals(38, Permissions.values().length); Assert.assertEquals("BOM_UPLOAD", Permissions.BOM_UPLOAD.name()); Assert.assertEquals("VIEW_PORTFOLIO", Permissions.VIEW_PORTFOLIO.name()); Assert.assertEquals("PORTFOLIO_MANAGEMENT", Permissions.PORTFOLIO_MANAGEMENT.name()); @@ -101,6 +102,7 @@ public void testPermissionEnums() { Assert.assertEquals("POLICY_MANAGEMENT_DELETE", Permissions.POLICY_MANAGEMENT_DELETE.name()); Assert.assertEquals("TAG_MANAGEMENT", Permissions.TAG_MANAGEMENT.name()); Assert.assertEquals("TAG_MANAGEMENT_DELETE", Permissions.TAG_MANAGEMENT_DELETE.name()); + Assert.assertEquals("VIEW_BADGES", Permissions.VIEW_BADGES.name()); } @Test @@ -142,5 +144,6 @@ public void testPermissionConstants() { Assert.assertEquals("POLICY_MANAGEMENT_DELETE", POLICY_MANAGEMENT_DELETE); Assert.assertEquals("TAG_MANAGEMENT", TAG_MANAGEMENT); Assert.assertEquals("TAG_MANAGEMENT_DELETE", TAG_MANAGEMENT_DELETE); + Assert.assertEquals("VIEW_BADGES", VIEW_BADGES); } } diff --git a/src/test/java/org/dependencytrack/persistence/DefaultObjectGeneratorTest.java b/src/test/java/org/dependencytrack/persistence/DefaultObjectGeneratorTest.java index c2f3de259..dd348626f 100644 --- a/src/test/java/org/dependencytrack/persistence/DefaultObjectGeneratorTest.java +++ b/src/test/java/org/dependencytrack/persistence/DefaultObjectGeneratorTest.java @@ -132,7 +132,7 @@ public void testLoadDefaultPersonas() throws Exception { Method method = generator.getClass().getDeclaredMethod("loadDefaultPersonas"); method.setAccessible(true); method.invoke(generator); - Assert.assertEquals(3, qm.getTeams().size()); + Assert.assertEquals(4, qm.getTeams().size()); } @Test diff --git a/src/test/java/org/dependencytrack/resources/v1/BadgeResourceTest.java b/src/test/java/org/dependencytrack/resources/v1/BadgeResourceTest.java index f427cc1f8..8d12638d4 100644 --- a/src/test/java/org/dependencytrack/resources/v1/BadgeResourceTest.java +++ b/src/test/java/org/dependencytrack/resources/v1/BadgeResourceTest.java @@ -20,20 +20,23 @@ import alpine.model.IConfigProperty; import alpine.server.filters.ApiFilter; +import jakarta.ws.rs.core.Response; import org.dependencytrack.JerseyTestRule; import org.dependencytrack.ResourceTest; +import org.dependencytrack.auth.Permissions; +import org.dependencytrack.model.ConfigPropertyConstants; import org.dependencytrack.model.Project; import org.glassfish.jersey.server.ResourceConfig; import org.junit.Assert; import org.junit.ClassRule; import org.junit.Test; -import jakarta.ws.rs.core.Response; import javax.xml.parsers.DocumentBuilder; import javax.xml.parsers.DocumentBuilderFactory; import java.io.ByteArrayInputStream; import java.io.InputStream; import java.nio.charset.StandardCharsets; +import java.util.List; import java.util.UUID; import static org.dependencytrack.model.ConfigPropertyConstants.GENERAL_BADGE_ENABLED; @@ -48,13 +51,28 @@ public class BadgeResourceTest extends ResourceTest { @Override public void before() throws Exception { super.before(); - qm.createConfigProperty(GENERAL_BADGE_ENABLED.getGroupName(), GENERAL_BADGE_ENABLED.getPropertyName(), "true", IConfigProperty.PropertyType.BOOLEAN, "Badge enabled"); + qm.createConfigProperty(GENERAL_BADGE_ENABLED.getGroupName(), GENERAL_BADGE_ENABLED.getPropertyName(), "false", IConfigProperty.PropertyType.BOOLEAN, "Unauthenticated access to badge enabled"); } @Test public void projectVulnerabilitiesByUuidTest() { + initializeWithPermissions(Permissions.VIEW_BADGES); + Project project = qm.createProject("Acme Example", null, "1.0.0", null, null, null, true, false); + Response response = jersey.target(V1_BADGE + "/vulns/project/" + project.getUuid()) + .queryParam(API_KEY, apiKey) + .request() + .get(Response.class); + Assert.assertEquals(200, response.getStatus(), 0); + Assert.assertEquals("image/svg+xml", response.getHeaderString("Content-Type")); + Assert.assertTrue(isLikelySvg(getPlainTextBody(response))); + } + + @Test + public void projectVulnerabilitiesByUuidWithHeaderAuthenticationTest() { + initializeWithPermissions(Permissions.VIEW_BADGES); Project project = qm.createProject("Acme Example", null, "1.0.0", null, null, null, true, false); Response response = jersey.target(V1_BADGE + "/vulns/project/" + project.getUuid()).request() + .header(X_API_KEY, apiKey) .get(Response.class); Assert.assertEquals(200, response.getStatus(), 0); Assert.assertEquals("image/svg+xml", response.getHeaderString("Content-Type")); @@ -62,24 +80,121 @@ public void projectVulnerabilitiesByUuidTest() { } @Test - public void projectVulnerabilitiesByUuidProjectDisabledTest() { - disableBadge(); - Response response = jersey.target(V1_BADGE + "/vulns/project/" + UUID.randomUUID()).request() + public void projectVulnerabilitiesByUuidMissingAuthenticationWithUnauthenticatedAccessEnabledTest() { + initializeWithPermissions(Permissions.VIEW_BADGES); + enableUnauthenticatedBadgeAccess(); + Project project = qm.createProject("Acme Example", null, "1.0.0", null, null, null, true, false); + Response response = jersey.target(V1_BADGE + "/vulns/project/" + project.getUuid()).request() .get(Response.class); - Assert.assertEquals(204, response.getStatus(), 0); + Assert.assertEquals(200, response.getStatus(), 0); + Assert.assertEquals("image/svg+xml", response.getHeaderString("Content-Type")); + Assert.assertTrue(isLikelySvg(getPlainTextBody(response))); } @Test public void projectVulnerabilitiesByUuidProjectNotFoundTest() { - Response response = jersey.target(V1_BADGE + "/vulns/project/" + UUID.randomUUID()).request() + initializeWithPermissions(Permissions.VIEW_BADGES); + Response response = jersey.target(V1_BADGE + "/vulns/project/" + UUID.randomUUID()) + .queryParam(API_KEY, apiKey) + .request() .get(Response.class); Assert.assertEquals(404, response.getStatus(), 0); } + @Test + public void projectVulnerabilitiesByUuidMissingAuthenticationTest() { + initializeWithPermissions(Permissions.VIEW_BADGES); + Project project = qm.createProject("Acme Example", null, "1.0.0", null, null, null, true, false); + Response response = jersey.target(V1_BADGE + "/vulns/project/" + project.getUuid()).request() + .get(Response.class); + Assert.assertEquals(401, response.getStatus(), 0); + } + + @Test + public void projectVulnerabilitiesByUuidMissingPermissionTest() { + Project project = qm.createProject("Acme Example", null, "1.0.0", null, null, null, true, false); + Response response = jersey.target(V1_BADGE + "/vulns/project/" + project.getUuid()) + .queryParam(API_KEY, apiKey) + .request() + .get(Response.class); + Assert.assertEquals(403, response.getStatus(), 0); + } + + @Test + public void projectVulnerabilitiesByUuidWithAclAccessTest() { + initializeWithPermissions(Permissions.VIEW_BADGES); + qm.createConfigProperty( + ConfigPropertyConstants.ACCESS_MANAGEMENT_ACL_ENABLED.getGroupName(), + ConfigPropertyConstants.ACCESS_MANAGEMENT_ACL_ENABLED.getPropertyName(), + "true", + ConfigPropertyConstants.ACCESS_MANAGEMENT_ACL_ENABLED.getPropertyType(), + null + ); + Project project = new Project(); + project.setName("Acme Example"); + project.setVersion("1.0.0"); + project.setAccessTeams(List.of(team)); + qm.persist(project); + Response response = jersey.target(V1_BADGE + "/vulns/project/" + project.getUuid()) + .queryParam(API_KEY, apiKey) + .request() + .get(Response.class); + Assert.assertEquals(200, response.getStatus(), 0); + Assert.assertEquals("image/svg+xml", response.getHeaderString("Content-Type")); + Assert.assertTrue(isLikelySvg(getPlainTextBody(response))); + } + + @Test + public void projectVulnerabilitiesByUuidWithAclAccessWithHeaderAuthenticationTest() { + initializeWithPermissions(Permissions.VIEW_BADGES); + qm.createConfigProperty( + ConfigPropertyConstants.ACCESS_MANAGEMENT_ACL_ENABLED.getGroupName(), + ConfigPropertyConstants.ACCESS_MANAGEMENT_ACL_ENABLED.getPropertyName(), + "true", + ConfigPropertyConstants.ACCESS_MANAGEMENT_ACL_ENABLED.getPropertyType(), + null + ); + Project project = new Project(); + project.setName("Acme Example"); + project.setVersion("1.0.0"); + project.setAccessTeams(List.of(team)); + qm.persist(project); + Response response = jersey.target(V1_BADGE + "/vulns/project/" + project.getUuid()).request() + .header(X_API_KEY, apiKey) + .get(Response.class); + Assert.assertEquals(200, response.getStatus(), 0); + Assert.assertEquals("image/svg+xml", response.getHeaderString("Content-Type")); + Assert.assertTrue(isLikelySvg(getPlainTextBody(response))); + } + + @Test + public void projectVulnerabilitiesByUuidWithAclNoAccessTest() { + initializeWithPermissions(Permissions.VIEW_BADGES); + qm.createConfigProperty( + ConfigPropertyConstants.ACCESS_MANAGEMENT_ACL_ENABLED.getGroupName(), + ConfigPropertyConstants.ACCESS_MANAGEMENT_ACL_ENABLED.getPropertyName(), + "true", + ConfigPropertyConstants.ACCESS_MANAGEMENT_ACL_ENABLED.getPropertyType(), + null + ); + Project project = new Project(); + project.setName("Acme Example"); + project.setVersion("1.0.0"); + qm.persist(project); + Response response = jersey.target(V1_BADGE + "/vulns/project/" + project.getUuid()) + .queryParam(API_KEY, apiKey) + .request() + .get(Response.class); + Assert.assertEquals(403, response.getStatus(), 0); + } + @Test public void projectVulnerabilitiesByNameAndVersionTest() { + initializeWithPermissions(Permissions.VIEW_BADGES); qm.createProject("Acme Example", null, "1.0.0", null, null, null, true, false); - Response response = jersey.target(V1_BADGE + "/vulns/project/Acme%20Example/1.0.0").request() + Response response = jersey.target(V1_BADGE + "/vulns/project/Acme%20Example/1.0.0") + .queryParam(API_KEY, apiKey) + .request() .get(Response.class); Assert.assertEquals(200, response.getStatus(), 0); Assert.assertEquals("image/svg+xml", response.getHeaderString("Content-Type")); @@ -87,32 +202,156 @@ public void projectVulnerabilitiesByNameAndVersionTest() { } @Test - public void projectVulnerabilitiesByNameAndVersionDisabledTest() { - disableBadge(); - Response response = jersey.target(V1_BADGE + "/vulns/project/ProjectNameDoesNotExist/1.0.0").request() + public void projectVulnerabilitiesByNameAndVersionWithHeaderAuthenticationTest() { + initializeWithPermissions(Permissions.VIEW_BADGES); + enableUnauthenticatedBadgeAccess(); + qm.createProject("Acme Example", null, "1.0.0", null, null, null, true, false); + Response response = jersey.target(V1_BADGE + "/vulns/project/Acme%20Example/1.0.0").request() + .get(Response.class); + Assert.assertEquals(200, response.getStatus(), 0); + Assert.assertEquals("image/svg+xml", response.getHeaderString("Content-Type")); + Assert.assertTrue(isLikelySvg(getPlainTextBody(response))); + } + + public void projectVulnerabilitiesByNameAndVersionMissingAuthenticationWithUnauthenticatedAccessEnabledTest() { + initializeWithPermissions(Permissions.VIEW_BADGES); + qm.createProject("Acme Example", null, "1.0.0", null, null, null, true, false); + Response response = jersey.target(V1_BADGE + "/vulns/project/Acme%20Example/1.0.0") + .queryParam(API_KEY, apiKey) + .request() .get(Response.class); - Assert.assertEquals(204, response.getStatus(), 0); + Assert.assertEquals(200, response.getStatus(), 0); + Assert.assertEquals("image/svg+xml", response.getHeaderString("Content-Type")); + Assert.assertTrue(isLikelySvg(getPlainTextBody(response))); } @Test public void projectVulnerabilitiesByNameAndVersionProjectNotFoundTest() { - Response response = jersey.target(V1_BADGE + "/vulns/project/ProjectNameDoesNotExist/1.0.0").request() + initializeWithPermissions(Permissions.VIEW_BADGES); + Response response = jersey.target(V1_BADGE + "/vulns/project/ProjectNameDoesNotExist/1.0.0") + .queryParam(API_KEY, apiKey) + .request() .get(Response.class); Assert.assertEquals(404, response.getStatus(), 0); } @Test public void projectVulnerabilitiesByNameAndVersionVersionNotFoundTest() { + initializeWithPermissions(Permissions.VIEW_BADGES); qm.createProject("Acme Example", null, "1.0.0", null, null, null, true, false); - Response response = jersey.target(V1_BADGE + "/vulns/project/Acme%20Example/1.2.0").request() + Response response = jersey.target(V1_BADGE + "/vulns/project/Acme%20Example/1.2.0") + .queryParam(API_KEY, apiKey) + .request() .get(Response.class); Assert.assertEquals(404, response.getStatus(), 0); } + @Test + public void projectVulnerabilitiesByNameAndVersionMissingAuthenticationTest() { + initializeWithPermissions(Permissions.VIEW_BADGES); + qm.createProject("Acme Example", null, "1.0.0", null, null, null, true, false); + Response response = jersey.target(V1_BADGE + "/vulns/project/Acme%20Example/1.0.0").request() + .get(Response.class); + Assert.assertEquals(401, response.getStatus(), 0); + } + + @Test + public void projectVulnerabilitiesByNameAndVersionMissingPermissionTest() { + qm.createProject("Acme Example", null, "1.0.0", null, null, null, true, false); + Response response = jersey.target(V1_BADGE + "/vulns/project/Acme%20Example/1.0.0") + .queryParam(API_KEY, apiKey) + .request() + .get(Response.class); + Assert.assertEquals(403, response.getStatus(), 0); + } + + @Test + public void projectVulnerabilitiesByNameAndVersionWithAclAccessTest() { + initializeWithPermissions(Permissions.VIEW_BADGES); + qm.createConfigProperty( + ConfigPropertyConstants.ACCESS_MANAGEMENT_ACL_ENABLED.getGroupName(), + ConfigPropertyConstants.ACCESS_MANAGEMENT_ACL_ENABLED.getPropertyName(), + "true", + ConfigPropertyConstants.ACCESS_MANAGEMENT_ACL_ENABLED.getPropertyType(), + null + ); + Project project = new Project(); + project.setName("Acme Example"); + project.setVersion("1.0.0"); + project.setAccessTeams(List.of(team)); + qm.persist(project); + Response response = jersey.target(V1_BADGE + "/vulns/project/Acme%20Example/1.0.0") + .queryParam(API_KEY, apiKey) + .request() + .get(Response.class); + Assert.assertEquals(200, response.getStatus(), 0); + Assert.assertEquals("image/svg+xml", response.getHeaderString("Content-Type")); + Assert.assertTrue(isLikelySvg(getPlainTextBody(response))); + } + + @Test + public void projectVulnerabilitiesByNameAndVersionWithAclAccessWithHeaderAuthenticationTest() { + initializeWithPermissions(Permissions.VIEW_BADGES); + qm.createConfigProperty( + ConfigPropertyConstants.ACCESS_MANAGEMENT_ACL_ENABLED.getGroupName(), + ConfigPropertyConstants.ACCESS_MANAGEMENT_ACL_ENABLED.getPropertyName(), + "true", + ConfigPropertyConstants.ACCESS_MANAGEMENT_ACL_ENABLED.getPropertyType(), + null + ); + Project project = new Project(); + project.setName("Acme Example"); + project.setVersion("1.0.0"); + project.setAccessTeams(List.of(team)); + qm.persist(project); + Response response = jersey.target(V1_BADGE + "/vulns/project/Acme%20Example/1.0.0").request() + .header(X_API_KEY, apiKey) + .get(Response.class); + Assert.assertEquals(200, response.getStatus(), 0); + Assert.assertEquals("image/svg+xml", response.getHeaderString("Content-Type")); + Assert.assertTrue(isLikelySvg(getPlainTextBody(response))); + } + + @Test + public void projectVulnerabilitiesByNameAndVersionWithAclNoAccessTest() { + initializeWithPermissions(Permissions.VIEW_BADGES); + qm.createConfigProperty( + ConfigPropertyConstants.ACCESS_MANAGEMENT_ACL_ENABLED.getGroupName(), + ConfigPropertyConstants.ACCESS_MANAGEMENT_ACL_ENABLED.getPropertyName(), + "true", + ConfigPropertyConstants.ACCESS_MANAGEMENT_ACL_ENABLED.getPropertyType(), + null + ); + Project project = new Project(); + project.setName("Acme Example"); + project.setVersion("1.0.0"); + qm.persist(project); + Response response = jersey.target(V1_BADGE + "/vulns/project/Acme%20Example/1.0.0") + .queryParam(API_KEY, apiKey) + .request() + .get(Response.class); + Assert.assertEquals(403, response.getStatus(), 0); + } + @Test public void projectPolicyViolationsByUuidTest() { + initializeWithPermissions(Permissions.VIEW_BADGES); + Project project = qm.createProject("Acme Example", null, "1.0.0", null, null, null, true, false); + Response response = jersey.target(V1_BADGE + "/violations/project/" + project.getUuid()) + .queryParam(API_KEY, apiKey) + .request() + .get(Response.class); + Assert.assertEquals(200, response.getStatus(), 0); + Assert.assertEquals("image/svg+xml", response.getHeaderString("Content-Type")); + Assert.assertTrue(isLikelySvg(getPlainTextBody(response))); + } + + @Test + public void projectPolicyViolationsByUuidWithHeaderAuthenticationTest() { + initializeWithPermissions(Permissions.VIEW_BADGES); Project project = qm.createProject("Acme Example", null, "1.0.0", null, null, null, true, false); Response response = jersey.target(V1_BADGE + "/violations/project/" + project.getUuid()).request() + .header(X_API_KEY, apiKey) .get(Response.class); Assert.assertEquals(200, response.getStatus(), 0); Assert.assertEquals("image/svg+xml", response.getHeaderString("Content-Type")); @@ -120,24 +359,134 @@ public void projectPolicyViolationsByUuidTest() { } @Test - public void projectPolicyViolationsByUuidProjectDisabledTest() { - disableBadge(); - Response response = jersey.target(V1_BADGE + "/violations/project/" + UUID.randomUUID()).request() + public void projectPolicyViolationsByUuidMissingAuthenticationWithUnauthenticatedAccessEnabledTest() { + initializeWithPermissions(Permissions.VIEW_BADGES); + enableUnauthenticatedBadgeAccess(); + Project project = qm.createProject("Acme Example", null, "1.0.0", null, null, null, true, false); + Response response = jersey.target(V1_BADGE + "/violations/project/" + project.getUuid()) + .request() .get(Response.class); - Assert.assertEquals(204, response.getStatus(), 0); + Assert.assertEquals(200, response.getStatus(), 0); + Assert.assertEquals("image/svg+xml", response.getHeaderString("Content-Type")); + Assert.assertTrue(isLikelySvg(getPlainTextBody(response))); } @Test public void projectPolicyViolationsByUuidProjectNotFoundTest() { - Response response = jersey.target(V1_BADGE + "/violations/project/" + UUID.randomUUID()).request() + initializeWithPermissions(Permissions.VIEW_BADGES); + Response response = jersey.target(V1_BADGE + "/violations/project/" + UUID.randomUUID()) + .queryParam(API_KEY, apiKey) + .request() .get(Response.class); Assert.assertEquals(404, response.getStatus(), 0); } + @Test + public void projectPolicyViolationsByUuidMissingAuthenticationTest() { + initializeWithPermissions(Permissions.VIEW_BADGES); + Project project = qm.createProject("Acme Example", null, "1.0.0", null, null, null, true, false); + Response response = jersey.target(V1_BADGE + "/violations/project/" + project.getUuid()).request() + .get(Response.class); + Assert.assertEquals(401, response.getStatus(), 0); + } + + @Test + public void projectPolicyViolationsByUuidMissingPermissionTest() { + Project project = qm.createProject("Acme Example", null, "1.0.0", null, null, null, true, false); + Response response = jersey.target(V1_BADGE + "/violations/project/" + project.getUuid()) + .queryParam(API_KEY, apiKey) + .request() + .get(Response.class); + Assert.assertEquals(403, response.getStatus(), 0); + } + + @Test + public void projectPolicyViolationsByUuidWithAclAccessTest() { + initializeWithPermissions(Permissions.VIEW_BADGES); + qm.createConfigProperty( + ConfigPropertyConstants.ACCESS_MANAGEMENT_ACL_ENABLED.getGroupName(), + ConfigPropertyConstants.ACCESS_MANAGEMENT_ACL_ENABLED.getPropertyName(), + "true", + ConfigPropertyConstants.ACCESS_MANAGEMENT_ACL_ENABLED.getPropertyType(), + null + ); + Project project = new Project(); + project.setName("Acme Example"); + project.setVersion("1.0.0"); + project.setAccessTeams(List.of(team)); + qm.persist(project); + Response response = jersey.target(V1_BADGE + "/violations/project/" + project.getUuid()) + .queryParam(API_KEY, apiKey) + .request() + .get(Response.class); + Assert.assertEquals(200, response.getStatus(), 0); + Assert.assertEquals("image/svg+xml", response.getHeaderString("Content-Type")); + Assert.assertTrue(isLikelySvg(getPlainTextBody(response))); + } + + @Test + public void projectPolicyViolationsByUuidWithAclAccessWithHeaderAuthenticationTest() { + initializeWithPermissions(Permissions.VIEW_BADGES); + qm.createConfigProperty( + ConfigPropertyConstants.ACCESS_MANAGEMENT_ACL_ENABLED.getGroupName(), + ConfigPropertyConstants.ACCESS_MANAGEMENT_ACL_ENABLED.getPropertyName(), + "true", + ConfigPropertyConstants.ACCESS_MANAGEMENT_ACL_ENABLED.getPropertyType(), + null + ); + Project project = new Project(); + project.setName("Acme Example"); + project.setVersion("1.0.0"); + project.setAccessTeams(List.of(team)); + qm.persist(project); + Response response = jersey.target(V1_BADGE + "/violations/project/" + project.getUuid()).request() + .header(X_API_KEY, apiKey) + .get(Response.class); + Assert.assertEquals(200, response.getStatus(), 0); + Assert.assertEquals("image/svg+xml", response.getHeaderString("Content-Type")); + Assert.assertTrue(isLikelySvg(getPlainTextBody(response))); + } + + @Test + public void projectPolicyViolationsByUuidWithAclNoAccessTest() { + initializeWithPermissions(Permissions.VIEW_BADGES); + qm.createConfigProperty( + ConfigPropertyConstants.ACCESS_MANAGEMENT_ACL_ENABLED.getGroupName(), + ConfigPropertyConstants.ACCESS_MANAGEMENT_ACL_ENABLED.getPropertyName(), + "true", + ConfigPropertyConstants.ACCESS_MANAGEMENT_ACL_ENABLED.getPropertyType(), + null + ); + Project project = new Project(); + project.setName("Acme Example"); + project.setVersion("1.0.0"); + qm.persist(project); + Response response = jersey.target(V1_BADGE + "/violations/project/" + project.getUuid()) + .queryParam(API_KEY, apiKey) + .request() + .get(Response.class); + Assert.assertEquals(403, response.getStatus(), 0); + } + @Test public void projectPolicyViolationsByNameAndVersionTest() { + initializeWithPermissions(Permissions.VIEW_BADGES); + qm.createProject("Acme Example", null, "1.0.0", null, null, null, true, false); + Response response = jersey.target(V1_BADGE + "/violations/project/Acme%20Example/1.0.0") + .queryParam(API_KEY, apiKey) + .request() + .get(Response.class); + Assert.assertEquals(200, response.getStatus(), 0); + Assert.assertEquals("image/svg+xml", response.getHeaderString("Content-Type")); + Assert.assertTrue(isLikelySvg(getPlainTextBody(response))); + } + + @Test + public void projectPolicyViolationsByNameAndVersionWithHeaderAuthenticationTest() { + initializeWithPermissions(Permissions.VIEW_BADGES); qm.createProject("Acme Example", null, "1.0.0", null, null, null, true, false); Response response = jersey.target(V1_BADGE + "/violations/project/Acme%20Example/1.0.0").request() + .header(X_API_KEY, apiKey) .get(Response.class); Assert.assertEquals(200, response.getStatus(), 0); Assert.assertEquals("image/svg+xml", response.getHeaderString("Content-Type")); @@ -145,31 +494,120 @@ public void projectPolicyViolationsByNameAndVersionTest() { } @Test - public void projectPolicyViolationsByNameAndVersionDisabledTest() { - disableBadge(); - Response response = jersey.target(V1_BADGE + "/violations/project/ProjectNameDoesNotExist/1.0.0").request() + public void projectPolicyViolationsByNameAndVersionMissingAuthenticationWithUnauthenticatedAccessEnabledTest() { + initializeWithPermissions(Permissions.VIEW_BADGES); + enableUnauthenticatedBadgeAccess(); + qm.createProject("Acme Example", null, "1.0.0", null, null, null, true, false); + Response response = jersey.target(V1_BADGE + "/violations/project/Acme%20Example/1.0.0") + .request() .get(Response.class); - Assert.assertEquals(204, response.getStatus(), 0); + Assert.assertEquals(200, response.getStatus(), 0); + Assert.assertEquals("image/svg+xml", response.getHeaderString("Content-Type")); + Assert.assertTrue(isLikelySvg(getPlainTextBody(response))); } @Test public void projectPolicyViolationsByNameAndVersionProjectNotFoundTest() { - Response response = jersey.target(V1_BADGE + "/violations/project/ProjectNameDoesNotExist/1.0.0").request() + initializeWithPermissions(Permissions.VIEW_BADGES); + Response response = jersey.target(V1_BADGE + "/violations/project/ProjectNameDoesNotExist/1.0.0") + .queryParam(API_KEY, apiKey) + .request() .get(Response.class); Assert.assertEquals(404, response.getStatus(), 0); } @Test public void projectPolicyViolationsByNameAndVersionVersionNotFoundTest() { + initializeWithPermissions(Permissions.VIEW_BADGES); qm.createProject("Acme Example", null, "1.0.0", null, null, null, true, false); - Response response = jersey.target(V1_BADGE + "/violations/project/Acme%20Example/1.2.0").request() + Response response = jersey.target(V1_BADGE + "/violations/project/Acme%20Example/1.2.0") + .queryParam(API_KEY, apiKey) + .request() .get(Response.class); Assert.assertEquals(404, response.getStatus(), 0); } - private void disableBadge() { - qm.getConfigProperty(GENERAL_BADGE_ENABLED.getGroupName(), GENERAL_BADGE_ENABLED.getPropertyName()) - .setPropertyValue("false"); + @Test + public void projectPolicyViolationsByNameAndVersionMissingAuthenticationTest() { + initializeWithPermissions(Permissions.VIEW_BADGES); + qm.createProject("Acme Example", null, "1.0.0", null, null, null, true, false); + Response response = jersey.target(V1_BADGE + "/violations/project/Acme%20Example/1.0.0").request() + .get(Response.class); + Assert.assertEquals(401, response.getStatus(), 0); + } + + @Test + public void projectPolicyViolationsByNameAndVersionMissingPermissionTest() { + qm.createProject("Acme Example", null, "1.0.0", null, null, null, true, false); + Response response = jersey.target(V1_BADGE + "/violations/project/Acme%20Example/1.0.0") + .queryParam(API_KEY, apiKey) + .request() + .get(Response.class); + Assert.assertEquals(403, response.getStatus(), 0); + } + + @Test + public void projectPolicyViolationsByNameAndVersionWithAclAccessTest() { + initializeWithPermissions(Permissions.VIEW_BADGES); + qm.createConfigProperty( + ConfigPropertyConstants.ACCESS_MANAGEMENT_ACL_ENABLED.getGroupName(), + ConfigPropertyConstants.ACCESS_MANAGEMENT_ACL_ENABLED.getPropertyName(), + "true", + ConfigPropertyConstants.ACCESS_MANAGEMENT_ACL_ENABLED.getPropertyType(), + null + ); + Project project = qm.createProject("Acme Example", null, "1.0.0", null, null, null, true, false); + project.setAccessTeams(List.of(team)); + qm.persist(project); + Response response = jersey.target(V1_BADGE + "/violations/project/Acme%20Example/1.0.0") + .queryParam(API_KEY, apiKey) + .request() + .get(Response.class); + Assert.assertEquals(200, response.getStatus(), 0); + Assert.assertEquals("image/svg+xml", response.getHeaderString("Content-Type")); + Assert.assertTrue(isLikelySvg(getPlainTextBody(response))); + } + + @Test + public void projectPolicyViolationsByNameAndVersionWithAclAccessWithHeaderAuthenticationTest() { + initializeWithPermissions(Permissions.VIEW_BADGES); + qm.createConfigProperty( + ConfigPropertyConstants.ACCESS_MANAGEMENT_ACL_ENABLED.getGroupName(), + ConfigPropertyConstants.ACCESS_MANAGEMENT_ACL_ENABLED.getPropertyName(), + "true", + ConfigPropertyConstants.ACCESS_MANAGEMENT_ACL_ENABLED.getPropertyType(), + null + ); + Project project = qm.createProject("Acme Example", null, "1.0.0", null, null, null, true, false); + project.setAccessTeams(List.of(team)); + qm.persist(project); + Response response = jersey.target(V1_BADGE + "/violations/project/Acme%20Example/1.0.0").request() + .header(X_API_KEY, apiKey) + .get(Response.class); + Assert.assertEquals(200, response.getStatus(), 0); + Assert.assertEquals("image/svg+xml", response.getHeaderString("Content-Type")); + Assert.assertTrue(isLikelySvg(getPlainTextBody(response))); + } + + @Test + public void projectPolicyViolationsByNameAndVersionWithAclNoAccessTest() { + initializeWithPermissions(Permissions.VIEW_BADGES); + qm.createConfigProperty( + ConfigPropertyConstants.ACCESS_MANAGEMENT_ACL_ENABLED.getGroupName(), + ConfigPropertyConstants.ACCESS_MANAGEMENT_ACL_ENABLED.getPropertyName(), + "true", + ConfigPropertyConstants.ACCESS_MANAGEMENT_ACL_ENABLED.getPropertyType(), + null + ); + Project project = new Project(); + project.setName("Acme Example"); + project.setVersion("1.0.0"); + qm.persist(project); + Response response = jersey.target(V1_BADGE + "/violations/project/Acme%20Example/1.0.0") + .queryParam(API_KEY, apiKey) + .request() + .get(Response.class); + Assert.assertEquals(403, response.getStatus(), 0); } private boolean isLikelySvg(String body) { @@ -183,4 +621,9 @@ private boolean isLikelySvg(String body) { return false; } } + + private void enableUnauthenticatedBadgeAccess() { + qm.getConfigProperty(GENERAL_BADGE_ENABLED.getGroupName(), GENERAL_BADGE_ENABLED.getPropertyName()) + .setPropertyValue("true"); + } } diff --git a/src/test/java/org/dependencytrack/resources/v1/PermissionResourceTest.java b/src/test/java/org/dependencytrack/resources/v1/PermissionResourceTest.java index 2b7a9cc21..636775f93 100644 --- a/src/test/java/org/dependencytrack/resources/v1/PermissionResourceTest.java +++ b/src/test/java/org/dependencytrack/resources/v1/PermissionResourceTest.java @@ -64,7 +64,7 @@ public void getAllPermissionsTest() { Assert.assertNull(response.getHeaderString(TOTAL_COUNT_HEADER)); JsonArray json = parseJsonArray(response); Assert.assertNotNull(json); - Assert.assertEquals(37, json.size()); + Assert.assertEquals(38, json.size()); Assert.assertEquals("ACCESS_MANAGEMENT", json.getJsonObject(0).getString("name")); Assert.assertEquals("Allows the management of users, teams, and API keys", json.getJsonObject(0).getString("description")); }