Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
36 commits
Select commit Hold shift + click to select a range
23057e5
refactor: normalize permission model
jhoward-lm Jun 20, 2025
c32a1c4
Merge branch 'main' of https://github.com/DependencyTrack/hyades-apis…
jhoward-lm Jul 2, 2025
5498303
fix: accidental overwrite during resolve
jhoward-lm Jul 2, 2025
2c4e931
test: fix asserts
jhoward-lm Jul 2, 2025
b3d3882
test: enable portfolio access control where needed
jhoward-lm Jul 3, 2025
a68d944
fix: generated JDBI statement
jhoward-lm Jul 7, 2025
1b2ad79
test: fix asserts
jhoward-lm Jul 7, 2025
df7ddda
test: fix asserts
jhoward-lm Jul 7, 2025
082cac6
Merge branch 'main' of https://github.com/jhoward-lm/hyades-apiserver…
jhoward-lm Jul 9, 2025
aa48c35
chore: revert workaround
jhoward-lm Jul 9, 2025
f59b065
refactor: validate that only project-scoped permissions can be added …
jhoward-lm Jul 9, 2025
abcdfe4
chore: resolve PR conflict
jhoward-lm Jul 9, 2025
0ef25e7
refactor: add migration to update old permissions
jhoward-lm Jul 10, 2025
384b7e2
refactor: clean up migration DDL
jhoward-lm Jul 10, 2025
fb58cbb
refactor: use temp tables for migrations
jhoward-lm Jul 11, 2025
3eff73c
fix: population of existing permission map
jhoward-lm Jul 14, 2025
5891fd9
fix: update references to changed permission names
jhoward-lm Jul 14, 2025
d44ec9f
Merge branch 'main' into normalize-permission-model
jhoward-lm Jul 14, 2025
b442807
fix: checkstyle violation
jhoward-lm Jul 14, 2025
aa1306b
fix: update references to changed permission names
jhoward-lm Jul 14, 2025
b56ab2e
fix: permissions for update/patch projects
jhoward-lm Jul 21, 2025
527e98a
Merge branch 'main' of https://github.com/DependencyTrack/hyades-apis…
jhoward-lm Jul 21, 2025
64a49e4
fix: permissions in v2 resources
jhoward-lm Jul 21, 2025
ffaefaf
fix: permissions in v2 resources
jhoward-lm Jul 21, 2025
47534df
test: fix permission setup
jhoward-lm Jul 23, 2025
752229a
test: fix permission setup
jhoward-lm Jul 23, 2025
41f82d6
fix: notification rule resource permissions
jhoward-lm Jul 23, 2025
ec6b9fb
Merge branch 'main' of https://github.com/DependencyTrack/hyades-apis…
jhoward-lm Jul 24, 2025
d54ab29
fix: changeset permission names, address comments
jhoward-lm Jul 24, 2025
68665c4
fix: update v2 metrics resource
jhoward-lm Jul 24, 2025
b336128
Merge branch 'main' of github.com:DependencyTrack/hyades-apiserver in…
nscuro Jul 25, 2025
21b608b
Fix missing unique index on `PERMISSION.NAME`
nscuro Jul 25, 2025
f21cc84
Merge pull request #16 from DependencyTrack/normalize-permission-mode…
jhoward-lm Jul 25, 2025
b54a60e
refactor: use explicit AND/OR with PermissionRequired
jhoward-lm Jul 28, 2025
2bdf477
Merge branch 'main' of https://github.com/DependencyTrack/hyades-apis…
jhoward-lm Jul 31, 2025
22923ba
chore: create new changelog file
jhoward-lm Jul 31, 2025
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 @@ -36,10 +36,16 @@
@Documented
public @interface PermissionRequired {

public enum Operator {
AND, OR
}

/**
* An array of permissions.
* @return an array of permissions
*/
String[] value();

Operator operator() default Operator.OR;

}
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,6 @@
import jakarta.ws.rs.core.Context;
import jakarta.ws.rs.core.Response;
import java.security.Principal;
import java.util.Collections;
import java.util.Set;

