Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -246,7 +246,7 @@ public Component load(String key) throws SW360Exception {
@Override
public List<ProjectVulnerabilityRating> getProjectVulnerabilityRatingByProjectId(String projectId, User user) {
if (!PermissionUtils.isUserAtLeast(UserGroup.USER, user)) {
return null;
return Collections.emptyList();
}
return projectDatabaseHandler.getProjectVulnerabilityRatingByProjectId(projectId);
}
Expand Down
5 changes: 5 additions & 0 deletions build-configuration/resources/orgmapping.properties
Original file line number Diff line number Diff line change
Expand Up @@ -38,3 +38,8 @@ mapping.5.target=UPPER_MAPPED
# Prefix matching test cases
mapping.6=PREFIX
mapping.6.target=PREFIX_MAPPED

# Viewer-Only Departments (comma-separated)
# Users belonging to these departments (after mapping) will be assigned the VIEWER role.
# VIEWER role is read-only with restricted access to sensitive data.
viewer.departments=EXTERNAL
Original file line number Diff line number Diff line change
Expand Up @@ -30,11 +30,13 @@ public class OrganizationMapper {
private static final String MAPPING_VALUES_SUFFIX = ".target";
private static final String MATCH_PREFIX_KEY = "match.prefix";
private static final String ENABLE_CUSTOM_MAPPING_KEY = "enable.custom.mapping";
private static final String VIEWER_DEPARTMENTS_KEY = "viewer.departments";
private static final String PROPERTIES_FILE_PATH = "/orgmapping.properties";

private static boolean matchPrefix = false;
private static boolean customMappingEnabled = false;
private static List<Map.Entry<String, String>> sortedOrganizationMappings = new ArrayList<>();
private static Set<String> viewerOnlyDepartments = new HashSet<>();

// Flags for intelligent loading
private static volatile boolean initialized = false;
Expand Down Expand Up @@ -139,6 +141,21 @@ public static boolean isMatchPrefixEnabled() {
return matchPrefix;
}

/**
* Check if a department is configured as viewer-only.
* Viewer-only departments result in users being assigned the VIEWER role.
*
* @param department the department name (already mapped/sanitized)
* @return true if the department is viewer-only
*/
public static boolean isViewerOnlyDepartment(String department) {
ensureInitialized();
if (department == null || department.isEmpty() || viewerOnlyDepartments.isEmpty()) {
return false;
}
return viewerOnlyDepartments.contains(department);
}

private static void loadOrganizationMapperSettings() {
log.info("Initializing OrganizationMapper...");

Expand All @@ -162,6 +179,16 @@ private static void loadOrganizationMapperSettings() {
matchPrefix = Boolean.parseBoolean(orgmappingProperties.getProperty(MATCH_PREFIX_KEY, "false"));
customMappingEnabled = Boolean.parseBoolean(orgmappingProperties.getProperty(ENABLE_CUSTOM_MAPPING_KEY, "false"));

// Load viewer-only departments (comma-separated list)
String viewerDepts = orgmappingProperties.getProperty(VIEWER_DEPARTMENTS_KEY, "");
viewerOnlyDepartments = Arrays.stream(viewerDepts.split(","))
.map(String::trim)
.filter(s -> !s.isEmpty())
.collect(Collectors.toSet());
if (!viewerOnlyDepartments.isEmpty()) {
log.info("Loaded viewer-only departments: " + viewerOnlyDepartments);
}

if (!customMappingEnabled) {
log.info("Custom organization mapping is disabled via configuration");
return;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,12 @@ public void userRegistrationEvent(Event event) {
updateKeycloakUserGroup(event, eu.getUserGroup());
});

userService.createOrUpdateUser(user, LISTENER);
// For new users, check if department is viewer-only
if (existingUser.isEmpty()) {
assignViewerRoleIfApplicable(user, event);
}

userService.createOrUpdateUser(user, LISTENER);
}

private User fillUserFromEvent(Map<String, String> userDetails) {
Expand Down Expand Up @@ -95,6 +100,11 @@ public void userLoginEvent(Event event) {
RealmModel realmModel = keycloakSession.realms().getRealmByName(REALM_SW360);
UserModel userModel = getUserFromKeycloakRealm(event, realmModel, userProvider);
User user = convertKcUserModelToUser(userModel);
// For new users (not yet in CouchDB), check if department is viewer-only
if (userService.getUserByEmail(user.getEmail()) == null) {
assignViewerRoleIfApplicable(user, event);
}

userService.createOrUpdateUser(user, LISTENER);
}

Expand Down Expand Up @@ -144,6 +154,23 @@ private void mapSetExternalId(UserModel userModel, User user) {
user.setExternalid(sanitizeExternalId(externalId));
}

/**
* Checks if the user's department is configured as viewer-only in orgmapping.properties.
* If so, assigns the VIEWER role to the user and updates the Keycloak group.
*
* @param user the user to check and update
* @param event the Keycloak event for group update
* @see OrganizationMapper#isViewerOnlyDepartment(String)
*/
private void assignViewerRoleIfApplicable(User user, Event event) {
if (OrganizationMapper.isViewerOnlyDepartment(user.getDepartment())) {
log.infof("Department '%s' is viewer-only, assigning VIEWER role to user %s",
user.getDepartment(), user.getEmail());
user.setUserGroup(UserGroup.VIEWER);
updateKeycloakUserGroup(event, UserGroup.VIEWER);
}
}

/**
* Sanitizes and maps the department name from identity provider.
* Applies organization name mapping if configured.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,3 +38,10 @@ match.prefix=false
# mapping.3=DEPT_
# mapping.3.target=DEPARTMENT
# This would map "DEPT_ENGINEERING", "DEPT_SALES", etc. all to "DEPARTMENT"

# Viewer-Only Departments (comma-separated)
# Users belonging to these departments (after mapping) will be assigned the VIEWER role.
# VIEWER role is read-only with restricted access to sensitive data.
# Example:
# viewer.departments=EXTERNAL,PARTNER_A,READONLY_DEPT
viewer.departments=
Original file line number Diff line number Diff line change
Expand Up @@ -270,6 +270,7 @@ private ThriftEnumUtils() {
.put(UserGroup.SECURITY_ADMIN, "Security Admin")
.put(UserGroup.SW360_ADMIN, "SW360 Admin")
.put(UserGroup.SECURITY_USER, "Security User")
.put(UserGroup.VIEWER, "Viewer")
.build();

private static final ImmutableMap<VulnerabilityImpact, String> MAP_VULNERABILITY_IMPACT = ImmutableMap.of(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,10 @@ public static boolean isSecurityUser(User user) {
return isInGroup(user, UserGroup.SECURITY_USER);
}

public static boolean isViewer(User user) {
return isInGroup(user, UserGroup.VIEWER);
}

public static boolean isSecurityAdminBySecondaryRoles(Set<UserGroup> roles) {
return roles.contains(UserGroup.SECURITY_ADMIN);
}
Expand Down Expand Up @@ -124,6 +128,8 @@ public static boolean isUserAtLeast(UserGroup group, User user) {
return isAdmin(user);
case SECURITY_USER:
return isSecurityUser(user);
case VIEWER: // VIEWER is lowest role — any authenticated role satisfies "at least VIEWER"
return isNormalUser(user) || isAdmin(user) || isClearingAdmin(user) || isEccAdmin(user) || isSecurityAdmin(user) || isSecurityUser(user) || isViewer(user);
default:
throw new IllegalArgumentException("Unknown group: " + group);
}
Expand All @@ -146,6 +152,13 @@ public static boolean isUserAtLeastDesiredRoleInSecondaryGroup(UserGroup role, S
return isAdminBySecondaryRoles(secondaryRoles);
case ADMIN:
return isAdminBySecondaryRoles(secondaryRoles);
case VIEWER: // VIEWER is lowest role — any higher role satisfies "at least VIEWER"
return secondaryRoles.contains(UserGroup.VIEWER)
|| isNormalUserBySecondaryRoles(secondaryRoles)
|| isAdminBySecondaryRoles(secondaryRoles)
|| isClearingAdminBySecondaryRoles(secondaryRoles)
|| isEccAdminBySecondaryRoles(secondaryRoles)
|| isSecurityAdminBySecondaryRoles(secondaryRoles);
default:
throw new IllegalArgumentException("Unknown role: " + role);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,12 @@ public static boolean userIsEquivalentToModeratorInProject(Project input, String
@NotNull
public static Predicate<Project> isVisible(final User user) {
return input -> {
// VIEWER can only see projects with visibility EVERYONE
if (PermissionUtils.isViewer(user)) {
Visibility v = input.getVisbility();
return v != null && v == Visibility.EVERYONE;
}

Visibility visibility = input.getVisbility();
if (visibility == null) {
visibility = Visibility.BUISNESSUNIT_AND_MODERATORS; // the current default
Expand Down
3 changes: 2 additions & 1 deletion libraries/datahandler/src/main/thrift/users.thrift
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,8 @@ enum UserGroup {
SECURITY_ADMIN = 4,
SW360_ADMIN = 5,
CLEARING_EXPERT = 6,
SECURITY_USER = 7
SECURITY_USER = 7,
VIEWER = 8
}

enum UserAccess {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
/*
* Copyright Siemens AG, 2026. Part of the SW360 Portal Project.
*
* This program and the accompanying materials are made
* available under the terms of the Eclipse Public License 2.0
* which is available at https://www.eclipse.org/legal/epl-2.0/
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.eclipse.sw360.datahandler.permissions;

import org.eclipse.sw360.datahandler.thrift.users.User;
import org.eclipse.sw360.datahandler.thrift.users.UserGroup;
import org.junit.Test;

import java.util.EnumSet;
import java.util.Set;

import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;

/**
* Tests for VIEWER role handling in PermissionUtils.
*/
public class PermissionUtilsViewerTest {

private User userWithGroup(UserGroup group) {
return new User("test@sw360.org", "sw360").setUserGroup(group);
}

// ---- isViewer() ----

@Test
public void isViewer_should_return_true_for_viewer() {
assertTrue(PermissionUtils.isViewer(userWithGroup(UserGroup.VIEWER)));
}

@Test
public void isViewer_should_return_false_for_user() {
assertFalse(PermissionUtils.isViewer(userWithGroup(UserGroup.USER)));
}

@Test
public void isViewer_should_return_false_for_admin() {
assertFalse(PermissionUtils.isViewer(userWithGroup(UserGroup.ADMIN)));
}

@Test
public void isViewer_should_return_false_for_null() {
assertFalse(PermissionUtils.isViewer(null));
}

@Test
public void isViewer_should_return_false_when_userGroup_not_set() {
User user = new User("test@sw360.org", "sw360");
assertFalse(PermissionUtils.isViewer(user));
}

// ---- isUserAtLeast(VIEWER, user) — VIEWER is the lowest role ----

@Test
public void isUserAtLeast_viewer_should_be_true_for_viewer() {
assertTrue(PermissionUtils.isUserAtLeast(UserGroup.VIEWER, userWithGroup(UserGroup.VIEWER)));
}

@Test
public void isUserAtLeast_viewer_should_be_true_for_user() {
assertTrue(PermissionUtils.isUserAtLeast(UserGroup.VIEWER, userWithGroup(UserGroup.USER)));
}

@Test
public void isUserAtLeast_viewer_should_be_true_for_admin() {
assertTrue(PermissionUtils.isUserAtLeast(UserGroup.VIEWER, userWithGroup(UserGroup.ADMIN)));
}

@Test
public void isUserAtLeast_viewer_should_be_true_for_clearing_admin() {
assertTrue(PermissionUtils.isUserAtLeast(UserGroup.VIEWER, userWithGroup(UserGroup.CLEARING_ADMIN)));
}

@Test
public void isUserAtLeast_viewer_should_be_true_for_security_admin() {
assertTrue(PermissionUtils.isUserAtLeast(UserGroup.VIEWER, userWithGroup(UserGroup.SECURITY_ADMIN)));
}

@Test
public void isUserAtLeast_viewer_should_be_true_for_ecc_admin() {
assertTrue(PermissionUtils.isUserAtLeast(UserGroup.VIEWER, userWithGroup(UserGroup.ECC_ADMIN)));
}

@Test
public void isUserAtLeast_viewer_should_be_true_for_security_user() {
assertTrue(PermissionUtils.isUserAtLeast(UserGroup.VIEWER, userWithGroup(UserGroup.SECURITY_USER)));
}

// ---- isUserAtLeast(USER, viewer) — VIEWER is BELOW USER ----

@Test
public void isUserAtLeast_user_should_be_false_for_viewer() {
assertFalse(PermissionUtils.isUserAtLeast(UserGroup.USER, userWithGroup(UserGroup.VIEWER)));
}

// ---- isUserAtLeastDesiredRoleInSecondaryGroup with VIEWER ----

@Test
public void secondary_viewer_should_match_viewer_role() {
Set<UserGroup> secondaryRoles = EnumSet.of(UserGroup.VIEWER);
assertTrue(PermissionUtils.isUserAtLeastDesiredRoleInSecondaryGroup(UserGroup.VIEWER, secondaryRoles));
}

@Test
public void secondary_user_should_satisfy_viewer_role() {
Set<UserGroup> secondaryRoles = EnumSet.of(UserGroup.USER);
assertTrue(PermissionUtils.isUserAtLeastDesiredRoleInSecondaryGroup(UserGroup.VIEWER, secondaryRoles));
}

@Test
public void secondary_admin_should_satisfy_viewer_role() {
Set<UserGroup> secondaryRoles = EnumSet.of(UserGroup.ADMIN);
assertTrue(PermissionUtils.isUserAtLeastDesiredRoleInSecondaryGroup(UserGroup.VIEWER, secondaryRoles));
}

@Test
public void empty_secondary_roles_should_not_satisfy_viewer() {
Set<UserGroup> secondaryRoles = EnumSet.noneOf(UserGroup.class);
assertFalse(PermissionUtils.isUserAtLeastDesiredRoleInSecondaryGroup(UserGroup.VIEWER, secondaryRoles));
}
}
Loading
Loading