From 71fb9fd11f9f2ec928e272bc322c358139b2e486 Mon Sep 17 00:00:00 2001 From: Rad14nt Date: Wed, 15 Jul 2026 12:36:29 +0300 Subject: [PATCH 1/3] Add GuiComponent, GuiAction and GuiAbilities models --- .../gbl/model/user/GuiAbilities.java | 52 +++++++++++++++++++ .../sebserver/gbl/model/user/GuiAction.java | 37 +++++++++++++ .../gbl/model/user/GuiComponent.java | 43 +++++++++++++++ 3 files changed, 132 insertions(+) create mode 100644 src/main/java/ch/ethz/seb/sebserver/gbl/model/user/GuiAbilities.java create mode 100644 src/main/java/ch/ethz/seb/sebserver/gbl/model/user/GuiAction.java create mode 100644 src/main/java/ch/ethz/seb/sebserver/gbl/model/user/GuiComponent.java diff --git a/src/main/java/ch/ethz/seb/sebserver/gbl/model/user/GuiAbilities.java b/src/main/java/ch/ethz/seb/sebserver/gbl/model/user/GuiAbilities.java new file mode 100644 index 000000000..c67ce4891 --- /dev/null +++ b/src/main/java/ch/ethz/seb/sebserver/gbl/model/user/GuiAbilities.java @@ -0,0 +1,52 @@ +/* + * Copyright (c) 2026 ETH Zürich, IT Services + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ + +package ch.ethz.seb.sebserver.gbl.model.user; + +import java.util.List; +import java.util.Objects; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; +import io.swagger.v3.oas.annotations.media.ArraySchema; +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotNull; + +/** The effective GUI abilities of one user, merged over the user's roles. + * + * This is GUI presentation configuration only. The webservice authorizes every request + * through its own privilege checks, independently of this data. */ +@Schema( + name = "GuiAbilities", + description = "Effective GUI abilities of the current user, merged over the user's roles.") +public record GuiAbilities( + @NotNull + @ArraySchema( + arraySchema = @Schema( + description = "GUI components the current user may see.", + requiredMode = Schema.RequiredMode.REQUIRED)) + @JsonProperty("components") + List components, + + @NotNull + @ArraySchema( + arraySchema = @Schema( + description = "GUI actions the current user may perform.", + requiredMode = Schema.RequiredMode.REQUIRED)) + @JsonProperty("actions") + List actions) { + + @JsonCreator + public GuiAbilities( + @JsonProperty("components") final List components, + @JsonProperty("actions") final List actions) { + + this.components = List.copyOf(Objects.requireNonNull(components)); + this.actions = List.copyOf(Objects.requireNonNull(actions)); + } +} diff --git a/src/main/java/ch/ethz/seb/sebserver/gbl/model/user/GuiAction.java b/src/main/java/ch/ethz/seb/sebserver/gbl/model/user/GuiAction.java new file mode 100644 index 000000000..546228369 --- /dev/null +++ b/src/main/java/ch/ethz/seb/sebserver/gbl/model/user/GuiAction.java @@ -0,0 +1,37 @@ +/* + * Copyright (c) 2026 ETH Zürich, IT Services + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ + +package ch.ethz.seb.sebserver.gbl.model.user; + +import io.swagger.v3.oas.annotations.media.Schema; + +/** Identifier of one GUI action (button, switch or workflow step) the GUI can gate by role. + * + * The values mirror the GUIAction enum of the SEB Server GUI and must stay stable once + * released; they grow as the GUI adds gated actions. */ +@Schema(name = "GuiAction", description = "Identifier of one GUI action (button, switch or workflow step).") +public enum GuiAction { + EDIT_EXAM_SETTINGS, + ARCHIVE_EXAM, + DELETE_EXAM, + APPLY_TEST_RUN, + DISABLE_TEST_RUN, + EXPORT_EXAM_CLIENT_CONFIG, + VIEW_ASK_SETTINGS, + EDIT_ASK_SETTINGS, + EDIT_SCREEN_PROCTORING, + EDIT_SEB_SETTINGS, + EDIT_FULL_SEB_SETTINGS, + EDIT_SUPERVISORS, + EDIT_INDICATORS, + EDIT_CLIENT_GROUPS, + APPLY_SEB_RESTRICTION, + SHOW_MONITORING, + SHOW_FINISHED_EXAM_DATA, + EXCLUDE_FROM_DELETION +} diff --git a/src/main/java/ch/ethz/seb/sebserver/gbl/model/user/GuiComponent.java b/src/main/java/ch/ethz/seb/sebserver/gbl/model/user/GuiComponent.java new file mode 100644 index 000000000..569f2baa2 --- /dev/null +++ b/src/main/java/ch/ethz/seb/sebserver/gbl/model/user/GuiComponent.java @@ -0,0 +1,43 @@ +/* + * Copyright (c) 2026 ETH Zürich, IT Services + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ + +package ch.ethz.seb.sebserver.gbl.model.user; + +import io.swagger.v3.oas.annotations.media.Schema; + +/** Identifier of one GUI component (view, page or navigation entry) the GUI can gate by role. + * + * The values mirror the GUIComponent enum of the SEB Server GUI and must stay stable once + * released; they grow as the GUI adds gated components. */ +@Schema(name = "GuiComponent", description = "Identifier of one GUI component (view, page or navigation entry).") +public enum GuiComponent { + NAVIGATION_OVERVIEW, + HOME, + + SETTINGS, + INSTITUTIONS, + USER_ACCOUNTS, + CONNECTION_CONFIGS, + LMS_SETUPS, + CERTIFICATES, + + EXAM_TEMPLATE, + PREPARE_EXAM, + ADD_EXAM_WITH_URL, + + EXAMS, + + RUNNING_EXAMS, + SCREEN_PROCTORING, + SCREEN_PROCTORING_SEARCH, + SCREEN_PROCTORING_APPLICATION_SEARCH, + + ANALYZE_EXAMS, + ARCHIVE_EXAMS, + SCHEDULED_DELETION +} From f8dbcc19f560bf34992af2bc69be9933323bc111 Mon Sep 17 00:00:00 2001 From: Rad14nt Date: Wed, 15 Jul 2026 12:36:29 +0300 Subject: [PATCH 2/3] Add per-role GUI abilities definition with tests --- .../weblayer/api/GuiAbilitiesDefinition.java | 139 ++++++++++++++++++ .../api/GuiAbilitiesDefinitionTest.java | 70 +++++++++ 2 files changed, 209 insertions(+) create mode 100644 src/main/java/ch/ethz/seb/sebserver/webservice/weblayer/api/GuiAbilitiesDefinition.java create mode 100644 src/test/java/ch/ethz/seb/sebserver/webservice/weblayer/api/GuiAbilitiesDefinitionTest.java diff --git a/src/main/java/ch/ethz/seb/sebserver/webservice/weblayer/api/GuiAbilitiesDefinition.java b/src/main/java/ch/ethz/seb/sebserver/webservice/weblayer/api/GuiAbilitiesDefinition.java new file mode 100644 index 000000000..ace66732c --- /dev/null +++ b/src/main/java/ch/ethz/seb/sebserver/webservice/weblayer/api/GuiAbilitiesDefinition.java @@ -0,0 +1,139 @@ +/* + * Copyright (c) 2026 ETH Zürich, IT Services + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ + +package ch.ethz.seb.sebserver.webservice.weblayer.api; + +import java.util.Collection; +import java.util.EnumSet; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import ch.ethz.seb.sebserver.gbl.model.user.GuiAbilities; +import ch.ethz.seb.sebserver.gbl.model.user.GuiAction; +import ch.ethz.seb.sebserver.gbl.model.user.GuiComponent; +import ch.ethz.seb.sebserver.gbl.model.user.UserRole; + +/** Hand-maintained definition of the GUI abilities per user role. + * + * This is GUI presentation configuration, not domain authorization: the webservice keeps + * enforcing every request through its own privilege checks regardless of this data. */ +public final class GuiAbilitiesDefinition { + + static final Map> COMPONENTS_BY_ROLE = Map.of( + + UserRole.SEB_SERVER_ADMIN, Set.of( + GuiComponent.NAVIGATION_OVERVIEW, + GuiComponent.SETTINGS, + GuiComponent.USER_ACCOUNTS, + GuiComponent.INSTITUTIONS), + + UserRole.INSTITUTIONAL_ADMIN, Set.of( + GuiComponent.NAVIGATION_OVERVIEW, + GuiComponent.HOME, + GuiComponent.SETTINGS, + GuiComponent.USER_ACCOUNTS, + GuiComponent.CONNECTION_CONFIGS, + GuiComponent.LMS_SETUPS, + GuiComponent.CERTIFICATES, + GuiComponent.EXAM_TEMPLATE, + GuiComponent.EXAMS, + GuiComponent.ANALYZE_EXAMS, + GuiComponent.ARCHIVE_EXAMS, + GuiComponent.SCHEDULED_DELETION), + + UserRole.EXAM_ADMIN, Set.of( + GuiComponent.HOME, + GuiComponent.PREPARE_EXAM, + GuiComponent.ADD_EXAM_WITH_URL, + GuiComponent.RUNNING_EXAMS, + GuiComponent.SCREEN_PROCTORING, + GuiComponent.SCREEN_PROCTORING_SEARCH, + GuiComponent.SCREEN_PROCTORING_APPLICATION_SEARCH, + GuiComponent.ANALYZE_EXAMS, + GuiComponent.ARCHIVE_EXAMS, + GuiComponent.EXAMS), + + UserRole.EXAM_SUPPORTER, Set.of( + GuiComponent.HOME, + GuiComponent.EXAMS, + GuiComponent.RUNNING_EXAMS, + GuiComponent.SCREEN_PROCTORING, + GuiComponent.SCREEN_PROCTORING_SEARCH, + GuiComponent.SCREEN_PROCTORING_APPLICATION_SEARCH), + + UserRole.TEACHER, Set.of( + GuiComponent.EXAMS, + GuiComponent.RUNNING_EXAMS, + GuiComponent.SCREEN_PROCTORING, + GuiComponent.SCREEN_PROCTORING_SEARCH, + GuiComponent.SCREEN_PROCTORING_APPLICATION_SEARCH)); + + static final Map> ACTIONS_BY_ROLE = Map.of( + + UserRole.SEB_SERVER_ADMIN, Set.of(), + + UserRole.INSTITUTIONAL_ADMIN, Set.of( + GuiAction.ARCHIVE_EXAM, + GuiAction.DELETE_EXAM, + GuiAction.EDIT_FULL_SEB_SETTINGS, // TODO just for testing yet + GuiAction.VIEW_ASK_SETTINGS, + GuiAction.EXCLUDE_FROM_DELETION), + + UserRole.EXAM_ADMIN, Set.of( + GuiAction.EDIT_EXAM_SETTINGS, + GuiAction.ARCHIVE_EXAM, + GuiAction.DELETE_EXAM, + GuiAction.APPLY_TEST_RUN, + GuiAction.DISABLE_TEST_RUN, + GuiAction.EXPORT_EXAM_CLIENT_CONFIG, + GuiAction.VIEW_ASK_SETTINGS, + GuiAction.EDIT_ASK_SETTINGS, + GuiAction.EDIT_SCREEN_PROCTORING, + GuiAction.EDIT_SEB_SETTINGS, + GuiAction.EDIT_SUPERVISORS, + GuiAction.EDIT_INDICATORS, + GuiAction.EDIT_CLIENT_GROUPS, + GuiAction.APPLY_SEB_RESTRICTION, + GuiAction.SHOW_MONITORING, + GuiAction.SHOW_FINISHED_EXAM_DATA), + + // to clarify: EDIT_SCREEN_PROCTORING, EDIT_CLIENT_GROUPS, EDIT_SUPERVISORS + UserRole.EXAM_SUPPORTER, Set.of( + GuiAction.EDIT_EXAM_SETTINGS, + GuiAction.APPLY_TEST_RUN, + GuiAction.DISABLE_TEST_RUN, + GuiAction.EXPORT_EXAM_CLIENT_CONFIG, + GuiAction.VIEW_ASK_SETTINGS, + GuiAction.EDIT_ASK_SETTINGS, + GuiAction.EDIT_SEB_SETTINGS, + GuiAction.APPLY_SEB_RESTRICTION, + GuiAction.SHOW_MONITORING, + GuiAction.SHOW_FINISHED_EXAM_DATA), + + UserRole.TEACHER, Set.of( + GuiAction.APPLY_TEST_RUN, + GuiAction.DISABLE_TEST_RUN, + GuiAction.VIEW_ASK_SETTINGS, + GuiAction.SHOW_MONITORING)); + + private GuiAbilitiesDefinition() { + } + + public static GuiAbilities abilitiesFor(final Collection userRoles) { + final EnumSet components = EnumSet.noneOf(GuiComponent.class); + final EnumSet actions = EnumSet.noneOf(GuiAction.class); + + for (final UserRole role : userRoles) { + components.addAll(COMPONENTS_BY_ROLE.getOrDefault(role, Set.of())); + actions.addAll(ACTIONS_BY_ROLE.getOrDefault(role, Set.of())); + } + + return new GuiAbilities(List.copyOf(components), List.copyOf(actions)); + } +} diff --git a/src/test/java/ch/ethz/seb/sebserver/webservice/weblayer/api/GuiAbilitiesDefinitionTest.java b/src/test/java/ch/ethz/seb/sebserver/webservice/weblayer/api/GuiAbilitiesDefinitionTest.java new file mode 100644 index 000000000..7eee0b783 --- /dev/null +++ b/src/test/java/ch/ethz/seb/sebserver/webservice/weblayer/api/GuiAbilitiesDefinitionTest.java @@ -0,0 +1,70 @@ +/* + * Copyright (c) 2026 ETH Zürich, IT Services + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ + +package ch.ethz.seb.sebserver.webservice.weblayer.api; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; + +import java.util.EnumSet; + +import org.junit.Test; + +import ch.ethz.seb.sebserver.gbl.model.user.GuiAbilities; +import ch.ethz.seb.sebserver.gbl.model.user.GuiAction; +import ch.ethz.seb.sebserver.gbl.model.user.GuiComponent; +import ch.ethz.seb.sebserver.gbl.model.user.UserRole; + +public class GuiAbilitiesDefinitionTest { + + @Test + public void everyRoleHasADefinition() { + for (final UserRole role : UserRole.values()) { + assertNotNull( + "missing component definition for role " + role, + GuiAbilitiesDefinition.COMPONENTS_BY_ROLE.get(role)); + assertNotNull( + "missing action definition for role " + role, + GuiAbilitiesDefinition.ACTIONS_BY_ROLE.get(role)); + } + } + + @Test + public void everyComponentIsGrantedToAtLeastOneRole() { + final EnumSet granted = EnumSet.noneOf(GuiComponent.class); + GuiAbilitiesDefinition.COMPONENTS_BY_ROLE.values().forEach(granted::addAll); + assertEquals(EnumSet.allOf(GuiComponent.class), granted); + } + + @Test + public void everyActionIsGrantedToAtLeastOneRole() { + final EnumSet granted = EnumSet.noneOf(GuiAction.class); + GuiAbilitiesDefinition.ACTIONS_BY_ROLE.values().forEach(granted::addAll); + assertEquals(EnumSet.allOf(GuiAction.class), granted); + } + + @Test + public void abilitiesForMergesOverAllGivenRoles() { + final GuiAbilities abilities = GuiAbilitiesDefinition.abilitiesFor( + EnumSet.of(UserRole.SEB_SERVER_ADMIN, UserRole.TEACHER)); + + assertTrue(abilities.components().contains(GuiComponent.INSTITUTIONS)); + assertTrue(abilities.components().contains(GuiComponent.RUNNING_EXAMS)); + assertTrue(abilities.actions().contains(GuiAction.SHOW_MONITORING)); + } + + @Test + public void abilitiesForWithoutRolesIsEmpty() { + final GuiAbilities abilities = GuiAbilitiesDefinition.abilitiesFor( + EnumSet.noneOf(UserRole.class)); + + assertTrue(abilities.components().isEmpty()); + assertTrue(abilities.actions().isEmpty()); + } +} From ec86f1f5167b75093a957e06f16e3ef5afa29bde Mon Sep 17 00:00:00 2001 From: Rad14nt Date: Wed, 15 Jul 2026 12:36:29 +0300 Subject: [PATCH 3/3] Add gui-abilities endpoint for the current user --- .../ch/ethz/seb/sebserver/gbl/api/API.java | 1 + .../weblayer/api/UserAccountController.java | 27 +++++++++++++++++++ 2 files changed, 28 insertions(+) diff --git a/src/main/java/ch/ethz/seb/sebserver/gbl/api/API.java b/src/main/java/ch/ethz/seb/sebserver/gbl/api/API.java index f83c2901b..c5c7359e0 100644 --- a/src/main/java/ch/ethz/seb/sebserver/gbl/api/API.java +++ b/src/main/java/ch/ethz/seb/sebserver/gbl/api/API.java @@ -86,6 +86,7 @@ public enum BatchActionType { public static final String LOGOUT_PATH_SEGMENT = "/logout"; public static final String FEATURES_PATH_SEGMENT = "/features"; + public static final String GUI_ABILITIES_PATH_SEGMENT = "/gui-abilities"; public static final String SEB_VERSION_PAGE_ENDPOINT = "/seb-version-info"; public static final String SEB_VERSION_SELECTED_EXAM = "selected-exam"; diff --git a/src/main/java/ch/ethz/seb/sebserver/webservice/weblayer/api/UserAccountController.java b/src/main/java/ch/ethz/seb/sebserver/webservice/weblayer/api/UserAccountController.java index 814f80045..ac58a0518 100644 --- a/src/main/java/ch/ethz/seb/sebserver/webservice/weblayer/api/UserAccountController.java +++ b/src/main/java/ch/ethz/seb/sebserver/webservice/weblayer/api/UserAccountController.java @@ -17,6 +17,7 @@ import ch.ethz.seb.sebserver.gbl.api.authorization.Privilege; import ch.ethz.seb.sebserver.gbl.api.authorization.PrivilegeType; import ch.ethz.seb.sebserver.gbl.model.*; +import ch.ethz.seb.sebserver.gbl.model.user.GuiAbilities; import ch.ethz.seb.sebserver.gbl.model.user.PasswordChange; import ch.ethz.seb.sebserver.gbl.model.user.UserAccount; import ch.ethz.seb.sebserver.gbl.model.user.UserFeatures; @@ -151,6 +152,32 @@ public UserFeatures getCurrentUserFeatures() { return this.featureService.getCurrentUserFeatures().getOrThrow(); } + @Operation( + operationId = "getCurrentUserGuiAbilities", + summary = "Get the effective GUI abilities of the current user", + description = "GUI components and actions merged over the current user's roles. " + + "Presentation configuration for the GUI only; the webservice authorizes every request independently.") + @ApiResponses({ + @ApiResponse( + responseCode = "200", + description = "The effective GUI abilities of the current user.", + content = @Content( + mediaType = MediaType.APPLICATION_JSON_VALUE, + schema = @Schema(implementation = GuiAbilities.class))) + }) + @RequestMapping( + path = API.CURRENT_USER_PATH_SEGMENT + API.GUI_ABILITIES_PATH_SEGMENT, + method = RequestMethod.GET, + produces = MediaType.APPLICATION_JSON_VALUE) + public GuiAbilities getCurrentUserGuiAbilities() { + return GuiAbilitiesDefinition.abilitiesFor( + this.authorization + .getUserService() + .getCurrentUser() + .getUserInfo() + .getUserRoles()); + } + @Operation(hidden = true) @RequestMapping(path = API.LOGIN_PATH_SEGMENT, method = RequestMethod.POST) public void login() {