/**
Expand Down Expand Up @@ -83,24 +82,30 @@ public void filter(ContainerRequestContext requestContext) {
final PermissionRequired annotation = resourceInfo.getResourceMethod().getDeclaredAnnotation(PermissionRequired.class);
final Set<String> permissions = Set.of(annotation.value());

final boolean hasNoRequiredPermission = Collections.disjoint(permissions, effectivePermissions);
if (hasNoRequiredPermission) {
final String requestUri = requestContext.getUriInfo().getRequestUri().toString();
final String requestPrincipal;
final boolean hasRequiredPermission = switch (annotation.operator()) {
case AND -> permissions.stream().allMatch(effectivePermissions::contains);
case OR -> permissions.stream().anyMatch(effectivePermissions::contains);
default -> false;
};

switch (principal) {
case ApiKey apiKey -> requestPrincipal = "API Key " + apiKey.getMaskedKey();
case User user -> requestPrincipal = user.getUsername();
default -> throw new IllegalStateException("Unexpected principal type: " + principal.getClass().getName());
}
if (hasRequiredPermission) {
requestContext.setProperty(EFFECTIVE_PERMISSIONS_PROPERTY, effectivePermissions);
return;
}

LOGGER.info(SecurityMarkers.SECURITY_FAILURE, "Unauthorized access attempt made by %s to %s"
.formatted(requestPrincipal, requestUri));
final String requestUri = requestContext.getUriInfo().getRequestUri().toString();
final String requestPrincipal;

throw new ForbiddenException(Response.status(Response.Status.FORBIDDEN).build());
} else {
requestContext.setProperty(EFFECTIVE_PERMISSIONS_PROPERTY, effectivePermissions);
switch (principal) {
case ApiKey apiKey -> requestPrincipal = "API Key " + apiKey.getMaskedKey();
case User user -> requestPrincipal = user.getUsername();
default -> throw new IllegalStateException("Unexpected principal type: " + principal.getClass().getName());
}

LOGGER.info(SecurityMarkers.SECURITY_FAILURE, "Unauthorized access attempt made by %s to %s"
.formatted(requestPrincipal, requestUri));

throw new ForbiddenException(Response.status(Response.Status.FORBIDDEN).build());
}

}
134 changes: 58 additions & 76 deletions apiserver/src/main/java/org/dependencytrack/auth/Permissions.java
Original file line number Diff line number Diff line change
Expand Up @@ -26,96 +26,78 @@
*/
public enum Permissions {
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Note so we don't forget:

