Skip to content
Merged
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
1 change: 1 addition & 0 deletions src/main/java/ch/ethz/seb/sebserver/gbl/api/API.java
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down
Original file line number Diff line number Diff line change
@@ -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<GuiComponent> components,

@NotNull
@ArraySchema(
arraySchema = @Schema(
description = "GUI actions the current user may perform.",
requiredMode = Schema.RequiredMode.REQUIRED))
@JsonProperty("actions")
List<GuiAction> actions) {

@JsonCreator
public GuiAbilities(
@JsonProperty("components") final List<GuiComponent> components,
@JsonProperty("actions") final List<GuiAction> actions) {

this.components = List.copyOf(Objects.requireNonNull(components));
this.actions = List.copyOf(Objects.requireNonNull(actions));
}
}
37 changes: 37 additions & 0 deletions src/main/java/ch/ethz/seb/sebserver/gbl/model/user/GuiAction.java
Original file line number Diff line number Diff line change
@@ -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
}
Original file line number Diff line number Diff line change
@@ -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
}
Original file line number Diff line number Diff line change
@@ -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<UserRole, Set<GuiComponent>> 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<UserRole, Set<GuiAction>> 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<UserRole> userRoles) {
final EnumSet<GuiComponent> components = EnumSet.noneOf(GuiComponent.class);
final EnumSet<GuiAction> 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));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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() {
Expand Down
Original file line number Diff line number Diff line change
@@ -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<GuiComponent> 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<GuiAction> 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());
}
}
Loading