  • Before this can be merged, we need a DB migration that reassigns any existing old permissions to their corresponding new counterparts.
  • Changes in frontend would also be required.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

@nscuro I added a change set for the migration. Not sure if it's what you were looking for, so any suggestions are welcome. Will work on the frontend tomorrow


BOM_UPLOAD("Allows the ability to upload CycloneDX Software Bill of Materials (SBOM)"),
VIEW_PORTFOLIO("Provides the ability to view the portfolio of projects, components, and licenses"),
PORTFOLIO_ACCESS_CONTROL_BYPASS("Provides the ability to bypass portfolio access control, granting access to all projects"),
PORTFOLIO_MANAGEMENT("Allows the creation, modification, and deletion of data in the portfolio"),
PORTFOLIO_MANAGEMENT_CREATE("Allows the creation of data in the portfolio"),
PORTFOLIO_MANAGEMENT_READ("Allows the reading of data in the portfolio"),
PORTFOLIO_MANAGEMENT_UPDATE("Allows the updating of data in the portfolio"),
PORTFOLIO_MANAGEMENT_DELETE("Allows the deletion of data in the portfolio"),
VIEW_VULNERABILITY("Provides the ability to view the vulnerabilities projects are affected by"),
VULNERABILITY_ANALYSIS("Provides all abilities to make analysis decisions on vulnerabilities"),
VULNERABILITY_ANALYSIS_CREATE("Provides the ability to upload supported VEX documents to a project"),
VULNERABILITY_ANALYSIS_READ("Provides the ability read the VEX document for a project"),
VULNERABILITY_ANALYSIS_UPDATE("Provides the ability to make analysis decisions on vulnerabilities and upload supported VEX documents for a project"),
VIEW_POLICY_VIOLATION("Provides the ability to view policy violations"),
VULNERABILITY_MANAGEMENT("Allows all management permissions of internally-defined vulnerabilities"),
VULNERABILITY_MANAGEMENT_CREATE("Allows creation of internally-defined vulnerabilities"),
VULNERABILITY_MANAGEMENT_READ("Allows reading internally-defined vulnerabilities"),
VULNERABILITY_MANAGEMENT_UPDATE("Allows updating internally-defined vulnerabilities and vulnerability tags"),
VULNERABILITY_MANAGEMENT_DELETE("Allows management of internally-defined vulnerabilities"),
POLICY_VIOLATION_ANALYSIS("Provides the ability to make analysis decisions on policy violations"),
ACCESS_MANAGEMENT("Allows the management of users, teams, and API keys"),
ACCESS_MANAGEMENT_CREATE("Allows create permissions of users, teams, and API keys"),
ACCESS_MANAGEMENT_READ("Allows read permissions of users, teams, and API keys"),
ACCESS_MANAGEMENT_UPDATE("Allows update permissions of users, teams, and API keys"),
ACCESS_MANAGEMENT_DELETE("Allows delete permissions of users, teams, and API keys"),
SYSTEM_CONFIGURATION("Allows all access to configuration of the system including notifications, repositories, and email settings"),
SYSTEM_CONFIGURATION_CREATE("Allows creating configuration of the system including notifications, repositories, and email settings"),
SYSTEM_CONFIGURATION_READ("Allows reading the configuration of the system including notifications, repositories, and email settings"),
SYSTEM_CONFIGURATION_UPDATE("Allows updating the configuration of the system including notifications, repositories, and email settings"),
SYSTEM_CONFIGURATION_DELETE("Allows deleting the configuration of the system including notifications, repositories, and email settings"),
PROJECT_CREATION_UPLOAD("Provides the ability to optionally create project (if non-existent) on BOM or scan upload"),
POLICY_MANAGEMENT("Allows the creation, modification, and deletion of policy"),
POLICY_MANAGEMENT_CREATE("Allows the creation of a policy"),
POLICY_MANAGEMENT_READ("Allows reading of policies"),
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"),
VIEW_BADGES("Provides the ability to view badges");
// @formatter:off
ACCESS_MANAGEMENT("Allows the management of users, teams, and API keys", Scope.SYSTEM),
BADGES_READ("Provides the ability to view badges", Scope.PROJECT),
BOM_CREATE("Allows the ability to upload CycloneDX Software Bill of Materials (SBOM)", Scope.PROJECT),
BOM_READ("Allows the ability to view CycloneDX Software Bill of Materials (SBOM)", Scope.PROJECT),
FINDING_CREATE("Provides the ability to upload supported VEX documents to a project", Scope.PROJECT),
FINDING_READ("Provides the ability read the VEX document for a project", Scope.PROJECT),
FINDING_UPDATE("Provides the ability to make analysis decisions on vulnerabilities and upload supported VEX documents for a project", Scope.PROJECT),
NOTIFICATION_RULE_MANAGEMENT("Allows configuration of notifications and email settings", Scope.SYSTEM),
POLICY_MANAGEMENT("Allows the creation, modification, and deletion of policy", Scope.SYSTEM),
POLICY_VIOLATION_CREATE("Provides the ability to create policy violations", Scope.PROJECT),
POLICY_VIOLATION_READ("Provides the ability to view policy violations", Scope.PROJECT),
POLICY_VIOLATION_UPDATE("Provides the ability to make analysis decisions on policy violations", Scope.PROJECT),
PORTFOLIO_ACCESS_CONTROL_BYPASS("Provides the ability to bypass portfolio access control, granting access to all projects", Scope.SYSTEM),
PORTFOLIO_MANAGEMENT("Allows the creation, modification, and deletion of data in the portfolio", Scope.SYSTEM),
PROJECT_DELETE("Provides the ability to delete resources within a project", Scope.PROJECT),
PROJECT_READ("Provides the ability to read resources within a project", Scope.PROJECT),
PROJECT_UPDATE("Provides the ability to update resources within a project", Scope.PROJECT),
SYSTEM_CONFIGURATION("Allows all access to configuration of the system including notifications, repositories, and email settings", Scope.SYSTEM),
TAG_MANAGEMENT("Allows the management of global tag definitions", Scope.SYSTEM),
VULNERABILITY_MANAGEMENT("Allows the management of custom vulnerabilities", Scope.SYSTEM);
// @formatter:on

enum Scope {
PROJECT, SYSTEM
}

private final String description;
private final Scope scope;

Permissions(final String description) {
Permissions(final String description, final Scope scope) {
this.description = description;
this.scope = scope;
}

public String getDescription() {
return description;
}

public Scope getScope() {
return scope;
}

public boolean isProjectScope() {
return scope == Scope.PROJECT;
}

public boolean isSystemScope() {
return scope == Scope.SYSTEM;
}

public static class Constants {
public static final String BOM_UPLOAD = "BOM_UPLOAD";
public static final String VIEW_PORTFOLIO = "VIEW_PORTFOLIO";
public static final String PORTFOLIO_ACCESS_CONTROL_BYPASS = Permissions.PORTFOLIO_ACCESS_CONTROL_BYPASS.name();
public static final String PORTFOLIO_MANAGEMENT = "PORTFOLIO_MANAGEMENT";
public static final String PORTFOLIO_MANAGEMENT_CREATE = "PORTFOLIO_MANAGEMENT_CREATE";
public static final String PORTFOLIO_MANAGEMENT_READ = "PORTFOLIO_MANAGEMENT_READ";
public static final String PORTFOLIO_MANAGEMENT_UPDATE = "PORTFOLIO_MANAGEMENT_UPDATE";
public static final String PORTFOLIO_MANAGEMENT_DELETE = "PORTFOLIO_MANAGEMENT_DELETE";
public static final String VIEW_VULNERABILITY = "VIEW_VULNERABILITY";
public static final String VULNERABILITY_ANALYSIS = "VULNERABILITY_ANALYSIS";
public static final String VULNERABILITY_ANALYSIS_CREATE = "VULNERABILITY_ANALYSIS_CREATE";
public static final String VULNERABILITY_ANALYSIS_READ = "VULNERABILITY_ANALYSIS_READ";
public static final String VULNERABILITY_ANALYSIS_UPDATE = "VULNERABILITY_ANALYSIS_UPDATE";
public static final String VIEW_POLICY_VIOLATION = "VIEW_POLICY_VIOLATION";
public static final String VULNERABILITY_MANAGEMENT = "VULNERABILITY_MANAGEMENT";
public static final String VULNERABILITY_MANAGEMENT_CREATE = "VULNERABILITY_MANAGEMENT_CREATE";
public static final String VULNERABILITY_MANAGEMENT_READ = "VULNERABILITY_MANAGEMENT_READ";
public static final String VULNERABILITY_MANAGEMENT_UPDATE = "VULNERABILITY_MANAGEMENT_UPDATE";
public static final String VULNERABILITY_MANAGEMENT_DELETE = "VULNERABILITY_MANAGEMENT_DELETE";
public static final String POLICY_VIOLATION_ANALYSIS = "POLICY_VIOLATION_ANALYSIS";
public static final String ACCESS_MANAGEMENT = "ACCESS_MANAGEMENT";
public static final String ACCESS_MANAGEMENT_CREATE = "ACCESS_MANAGEMENT_CREATE";
public static final String ACCESS_MANAGEMENT_READ = "ACCESS_MANAGEMENT_READ";
public static final String ACCESS_MANAGEMENT_UPDATE = "ACCESS_MANAGEMENT_UPDATE";
public static final String ACCESS_MANAGEMENT_DELETE = "ACCESS_MANAGEMENT_DELETE";
public static final String SYSTEM_CONFIGURATION = "SYSTEM_CONFIGURATION";
public static final String SYSTEM_CONFIGURATION_CREATE = "SYSTEM_CONFIGURATION_CREATE";
public static final String SYSTEM_CONFIGURATION_READ = "SYSTEM_CONFIGURATION_READ";
public static final String SYSTEM_CONFIGURATION_UPDATE = "SYSTEM_CONFIGURATION_UPDATE";
public static final String SYSTEM_CONFIGURATION_DELETE = "SYSTEM_CONFIGURATION_DELETE";
public static final String PROJECT_CREATION_UPLOAD = "PROJECT_CREATION_UPLOAD";
public static final String BADGES_READ = "BADGES_READ";
public static final String BOM_CREATE = "BOM_CREATE";
public static final String BOM_READ = "BOM_READ";
public static final String FINDING_CREATE = "FINDING_CREATE";
public static final String FINDING_READ = "FINDING_READ";
public static final String FINDING_UPDATE = "FINDING_UPDATE";
public static final String NOTIFICATION_RULE_MANAGEMENT = "NOTIFICATION_RULE_MANAGEMENT";
public static final String POLICY_MANAGEMENT = "POLICY_MANAGEMENT";
public static final String POLICY_MANAGEMENT_CREATE = "POLICY_MANAGEMENT_CREATE";
public static final String POLICY_MANAGEMENT_READ = "POLICY_MANAGEMENT_READ";
public static final String POLICY_MANAGEMENT_UPDATE = "POLICY_MANAGEMENT_UPDATE";
public static final String POLICY_MANAGEMENT_DELETE = "POLICY_MANAGEMENT_DELETE";
public static final String POLICY_VIOLATION_CREATE = "POLICY_VIOLATION_CREATE";
public static final String POLICY_VIOLATION_READ = "POLICY_VIOLATION_READ";
public static final String POLICY_VIOLATION_UPDATE = "POLICY_VIOLATION_UPDATE";
public static final String PORTFOLIO_ACCESS_CONTROL_BYPASS = "PORTFOLIO_ACCESS_CONTROL_BYPASS";
public static final String PORTFOLIO_MANAGEMENT = "PORTFOLIO_MANAGEMENT";
public static final String PROJECT_DELETE = "PROJECT_DELETE";
public static final String PROJECT_READ = "PROJECT_READ";
public static final String PROJECT_UPDATE = "PROJECT_UPDATE";
public static final String SYSTEM_CONFIGURATION = "SYSTEM_CONFIGURATION";
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";
public static final String VULNERABILITY_MANAGEMENT = "VULNERABILITY_MANAGEMENT";
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
/*
* This file is part of Dependency-Track.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
* SPDX-License-Identifier: Apache-2.0
* Copyright (c) OWASP Foundation. All Rights Reserved.
*/
package org.dependencytrack.model.validation;

import jakarta.validation.Constraint;
import jakarta.validation.Payload;
import jakarta.validation.ReportAsSingleViolation;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.Target;

import static java.lang.annotation.RetentionPolicy.RUNTIME;

/**
* @since 5.6.0
*/
@Target({ ElementType.FIELD, ElementType.PARAMETER, ElementType.TYPE_USE })
@Constraint(validatedBy = ValidRolePermissionValidator.class)
@Retention(RUNTIME)
@ReportAsSingleViolation
public @interface ValidRolePermission {

String message() default "{org.dependencytrack.model.validation.ValidRolePermission.message}";

Class<?>[] groups() default {};

Class<? extends Payload>[] payload() default {};

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
/*
* This file is part of Dependency-Track.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
* SPDX-License-Identifier: Apache-2.0
* Copyright (c) OWASP Foundation. All Rights Reserved.
*/
package org.dependencytrack.model.validation;

import jakarta.validation.ConstraintValidator;
import jakarta.validation.ConstraintValidatorContext;

import java.util.List;
import java.util.stream.Stream;

import org.dependencytrack.auth.Permissions;

/**
* @since 5.6.0
*/
public class ValidRolePermissionValidator implements ConstraintValidator<ValidRolePermission, String> {

private static final List<String> PROJECT_PERMISSIONS = Stream.of(Permissions.values())
.filter(Permissions::isProjectScope)
.map(Permissions::name)
.toList();

private static final List<String> SYSTEM_PERMISSIONS = Stream.of(Permissions.values())
.filter(Permissions::isSystemScope)
.map(Permissions::name)
.toList();

@Override
public boolean isValid(final String permission, final ConstraintValidatorContext validatorContext) {
if (SYSTEM_PERMISSIONS.contains(permission)) {
validatorContext.buildConstraintViolationWithTemplate("System permissions cannot be assigned to roles")
.addConstraintViolation();

return false;
}

if (!PROJECT_PERMISSIONS.contains(permission)) {
validatorContext.buildConstraintViolationWithTemplate("Invalid permission name")
.addConstraintViolation();

return false;
}

return true;
}

}
Loading
Loading