From e90bf79034630626db46d5cfe799825dd95b7543 Mon Sep 17 00:00:00 2001 From: anhefti Date: Mon, 3 Jun 2019 11:44:55 +0200 Subject: [PATCH] SEBSERV-44 added Exam API endpoints and discovery --- .../ethz/seb/sebserver/WebSecurityConfig.java | 6 +- .../ch/ethz/seb/sebserver/gbl/api/API.java | 2 +- .../sebserver/gbl/api/ExamAPIDiscovery.java | 105 ++++++++++++++++++ .../sebserver/gbl/model/seb/RunningExam.java | 29 +++++ .../sebserver/gbl/model/seb/RunningExams.java | 13 --- .../ch/ethz/seb/sebserver/gbl/util/Utils.java | 10 ++ ...rm.java => SebExamConfigSettingsForm.java} | 9 +- .../content/activity/PageStateDefinition.java | 4 +- .../dao/impl/LmsSetupDAOImpl.java | 5 +- .../sebconfig/SebClientConfigService.java | 4 + .../impl/SebClientConfigServiceImpl.java | 73 ++++++++++-- .../api/ConfigurationAttributeController.java | 2 +- .../weblayer/api/ConfigurationController.java | 2 +- .../api/ConfigurationNodeController.java | 2 +- .../api/ConfigurationValueController.java | 2 +- .../api/ExamAPIDiscoveryController.java | 87 +++++++++++++++ ...roller.java => ExamAPI_V1_Controller.java} | 28 +++-- .../api/ExamAdministrationController.java | 2 +- .../ExamConfigurationMappingController.java | 2 +- .../weblayer/api/IndicatorController.java | 2 +- .../weblayer/api/InfoController.java | 2 +- .../weblayer/api/InstitutionController.java | 2 +- .../weblayer/api/LmsSetupController.java | 4 +- .../weblayer/api/OrientationController.java | 2 +- .../weblayer/api/QuizController.java | 2 +- .../api/SebClientConfigController.java | 2 +- .../api/UserActivityLogController.java | 2 +- .../weblayer/api/ViewController.java | 2 +- .../oauth/WebClientDetailsService.java | 45 ++++---- .../config/application-dev-ws.properties | 4 +- .../api/exam/ExamAPIIntegrationTester.java | 45 +++++++- .../api/exam/ExamAPITestController.java | 2 +- .../api/exam/ExamDiscoveryEndpointTest.java | 76 +++++++++++++ .../resources/application-test.properties | 4 + 34 files changed, 499 insertions(+), 84 deletions(-) create mode 100644 src/main/java/ch/ethz/seb/sebserver/gbl/api/ExamAPIDiscovery.java create mode 100644 src/main/java/ch/ethz/seb/sebserver/gbl/model/seb/RunningExam.java delete mode 100644 src/main/java/ch/ethz/seb/sebserver/gbl/model/seb/RunningExams.java rename src/main/java/ch/ethz/seb/sebserver/gui/content/{SebExamConfigForm.java => SebExamConfigSettingsForm.java} (94%) create mode 100644 src/main/java/ch/ethz/seb/sebserver/webservice/weblayer/api/ExamAPIDiscoveryController.java rename src/main/java/ch/ethz/seb/sebserver/webservice/weblayer/api/{ExamAPIController.java => ExamAPI_V1_Controller.java} (72%) create mode 100644 src/test/java/ch/ethz/seb/sebserver/webservice/integration/api/exam/ExamDiscoveryEndpointTest.java diff --git a/src/main/java/ch/ethz/seb/sebserver/WebSecurityConfig.java b/src/main/java/ch/ethz/seb/sebserver/WebSecurityConfig.java index 40422848..b93bfcac 100644 --- a/src/main/java/ch/ethz/seb/sebserver/WebSecurityConfig.java +++ b/src/main/java/ch/ethz/seb/sebserver/WebSecurityConfig.java @@ -67,6 +67,8 @@ public class WebSecurityConfig extends WebSecurityConfigurerAdapter implements E // private String adminEndpoint; @Value("${sebserver.webservice.api.redirect.unauthorized}") private String unauthorizedRedirect; + @Value("${sebserver.webservice.api.exam.endpoint.discovery}") + private String examAPIDiscoveryEndpoint; /** Spring bean name of user password encoder */ public static final String USER_PASSWORD_ENCODER_BEAN_NAME = "userPasswordEncoder"; @@ -100,9 +102,7 @@ public class WebSecurityConfig extends WebSecurityConfigurerAdapter implements E web .ignoring() .antMatchers("/error") - // TODO this may not be necessary, test with separated GUI and webservice server - //.antMatchers(this.adminEndpoint + API.INFO_ENDPOINT + "/**") - ; + .antMatchers(this.examAPIDiscoveryEndpoint); } @RequestMapping("/error") 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 1299134d..3ac2d325 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 @@ -116,6 +116,6 @@ public final class API { public static final String EXAM_API_PING_ENDPOINT = "/sebping"; - public static final String EXAM_API_EVENT_ENDPOINT = "/sebevent"; + public static final String EXAM_API_EVENT_ENDPOINT = "/seblog"; } diff --git a/src/main/java/ch/ethz/seb/sebserver/gbl/api/ExamAPIDiscovery.java b/src/main/java/ch/ethz/seb/sebserver/gbl/api/ExamAPIDiscovery.java new file mode 100644 index 00000000..a982a154 --- /dev/null +++ b/src/main/java/ch/ethz/seb/sebserver/gbl/api/ExamAPIDiscovery.java @@ -0,0 +1,105 @@ +/* + * Copyright (c) 2019 ETH Zürich, Educational Development and Technology (LET) + * + * 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.api; + +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; + +import com.fasterxml.jackson.annotation.JsonProperty; + +public final class ExamAPIDiscovery { + + @JsonProperty("title") + public final String title; + + @JsonProperty("description") + public final String description; + + @JsonProperty("server-location") + public final String serverLocation; + + @JsonProperty("api-versions") + public final Collection versions; + + public ExamAPIDiscovery( + final String title, + final String description, + final String serverLocation, + final Collection versions) { + + this.title = title; + this.description = description; + this.serverLocation = serverLocation; + this.versions = versions; + } + + public ExamAPIDiscovery( + final String title, + final String description, + final String serverLocation, + final ExamAPIVersion... versions) { + + this( + title, + description, + serverLocation, + (versions != null) ? Arrays.asList(versions) : Collections.emptyList()); + } + + public static final class ExamAPIVersion { + + @JsonProperty("name") + public final String name; + + @JsonProperty("endpoints") + public final Collection endpoints; + + public ExamAPIVersion( + final String name, + final Collection endpoints) { + + this.name = name; + this.endpoints = endpoints; + } + + public ExamAPIVersion( + final String name, + final Endpoint... endpoints) { + + this.name = name; + this.endpoints = (endpoints != null) ? Arrays.asList(endpoints) : Collections.emptyList(); + } + } + + public static final class Endpoint { + + @JsonProperty("name") + public final String name; + + @JsonProperty("descripiton") + public final String descripiton; + + @JsonProperty("location") + public final String location; + + @JsonProperty("authorization") + public final String authorization; + + public Endpoint(final String name, final String descripiton, final String location, + final String authorization) { + super(); + this.name = name; + this.descripiton = descripiton; + this.location = location; + this.authorization = authorization; + } + + } +} diff --git a/src/main/java/ch/ethz/seb/sebserver/gbl/model/seb/RunningExam.java b/src/main/java/ch/ethz/seb/sebserver/gbl/model/seb/RunningExam.java new file mode 100644 index 00000000..571db6cf --- /dev/null +++ b/src/main/java/ch/ethz/seb/sebserver/gbl/model/seb/RunningExam.java @@ -0,0 +1,29 @@ +/* + * Copyright (c) 2019 ETH Zürich, Educational Development and Technology (LET) + * + * 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.seb; + +import com.fasterxml.jackson.annotation.JsonProperty; + +public final class RunningExam { + + @JsonProperty() + public final String examId; + @JsonProperty() + public final String name; + @JsonProperty() + public final String url; + + public RunningExam(final String examId, final String name, final String url) { + super(); + this.examId = examId; + this.name = name; + this.url = url; + } + +} diff --git a/src/main/java/ch/ethz/seb/sebserver/gbl/model/seb/RunningExams.java b/src/main/java/ch/ethz/seb/sebserver/gbl/model/seb/RunningExams.java deleted file mode 100644 index d19e6f1b..00000000 --- a/src/main/java/ch/ethz/seb/sebserver/gbl/model/seb/RunningExams.java +++ /dev/null @@ -1,13 +0,0 @@ -/* - * Copyright (c) 2019 ETH Zürich, Educational Development and Technology (LET) - * - * 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.seb; - -public class RunningExams { - // TODO -} diff --git a/src/main/java/ch/ethz/seb/sebserver/gbl/util/Utils.java b/src/main/java/ch/ethz/seb/sebserver/gbl/util/Utils.java index efe9b465..ebea2294 100644 --- a/src/main/java/ch/ethz/seb/sebserver/gbl/util/Utils.java +++ b/src/main/java/ch/ethz/seb/sebserver/gbl/util/Utils.java @@ -293,4 +293,14 @@ public final class Utils { return toCharArray(CharBuffer.wrap(chars)); } + public static String toString(final CharSequence charSequence) { + if (charSequence == null) { + return null; + } + + final StringBuilder builder = new StringBuilder(); + builder.append(charSequence); + return builder.toString(); + } + } diff --git a/src/main/java/ch/ethz/seb/sebserver/gui/content/SebExamConfigForm.java b/src/main/java/ch/ethz/seb/sebserver/gui/content/SebExamConfigSettingsForm.java similarity index 94% rename from src/main/java/ch/ethz/seb/sebserver/gui/content/SebExamConfigForm.java rename to src/main/java/ch/ethz/seb/sebserver/gui/content/SebExamConfigSettingsForm.java index f96d0a89..4844751c 100644 --- a/src/main/java/ch/ethz/seb/sebserver/gui/content/SebExamConfigForm.java +++ b/src/main/java/ch/ethz/seb/sebserver/gui/content/SebExamConfigSettingsForm.java @@ -50,11 +50,12 @@ import ch.ethz.seb.sebserver.gui.widget.WidgetFactory; @Lazy @Component @GuiProfile -public class SebExamConfigForm implements TemplateComposer { +public class SebExamConfigSettingsForm implements TemplateComposer { - private static final Logger log = LoggerFactory.getLogger(SebExamConfigForm.class); + private static final Logger log = LoggerFactory.getLogger(SebExamConfigSettingsForm.class); - private static final String VIEW_TEXT_KEY_PREFIX = "sebserver.examconfig.props.form.views."; + private static final String VIEW_TEXT_KEY_PREFIX = + "sebserver.examconfig.props.form.views."; private static final String KEY_SAVE_TO_HISTORY_SUCCESS = "sebserver.examconfig.action.saveToHistory.success"; private static final String KEY_UNDO_SUCCESS = @@ -68,7 +69,7 @@ public class SebExamConfigForm implements TemplateComposer { private final CurrentUser currentUser; private final ExamConfigurationService examConfigurationService; - protected SebExamConfigForm( + protected SebExamConfigSettingsForm( final PageService pageService, final RestService restService, final CurrentUser currentUser, diff --git a/src/main/java/ch/ethz/seb/sebserver/gui/content/activity/PageStateDefinition.java b/src/main/java/ch/ethz/seb/sebserver/gui/content/activity/PageStateDefinition.java index c4cf1a19..f81e754b 100644 --- a/src/main/java/ch/ethz/seb/sebserver/gui/content/activity/PageStateDefinition.java +++ b/src/main/java/ch/ethz/seb/sebserver/gui/content/activity/PageStateDefinition.java @@ -19,7 +19,7 @@ import ch.ethz.seb.sebserver.gui.content.LmsSetupList; import ch.ethz.seb.sebserver.gui.content.QuizDiscoveryList; import ch.ethz.seb.sebserver.gui.content.SebClientConfigForm; import ch.ethz.seb.sebserver.gui.content.SebClientConfigList; -import ch.ethz.seb.sebserver.gui.content.SebExamConfigForm; +import ch.ethz.seb.sebserver.gui.content.SebExamConfigSettingsForm; import ch.ethz.seb.sebserver.gui.content.SebExamConfigList; import ch.ethz.seb.sebserver.gui.content.SebExamConfigPropForm; import ch.ethz.seb.sebserver.gui.content.UserAccountChangePasswordForm; @@ -60,7 +60,7 @@ public enum PageStateDefinition implements PageState { SEB_EXAM_CONFIG_LIST(Type.LIST_VIEW, SebExamConfigList.class, ActivityDefinition.SEB_EXAM_CONFIG), SEB_EXAM_CONFIG_VIEW(Type.FORM_VIEW, SebExamConfigPropForm.class, ActivityDefinition.SEB_EXAM_CONFIG), SEB_EXAM_CONFIG_PROP_EDIT(Type.FORM_EDIT, SebExamConfigPropForm.class, ActivityDefinition.SEB_EXAM_CONFIG), - SEB_EXAM_CONFIG_EDIT(Type.FORM_VIEW, SebExamConfigForm.class, ActivityDefinition.SEB_EXAM_CONFIG), + SEB_EXAM_CONFIG_EDIT(Type.FORM_VIEW, SebExamConfigSettingsForm.class, ActivityDefinition.SEB_EXAM_CONFIG), ; diff --git a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/dao/impl/LmsSetupDAOImpl.java b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/dao/impl/LmsSetupDAOImpl.java index 5b409160..59111aaa 100644 --- a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/dao/impl/LmsSetupDAOImpl.java +++ b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/dao/impl/LmsSetupDAOImpl.java @@ -33,6 +33,7 @@ import ch.ethz.seb.sebserver.gbl.model.institution.LmsSetup; import ch.ethz.seb.sebserver.gbl.model.institution.LmsSetup.LmsType; import ch.ethz.seb.sebserver.gbl.profile.WebServiceProfile; import ch.ethz.seb.sebserver.gbl.util.Result; +import ch.ethz.seb.sebserver.gbl.util.Utils; import ch.ethz.seb.sebserver.webservice.datalayer.batis.mapper.LmsSetupRecordDynamicSqlSupport; import ch.ethz.seb.sebserver.webservice.datalayer.batis.mapper.LmsSetupRecordMapper; import ch.ethz.seb.sebserver.webservice.datalayer.batis.model.LmsSetupRecord; @@ -325,10 +326,10 @@ public class LmsSetupDAOImpl implements LmsSetupDAO { record.getInstitutionId(), record.getName(), LmsType.valueOf(record.getLmsType()), - (plainClientId != null) ? plainClientId.toString() : null, + Utils.toString(plainClientId), null, record.getLmsUrl(), - (plainAccessToken != null) ? plainAccessToken.toString() : null, + Utils.toString(plainAccessToken), BooleanUtils.toBooleanObject(record.getActive()))); } diff --git a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/sebconfig/SebClientConfigService.java b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/sebconfig/SebClientConfigService.java index a2796c22..aca50b24 100644 --- a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/sebconfig/SebClientConfigService.java +++ b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/sebconfig/SebClientConfigService.java @@ -40,6 +40,8 @@ public interface SebClientConfigService { " \r\n" + ""; + String getServerURL(); + boolean hasSebClientConfigurationForInstitution(Long institutionId); Result autoCreateSebClientConfigurationForInstitution(Long institutionId); @@ -48,4 +50,6 @@ public interface SebClientConfigService { OutputStream out, final String modelId); + Result getEncodedClientSecret(String clientId); + } diff --git a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/sebconfig/impl/SebClientConfigServiceImpl.java b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/sebconfig/impl/SebClientConfigServiceImpl.java index 37a7c047..55f7aeb2 100644 --- a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/sebconfig/impl/SebClientConfigServiceImpl.java +++ b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/sebconfig/impl/SebClientConfigServiceImpl.java @@ -16,14 +16,24 @@ import java.io.PipedOutputStream; import java.util.Collection; import java.util.UUID; +import javax.servlet.http.HttpServletRequest; + import org.apache.commons.io.IOUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.beans.factory.annotation.Value; import org.springframework.context.annotation.Lazy; +import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.stereotype.Service; +import org.springframework.web.context.request.RequestAttributes; +import org.springframework.web.context.request.RequestContextHolder; +import org.springframework.web.context.request.ServletRequestAttributes; import org.springframework.web.util.UriComponentsBuilder; +import ch.ethz.seb.sebserver.WebSecurityConfig; +import ch.ethz.seb.sebserver.gbl.api.API; import ch.ethz.seb.sebserver.gbl.model.institution.Institution; import ch.ethz.seb.sebserver.gbl.model.sebconfig.SebClientConfig; import ch.ethz.seb.sebserver.gbl.profile.WebServiceProfile; @@ -49,6 +59,9 @@ public class SebClientConfigServiceImpl implements SebClientConfigService { private final SebClientConfigDAO sebClientConfigDAO; private final ClientCredentialService clientCredentialService; private final SebConfigEncryptionService sebConfigEncryptionService; + @Autowired + @Qualifier(WebSecurityConfig.CLIENT_PASSWORD_ENCODER_BEAN_NAME) + private PasswordEncoder clientPasswordEncoder; private final ZipService zipService; private final String httpScheme; private final String serverAddress; @@ -99,6 +112,49 @@ public class SebClientConfigServiceImpl implements SebClientConfigService { .flatMap(this.sebClientConfigDAO::createNew); } + @Override + public Result getEncodedClientSecret(final String clientId) { + return Result.tryCatch(() -> { + final Collection clientConfigs = this.sebClientConfigDAO.all(extractInstitution(), true) + .getOrThrow(); + + final ClientCredentials clientCredentials = findClientCredentialsFor(clientId, clientConfigs); + return this.clientPasswordEncoder.encode( + this.clientCredentialService.getPlainClientSecret(clientCredentials)); + + }); + } + + public ClientCredentials findClientCredentialsFor(final String clientId, + final Collection clientConfigs) { + for (final SebClientConfig config : clientConfigs) { + try { + final ClientCredentials clientCredentials = + this.sebClientConfigDAO.getSebClientCredentials(config.getModelId()) + .getOrThrow(); + if (clientId.equals(this.clientCredentialService.getPlainClientId(clientCredentials))) { + return clientCredentials; + } + } catch (final Exception e) { + log.error("Unexpected error while trying to fetch client credentials: ", e); + } + } + + return null; + } + + private Long extractInstitution() { + try { + final RequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes(); + final HttpServletRequest request = ((ServletRequestAttributes) requestAttributes).getRequest(); + return Long.parseLong(request.getParameter(API.PARAM_INSTITUTION_ID)); + } catch (final Exception e) { + log.error( + "Failed to extract institution from current request. Search client Id over all active client configurations"); + return null; + } + } + @Override public void exportSebClientConfiguration( final OutputStream output, @@ -107,12 +163,6 @@ public class SebClientConfigServiceImpl implements SebClientConfigService { final SebClientConfig config = this.sebClientConfigDAO .byModelId(modelId).getOrThrow(); - final String serverURL = UriComponentsBuilder.newInstance() - .scheme(this.httpScheme) - .host(this.serverAddress) - .port(this.serverPort) - .toUriString(); - final ClientCredentials sebClientCredentials = this.sebClientConfigDAO .getSebClientCredentials(config.getModelId()) .getOrThrow(); @@ -128,7 +178,7 @@ public class SebClientConfigServiceImpl implements SebClientConfigService { final String plainTextConfig = String.format( SEB_CLIENT_CONFIG_EXAMPLE_XML, - serverURL, + getServerURL(), String.valueOf(config.institutionId), plainClientId, plainClientSecret, @@ -171,6 +221,15 @@ public class SebClientConfigServiceImpl implements SebClientConfigService { } } + @Override + public String getServerURL() { + return UriComponentsBuilder.newInstance() + .scheme(this.httpScheme) + .host(this.serverAddress) + .port(this.serverPort) + .toUriString(); + } + private void passwordEncryption( final OutputStream output, final CharSequence encryptionPassword, diff --git a/src/main/java/ch/ethz/seb/sebserver/webservice/weblayer/api/ConfigurationAttributeController.java b/src/main/java/ch/ethz/seb/sebserver/webservice/weblayer/api/ConfigurationAttributeController.java index 5825a95c..83c3cfbf 100644 --- a/src/main/java/ch/ethz/seb/sebserver/webservice/weblayer/api/ConfigurationAttributeController.java +++ b/src/main/java/ch/ethz/seb/sebserver/webservice/weblayer/api/ConfigurationAttributeController.java @@ -36,7 +36,7 @@ import ch.ethz.seb.sebserver.webservice.servicelayer.validation.BeanValidationSe @WebServiceProfile @RestController -@RequestMapping("/${sebserver.webservice.api.admin.endpoint}" + API.CONFIGURATION_ATTRIBUTE_ENDPOINT) +@RequestMapping("${sebserver.webservice.api.admin.endpoint}" + API.CONFIGURATION_ATTRIBUTE_ENDPOINT) public class ConfigurationAttributeController extends EntityController { protected ConfigurationAttributeController( diff --git a/src/main/java/ch/ethz/seb/sebserver/webservice/weblayer/api/ConfigurationController.java b/src/main/java/ch/ethz/seb/sebserver/webservice/weblayer/api/ConfigurationController.java index 6530dea3..c1916fc3 100644 --- a/src/main/java/ch/ethz/seb/sebserver/webservice/weblayer/api/ConfigurationController.java +++ b/src/main/java/ch/ethz/seb/sebserver/webservice/weblayer/api/ConfigurationController.java @@ -36,7 +36,7 @@ import ch.ethz.seb.sebserver.webservice.servicelayer.validation.BeanValidationSe @WebServiceProfile @RestController -@RequestMapping("/${sebserver.webservice.api.admin.endpoint}" + API.CONFIGURATION_ENDPOINT) +@RequestMapping("${sebserver.webservice.api.admin.endpoint}" + API.CONFIGURATION_ENDPOINT) public class ConfigurationController extends EntityController { private final ConfigurationDAO configurationDAO; diff --git a/src/main/java/ch/ethz/seb/sebserver/webservice/weblayer/api/ConfigurationNodeController.java b/src/main/java/ch/ethz/seb/sebserver/webservice/weblayer/api/ConfigurationNodeController.java index c05b0148..3c4e86f7 100644 --- a/src/main/java/ch/ethz/seb/sebserver/webservice/weblayer/api/ConfigurationNodeController.java +++ b/src/main/java/ch/ethz/seb/sebserver/webservice/weblayer/api/ConfigurationNodeController.java @@ -33,7 +33,7 @@ import ch.ethz.seb.sebserver.webservice.servicelayer.validation.BeanValidationSe @WebServiceProfile @RestController -@RequestMapping("/${sebserver.webservice.api.admin.endpoint}" + API.CONFIGURATION_NODE_ENDPOINT) +@RequestMapping("${sebserver.webservice.api.admin.endpoint}" + API.CONFIGURATION_NODE_ENDPOINT) public class ConfigurationNodeController extends EntityController { private final ConfigurationDAO configurationDAO; diff --git a/src/main/java/ch/ethz/seb/sebserver/webservice/weblayer/api/ConfigurationValueController.java b/src/main/java/ch/ethz/seb/sebserver/webservice/weblayer/api/ConfigurationValueController.java index 03069697..edef5793 100644 --- a/src/main/java/ch/ethz/seb/sebserver/webservice/weblayer/api/ConfigurationValueController.java +++ b/src/main/java/ch/ethz/seb/sebserver/webservice/weblayer/api/ConfigurationValueController.java @@ -41,7 +41,7 @@ import ch.ethz.seb.sebserver.webservice.servicelayer.validation.BeanValidationSe @WebServiceProfile @RestController -@RequestMapping("/${sebserver.webservice.api.admin.endpoint}" + API.CONFIGURATION_VALUE_ENDPOINT) +@RequestMapping("${sebserver.webservice.api.admin.endpoint}" + API.CONFIGURATION_VALUE_ENDPOINT) public class ConfigurationValueController extends EntityController { private final ConfigurationDAO configurationDAO; diff --git a/src/main/java/ch/ethz/seb/sebserver/webservice/weblayer/api/ExamAPIDiscoveryController.java b/src/main/java/ch/ethz/seb/sebserver/webservice/weblayer/api/ExamAPIDiscoveryController.java new file mode 100644 index 00000000..c6e68570 --- /dev/null +++ b/src/main/java/ch/ethz/seb/sebserver/webservice/weblayer/api/ExamAPIDiscoveryController.java @@ -0,0 +1,87 @@ +/* + * Copyright (c) 2019 ETH Zürich, Educational Development and Technology (LET) + * + * 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.Arrays; + +import javax.annotation.PostConstruct; + +import org.springframework.beans.factory.annotation.Value; +import org.springframework.http.MediaType; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestMethod; +import org.springframework.web.bind.annotation.RestController; + +import ch.ethz.seb.sebserver.gbl.api.API; +import ch.ethz.seb.sebserver.gbl.api.ExamAPIDiscovery; +import ch.ethz.seb.sebserver.gbl.profile.WebServiceProfile; +import ch.ethz.seb.sebserver.webservice.servicelayer.sebconfig.SebClientConfigService; + +@WebServiceProfile +@RestController +@RequestMapping("${sebserver.webservice.api.exam.endpoint.discovery}") +public class ExamAPIDiscoveryController { + + private final SebClientConfigService sebClientConfigService; + private final String examAPI_V1_Endpoint; + + protected ExamAPIDiscoveryController( + final SebClientConfigService sebClientConfigService, + @Value("${sebserver.webservice.api.exam.endpoint.v1}") final String examAPI_V1_Endpoint) { + + this.sebClientConfigService = sebClientConfigService; + this.examAPI_V1_Endpoint = examAPI_V1_Endpoint; + } + + private ExamAPIDiscovery DISCOVERY_INFO; + + @PostConstruct + void init() { + this.DISCOVERY_INFO = new ExamAPIDiscovery( + "Safe Exam Browser Server / Exam API Description", + "This is a description of Safe Exam Browser Server's Exam API", + this.sebClientConfigService.getServerURL(), + Arrays.asList(new ExamAPIDiscovery.ExamAPIVersion( + "v1", + Arrays.asList( + new ExamAPIDiscovery.Endpoint( + "access-token-endpoint", + "request OAuth2 access token with client credentials grant", + API.OAUTH_TOKEN_ENDPOINT, + "Basic"), + new ExamAPIDiscovery.Endpoint( + "seb-handshake-endpoint", + "endpoint to establish SEB - SEB Server connection", + this.examAPI_V1_Endpoint + API.EXAM_API_HANDSHAKE_ENDPOINT, + "Bearer"), + new ExamAPIDiscovery.Endpoint( + "seb-configuration-endpoint", + "endpoint to get SEB exam configuration in exchange of connection-token and exam identifier", + this.examAPI_V1_Endpoint + API.EXAM_API_CONFIGURATION_REQUEST_ENDPOINT, + "Bearer"), + new ExamAPIDiscovery.Endpoint( + "seb-ping-endpoint", + "endpoint to send pings to while running exam", + this.examAPI_V1_Endpoint + API.EXAM_API_PING_ENDPOINT, + "Bearer"), + new ExamAPIDiscovery.Endpoint( + "seb-ping-endpoint", + "endpoint to send log events to while running exam", + this.examAPI_V1_Endpoint + API.EXAM_API_EVENT_ENDPOINT, + "Bearer"))))); + } + + @RequestMapping( + method = RequestMethod.GET, + produces = MediaType.APPLICATION_JSON_UTF8_VALUE) + public ExamAPIDiscovery getDiscovery() { + return this.DISCOVERY_INFO; + } + +} diff --git a/src/main/java/ch/ethz/seb/sebserver/webservice/weblayer/api/ExamAPIController.java b/src/main/java/ch/ethz/seb/sebserver/webservice/weblayer/api/ExamAPI_V1_Controller.java similarity index 72% rename from src/main/java/ch/ethz/seb/sebserver/webservice/weblayer/api/ExamAPIController.java rename to src/main/java/ch/ethz/seb/sebserver/webservice/weblayer/api/ExamAPI_V1_Controller.java index bf5d0cb0..cd7b012e 100644 --- a/src/main/java/ch/ethz/seb/sebserver/webservice/weblayer/api/ExamAPIController.java +++ b/src/main/java/ch/ethz/seb/sebserver/webservice/weblayer/api/ExamAPI_V1_Controller.java @@ -8,37 +8,44 @@ package ch.ethz.seb.sebserver.webservice.weblayer.api; +import java.util.Arrays; +import java.util.Collection; + import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; +import org.springframework.http.HttpStatus; import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMethod; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.servlet.mvc.method.annotation.StreamingResponseBody; import ch.ethz.seb.sebserver.gbl.api.API; import ch.ethz.seb.sebserver.gbl.model.seb.PingResponse; -import ch.ethz.seb.sebserver.gbl.model.seb.RunningExams; +import ch.ethz.seb.sebserver.gbl.model.seb.RunningExam; import ch.ethz.seb.sebserver.gbl.profile.WebServiceProfile; +import ch.ethz.seb.sebserver.gbl.util.Utils; @WebServiceProfile @RestController -@RequestMapping("/${sebserver.webservice.api.exam.endpoint}") -public class ExamAPIController { +@RequestMapping("${sebserver.webservice.api.exam.endpoint.v1}") +public class ExamAPI_V1_Controller { @RequestMapping( path = API.EXAM_API_HANDSHAKE_ENDPOINT, method = RequestMethod.GET, consumes = MediaType.APPLICATION_FORM_URLENCODED_VALUE, produces = MediaType.APPLICATION_JSON_UTF8_VALUE) - public RunningExams handshake( + public Collection handshake( @RequestParam(name = API.PARAM_INSTITUTION_ID, required = true) final Long institutionId, final HttpServletRequest request, final HttpServletResponse response) { // TODO - return null; + return Arrays.asList(new RunningExam("1", "testExam", "TODO")); } @RequestMapping( @@ -46,14 +53,15 @@ public class ExamAPIController { method = RequestMethod.GET, consumes = MediaType.APPLICATION_FORM_URLENCODED_VALUE, produces = MediaType.TEXT_XML_VALUE) - public RunningExams getConfig( + public ResponseEntity getConfig( @RequestParam(name = API.EXAM_API_SEB_CONNECTION_TOKEN, required = true) final String connectionToken, - @RequestParam(name = API.EXAM_API_PARAM_EXAM_ID, required = true) final String examId, - final HttpServletRequest request, - final HttpServletResponse response) { + @RequestParam(name = API.EXAM_API_PARAM_EXAM_ID, required = true) final String examId) { // TODO - return null; + // 1. check connection validity (connection token) + // 2. get and stream SEB Exam configuration for specified exam (Id) + final StreamingResponseBody stream = out -> out.write(Utils.toByteArray("TODO SEB Config")); + return new ResponseEntity<>(stream, HttpStatus.OK); } @RequestMapping( diff --git a/src/main/java/ch/ethz/seb/sebserver/webservice/weblayer/api/ExamAdministrationController.java b/src/main/java/ch/ethz/seb/sebserver/webservice/weblayer/api/ExamAdministrationController.java index ecde95cd..d6f9a1c6 100644 --- a/src/main/java/ch/ethz/seb/sebserver/webservice/weblayer/api/ExamAdministrationController.java +++ b/src/main/java/ch/ethz/seb/sebserver/webservice/weblayer/api/ExamAdministrationController.java @@ -54,7 +54,7 @@ import ch.ethz.seb.sebserver.webservice.servicelayer.validation.BeanValidationSe @WebServiceProfile @RestController -@RequestMapping("/${sebserver.webservice.api.admin.endpoint}" + API.EXAM_ADMINISTRATION_ENDPOINT) +@RequestMapping("${sebserver.webservice.api.admin.endpoint}" + API.EXAM_ADMINISTRATION_ENDPOINT) public class ExamAdministrationController extends ActivatableEntityController { private final ExamDAO examDAO; diff --git a/src/main/java/ch/ethz/seb/sebserver/webservice/weblayer/api/ExamConfigurationMappingController.java b/src/main/java/ch/ethz/seb/sebserver/webservice/weblayer/api/ExamConfigurationMappingController.java index 7d637bb5..9981f48a 100644 --- a/src/main/java/ch/ethz/seb/sebserver/webservice/weblayer/api/ExamConfigurationMappingController.java +++ b/src/main/java/ch/ethz/seb/sebserver/webservice/weblayer/api/ExamConfigurationMappingController.java @@ -35,7 +35,7 @@ import ch.ethz.seb.sebserver.webservice.servicelayer.validation.BeanValidationSe @WebServiceProfile @RestController -@RequestMapping("/${sebserver.webservice.api.admin.endpoint}" + API.EXAM_CONFIGURATION_MAP_ENDPOINT) +@RequestMapping("${sebserver.webservice.api.admin.endpoint}" + API.EXAM_CONFIGURATION_MAP_ENDPOINT) public class ExamConfigurationMappingController extends EntityController { private final ExamDAO examDao; diff --git a/src/main/java/ch/ethz/seb/sebserver/webservice/weblayer/api/IndicatorController.java b/src/main/java/ch/ethz/seb/sebserver/webservice/weblayer/api/IndicatorController.java index f6cacf45..117b6699 100644 --- a/src/main/java/ch/ethz/seb/sebserver/webservice/weblayer/api/IndicatorController.java +++ b/src/main/java/ch/ethz/seb/sebserver/webservice/weblayer/api/IndicatorController.java @@ -31,7 +31,7 @@ import ch.ethz.seb.sebserver.webservice.servicelayer.validation.BeanValidationSe @WebServiceProfile @RestController -@RequestMapping("/${sebserver.webservice.api.admin.endpoint}" + API.EXAM_INDICATOR_ENDPOINT) +@RequestMapping("${sebserver.webservice.api.admin.endpoint}" + API.EXAM_INDICATOR_ENDPOINT) public class IndicatorController extends EntityController { private final ExamDAO examDao; diff --git a/src/main/java/ch/ethz/seb/sebserver/webservice/weblayer/api/InfoController.java b/src/main/java/ch/ethz/seb/sebserver/webservice/weblayer/api/InfoController.java index fc40e329..4edcd0cc 100644 --- a/src/main/java/ch/ethz/seb/sebserver/webservice/weblayer/api/InfoController.java +++ b/src/main/java/ch/ethz/seb/sebserver/webservice/weblayer/api/InfoController.java @@ -24,7 +24,7 @@ import ch.ethz.seb.sebserver.webservice.servicelayer.dao.InstitutionDAO; @WebServiceProfile @RestController -@RequestMapping("/${sebserver.webservice.api.admin.endpoint}" + API.INFO_ENDPOINT) +@RequestMapping("${sebserver.webservice.api.admin.endpoint}" + API.INFO_ENDPOINT) public class InfoController { private final InstitutionDAO institutionDAO; diff --git a/src/main/java/ch/ethz/seb/sebserver/webservice/weblayer/api/InstitutionController.java b/src/main/java/ch/ethz/seb/sebserver/webservice/weblayer/api/InstitutionController.java index 742986dd..1b936e49 100644 --- a/src/main/java/ch/ethz/seb/sebserver/webservice/weblayer/api/InstitutionController.java +++ b/src/main/java/ch/ethz/seb/sebserver/webservice/weblayer/api/InstitutionController.java @@ -28,7 +28,7 @@ import ch.ethz.seb.sebserver.webservice.servicelayer.validation.BeanValidationSe @WebServiceProfile @RestController -@RequestMapping("/${sebserver.webservice.api.admin.endpoint}" + API.INSTITUTION_ENDPOINT) +@RequestMapping("${sebserver.webservice.api.admin.endpoint}" + API.INSTITUTION_ENDPOINT) public class InstitutionController extends ActivatableEntityController { private final InstitutionDAO institutionDAO; diff --git a/src/main/java/ch/ethz/seb/sebserver/webservice/weblayer/api/LmsSetupController.java b/src/main/java/ch/ethz/seb/sebserver/webservice/weblayer/api/LmsSetupController.java index 7e6422dc..8db75729 100644 --- a/src/main/java/ch/ethz/seb/sebserver/webservice/weblayer/api/LmsSetupController.java +++ b/src/main/java/ch/ethz/seb/sebserver/webservice/weblayer/api/LmsSetupController.java @@ -22,9 +22,9 @@ import org.springframework.web.bind.annotation.RestController; import ch.ethz.seb.sebserver.gbl.api.API; import ch.ethz.seb.sebserver.gbl.api.APIMessage.APIMessageException; -import ch.ethz.seb.sebserver.gbl.api.authorization.PrivilegeType; import ch.ethz.seb.sebserver.gbl.api.EntityType; import ch.ethz.seb.sebserver.gbl.api.POSTMapper; +import ch.ethz.seb.sebserver.gbl.api.authorization.PrivilegeType; import ch.ethz.seb.sebserver.gbl.model.Entity; import ch.ethz.seb.sebserver.gbl.model.institution.LmsSetup; import ch.ethz.seb.sebserver.gbl.model.institution.LmsSetupTestResult; @@ -43,7 +43,7 @@ import ch.ethz.seb.sebserver.webservice.servicelayer.validation.BeanValidationSe @WebServiceProfile @RestController -@RequestMapping("/${sebserver.webservice.api.admin.endpoint}" + API.LMS_SETUP_ENDPOINT) +@RequestMapping("${sebserver.webservice.api.admin.endpoint}" + API.LMS_SETUP_ENDPOINT) public class LmsSetupController extends ActivatableEntityController { private final LmsAPIService lmsAPIService; diff --git a/src/main/java/ch/ethz/seb/sebserver/webservice/weblayer/api/OrientationController.java b/src/main/java/ch/ethz/seb/sebserver/webservice/weblayer/api/OrientationController.java index 903e7624..cc834963 100644 --- a/src/main/java/ch/ethz/seb/sebserver/webservice/weblayer/api/OrientationController.java +++ b/src/main/java/ch/ethz/seb/sebserver/webservice/weblayer/api/OrientationController.java @@ -30,7 +30,7 @@ import ch.ethz.seb.sebserver.webservice.servicelayer.validation.BeanValidationSe @WebServiceProfile @RestController -@RequestMapping("/${sebserver.webservice.api.admin.endpoint}" + API.ORIENTATION_ENDPOINT) +@RequestMapping("${sebserver.webservice.api.admin.endpoint}" + API.ORIENTATION_ENDPOINT) public class OrientationController extends EntityController { private final ConfigurationAttributeDAO configurationAttributeDAO; diff --git a/src/main/java/ch/ethz/seb/sebserver/webservice/weblayer/api/QuizController.java b/src/main/java/ch/ethz/seb/sebserver/webservice/weblayer/api/QuizController.java index 520a7e83..5d432f6a 100644 --- a/src/main/java/ch/ethz/seb/sebserver/webservice/weblayer/api/QuizController.java +++ b/src/main/java/ch/ethz/seb/sebserver/webservice/weblayer/api/QuizController.java @@ -33,7 +33,7 @@ import ch.ethz.seb.sebserver.webservice.servicelayer.lms.LmsAPIService; @WebServiceProfile @RestController -@RequestMapping("/${sebserver.webservice.api.admin.endpoint}" + API.QUIZ_DISCOVERY_ENDPOINT) +@RequestMapping("${sebserver.webservice.api.admin.endpoint}" + API.QUIZ_DISCOVERY_ENDPOINT) public class QuizController { private final int defaultPageSize; diff --git a/src/main/java/ch/ethz/seb/sebserver/webservice/weblayer/api/SebClientConfigController.java b/src/main/java/ch/ethz/seb/sebserver/webservice/weblayer/api/SebClientConfigController.java index 1804f007..ad7a18ff 100644 --- a/src/main/java/ch/ethz/seb/sebserver/webservice/weblayer/api/SebClientConfigController.java +++ b/src/main/java/ch/ethz/seb/sebserver/webservice/weblayer/api/SebClientConfigController.java @@ -44,7 +44,7 @@ import ch.ethz.seb.sebserver.webservice.servicelayer.validation.BeanValidationSe @WebServiceProfile @RestController @EnableAsync -@RequestMapping("/${sebserver.webservice.api.admin.endpoint}" + API.SEB_CLIENT_CONFIG_ENDPOINT) +@RequestMapping("${sebserver.webservice.api.admin.endpoint}" + API.SEB_CLIENT_CONFIG_ENDPOINT) public class SebClientConfigController extends ActivatableEntityController { private final SebClientConfigService sebClientConfigService; diff --git a/src/main/java/ch/ethz/seb/sebserver/webservice/weblayer/api/UserActivityLogController.java b/src/main/java/ch/ethz/seb/sebserver/webservice/weblayer/api/UserActivityLogController.java index 0ccf1c12..220a615a 100644 --- a/src/main/java/ch/ethz/seb/sebserver/webservice/weblayer/api/UserActivityLogController.java +++ b/src/main/java/ch/ethz/seb/sebserver/webservice/weblayer/api/UserActivityLogController.java @@ -33,7 +33,7 @@ import ch.ethz.seb.sebserver.webservice.servicelayer.dao.UserActivityLogDAO; @WebServiceProfile @RestController -@RequestMapping("/${sebserver.webservice.api.admin.endpoint}" + API.USER_ACTIVITY_LOG_ENDPOINT) +@RequestMapping("${sebserver.webservice.api.admin.endpoint}" + API.USER_ACTIVITY_LOG_ENDPOINT) public class UserActivityLogController { private final UserActivityLogDAO userActivityLogDAO; diff --git a/src/main/java/ch/ethz/seb/sebserver/webservice/weblayer/api/ViewController.java b/src/main/java/ch/ethz/seb/sebserver/webservice/weblayer/api/ViewController.java index 102f00e1..a4d11daa 100644 --- a/src/main/java/ch/ethz/seb/sebserver/webservice/weblayer/api/ViewController.java +++ b/src/main/java/ch/ethz/seb/sebserver/webservice/weblayer/api/ViewController.java @@ -28,7 +28,7 @@ import ch.ethz.seb.sebserver.webservice.servicelayer.validation.BeanValidationSe @WebServiceProfile @RestController -@RequestMapping("/${sebserver.webservice.api.admin.endpoint}" + API.VIEW_ENDPOINT) +@RequestMapping("${sebserver.webservice.api.admin.endpoint}" + API.VIEW_ENDPOINT) public class ViewController extends EntityController { protected ViewController( diff --git a/src/main/java/ch/ethz/seb/sebserver/webservice/weblayer/oauth/WebClientDetailsService.java b/src/main/java/ch/ethz/seb/sebserver/webservice/weblayer/oauth/WebClientDetailsService.java index 50e3efbf..6bf5bb84 100644 --- a/src/main/java/ch/ethz/seb/sebserver/webservice/weblayer/oauth/WebClientDetailsService.java +++ b/src/main/java/ch/ethz/seb/sebserver/webservice/weblayer/oauth/WebClientDetailsService.java @@ -10,8 +10,6 @@ package ch.ethz.seb.sebserver.webservice.weblayer.oauth; import java.util.Collections; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.context.annotation.Lazy; @@ -23,6 +21,9 @@ import org.springframework.security.oauth2.provider.client.BaseClientDetails; import org.springframework.stereotype.Component; import ch.ethz.seb.sebserver.WebSecurityConfig; +import ch.ethz.seb.sebserver.gbl.util.Result; +import ch.ethz.seb.sebserver.gbl.util.Utils; +import ch.ethz.seb.sebserver.webservice.servicelayer.sebconfig.SebClientConfigService; /** A ClientDetailsService to manage different API clients of SEB Server webservice API. * @@ -34,16 +35,19 @@ import ch.ethz.seb.sebserver.WebSecurityConfig; @Component public class WebClientDetailsService implements ClientDetailsService { - private static final Logger log = LoggerFactory.getLogger(WebClientDetailsService.class); - + private final SebClientConfigService sebClientConfigService; private final AdminAPIClientDetails adminClientDetails; @Autowired @Qualifier(WebSecurityConfig.CLIENT_PASSWORD_ENCODER_BEAN_NAME) private PasswordEncoder clientPasswordEncoder; // TODO inject a collection of BaseClientDetails here to allow multiple admin client configurations - public WebClientDetailsService(final AdminAPIClientDetails adminClientDetails) { + public WebClientDetailsService( + final AdminAPIClientDetails adminClientDetails, + final SebClientConfigService sebClientConfigService) { + this.adminClientDetails = adminClientDetails; + this.sebClientConfigService = sebClientConfigService; } /** Load a client by the client id. This method must not return null. @@ -67,25 +71,24 @@ public class WebClientDetailsService implements ClientDetailsService { return this.adminClientDetails; } - return getForExamClientAPI(clientId); + return getForExamClientAPI(clientId) + .getOrThrow(); } - private ClientDetails getForExamClientAPI(final String clientId) { - // TODO create ClientDetails from matching Institution - if ("test".equals(clientId)) { - final BaseClientDetails baseClientDetails = new BaseClientDetails( - clientId, - WebserviceResourceConfiguration.EXAM_API_RESOURCE_ID, - null, - "client_credentials", - ""); - baseClientDetails.setScope(Collections.emptySet()); - baseClientDetails.setClientSecret(this.clientPasswordEncoder.encode("test")); - return baseClientDetails; - } + protected Result getForExamClientAPI(final String clientId) { + return this.sebClientConfigService.getEncodedClientSecret(clientId) + .map(pwd -> { + final BaseClientDetails baseClientDetails = new BaseClientDetails( + Utils.toString(clientId), + WebserviceResourceConfiguration.EXAM_API_RESOURCE_ID, + null, + "client_credentials", + ""); - log.warn("ClientDetails for clientId: {} not found", clientId); - throw new ClientRegistrationException("clientId not found"); + baseClientDetails.setScope(Collections.emptySet()); + baseClientDetails.setClientSecret(pwd); + return baseClientDetails; + }); } } diff --git a/src/main/resources/config/application-dev-ws.properties b/src/main/resources/config/application-dev-ws.properties index ea67b7a9..e7a86205 100644 --- a/src/main/resources/config/application-dev-ws.properties +++ b/src/main/resources/config/application-dev-ws.properties @@ -13,7 +13,9 @@ sebserver.webservice.http.scheme=http sebserver.webservice.api.admin.endpoint=/admin-api/v1 sebserver.webservice.api.admin.accessTokenValiditySeconds=1800 sebserver.webservice.api.admin.refreshTokenValiditySeconds=-1 -sebserver.webservice.api.exam.endpoint=/exam-api/v1 +sebserver.webservice.api.exam.endpoint=/exam-api +sebserver.webservice.api.exam.endpoint.discovery=${sebserver.webservice.api.exam.endpoint}/discovery +sebserver.webservice.api.exam.endpoint.v1=${sebserver.webservice.api.exam.endpoint}/v1 sebserver.webservice.api.exam.accessTokenValiditySeconds=1800 sebserver.webservice.api.exam.refreshTokenValiditySeconds=-1 diff --git a/src/test/java/ch/ethz/seb/sebserver/webservice/integration/api/exam/ExamAPIIntegrationTester.java b/src/test/java/ch/ethz/seb/sebserver/webservice/integration/api/exam/ExamAPIIntegrationTester.java index 154200ac..8290ea19 100644 --- a/src/test/java/ch/ethz/seb/sebserver/webservice/integration/api/exam/ExamAPIIntegrationTester.java +++ b/src/test/java/ch/ethz/seb/sebserver/webservice/integration/api/exam/ExamAPIIntegrationTester.java @@ -13,13 +13,21 @@ import static org.springframework.test.web.servlet.request.MockMvcRequestBuilder import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; +import java.util.Collections; + import org.junit.Before; import org.junit.runner.RunWith; +import org.mockito.Mockito; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.beans.factory.annotation.Value; import org.springframework.boot.json.JacksonJsonParser; import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.security.crypto.password.PasswordEncoder; +import org.springframework.security.oauth2.provider.ClientDetails; +import org.springframework.security.oauth2.provider.client.BaseClientDetails; import org.springframework.security.web.FilterChainProxy; import org.springframework.test.context.ActiveProfiles; import org.springframework.test.context.junit4.SpringRunner; @@ -31,17 +39,22 @@ import org.springframework.util.MultiValueMap; import org.springframework.web.context.WebApplicationContext; import ch.ethz.seb.sebserver.SEBServer; +import ch.ethz.seb.sebserver.WebSecurityConfig; import ch.ethz.seb.sebserver.gbl.api.JSONMapper; +import ch.ethz.seb.sebserver.webservice.servicelayer.sebconfig.SebClientConfigService; +import ch.ethz.seb.sebserver.webservice.weblayer.oauth.AdminAPIClientDetails; +import ch.ethz.seb.sebserver.webservice.weblayer.oauth.WebClientDetailsService; +import ch.ethz.seb.sebserver.webservice.weblayer.oauth.WebserviceResourceConfiguration; @RunWith(SpringRunner.class) @SpringBootTest( - classes = SEBServer.class, - webEnvironment = SpringBootTest.WebEnvironment.DEFINED_PORT) + classes = { SEBServer.class }, + webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) @ActiveProfiles("test") @AutoConfigureMockMvc public abstract class ExamAPIIntegrationTester { - @Value("${sebserver.webservice.api.exam.endpoint}") + @Value("${sebserver.webservice.api.exam.endpoint.v1}") protected String endpoint; @Autowired @@ -53,10 +66,28 @@ public abstract class ExamAPIIntegrationTester { protected MockMvc mockMvc; + @MockBean + public WebClientDetailsService webClientDetailsService; + @Before public void setup() { this.mockMvc = MockMvcBuilders.webAppContextSetup(this.wac) .addFilter(this.springSecurityFilterChain).build(); + Mockito.when(this.webClientDetailsService.loadClientByClientId(Mockito.anyString())).thenReturn( + getForExamClientAPI()); + } + + protected ClientDetails getForExamClientAPI() { + final BaseClientDetails baseClientDetails = new BaseClientDetails( + "test", + WebserviceResourceConfiguration.EXAM_API_RESOURCE_ID, + null, + "client_credentials", + ""); + baseClientDetails.setScope(Collections.emptySet()); + baseClientDetails + .setClientSecret(ExamAPIIntegrationTester.this.clientPasswordEncoder.encode("test")); + return baseClientDetails; } protected String obtainAccessToken( @@ -82,4 +113,12 @@ public abstract class ExamAPIIntegrationTester { return jsonParser.parseMap(resultString).get("access_token").toString(); } + @Autowired + AdminAPIClientDetails adminClientDetails; + @Autowired + SebClientConfigService sebClientConfigService; + @Autowired + @Qualifier(WebSecurityConfig.CLIENT_PASSWORD_ENCODER_BEAN_NAME) + private PasswordEncoder clientPasswordEncoder; + } diff --git a/src/test/java/ch/ethz/seb/sebserver/webservice/integration/api/exam/ExamAPITestController.java b/src/test/java/ch/ethz/seb/sebserver/webservice/integration/api/exam/ExamAPITestController.java index 109ad986..3a05f540 100644 --- a/src/test/java/ch/ethz/seb/sebserver/webservice/integration/api/exam/ExamAPITestController.java +++ b/src/test/java/ch/ethz/seb/sebserver/webservice/integration/api/exam/ExamAPITestController.java @@ -18,7 +18,7 @@ import org.springframework.web.bind.annotation.RestController; import ch.ethz.seb.sebserver.gbl.profile.WebServiceProfile; @RestController -@RequestMapping("${sebserver.webservice.api.exam.endpoint}") +@RequestMapping("${sebserver.webservice.api.exam.endpoint.v1}") @WebServiceProfile public class ExamAPITestController { diff --git a/src/test/java/ch/ethz/seb/sebserver/webservice/integration/api/exam/ExamDiscoveryEndpointTest.java b/src/test/java/ch/ethz/seb/sebserver/webservice/integration/api/exam/ExamDiscoveryEndpointTest.java new file mode 100644 index 00000000..daa4bc3b --- /dev/null +++ b/src/test/java/ch/ethz/seb/sebserver/webservice/integration/api/exam/ExamDiscoveryEndpointTest.java @@ -0,0 +1,76 @@ +/* + * Copyright (c) 2019 ETH Zürich, Educational Development and Technology (LET) + * + * 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.integration.api.exam; + +import static org.junit.Assert.assertEquals; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +import org.junit.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; + +import ch.ethz.seb.sebserver.gbl.api.JSONMapper; + +public class ExamDiscoveryEndpointTest extends ExamAPIIntegrationTester { + + @Value("${sebserver.webservice.api.exam.endpoint.discovery}") + private String discoveryEndpoint; + @Autowired + private JSONMapper jsonMapper; + + @Test + public void testExamDiscoveryEndpoint() throws Exception { + // no authorization needed here + + final String contentAsString = this.mockMvc.perform(get(this.discoveryEndpoint)) + .andExpect(status().isOk()) + .andReturn().getResponse().getContentAsString(); + + final Object json = this.jsonMapper.readValue(contentAsString, Object.class); + assertEquals( + "{\r\n" + + " \"title\" : \"Safe Exam Browser Server / Exam API Description\",\r\n" + + " \"description\" : \"This is a description of Safe Exam Browser Server's Exam API\",\r\n" + + " \"server-location\" : \"http://localhost:0\",\r\n" + + " \"api-versions\" : [ {\r\n" + + " \"name\" : \"v1\",\r\n" + + " \"endpoints\" : [ {\r\n" + + " \"name\" : \"access-token-endpoint\",\r\n" + + " \"descripiton\" : \"request OAuth2 access token with client credentials grant\",\r\n" + + " \"location\" : \"/oauth/token\",\r\n" + + " \"authorization\" : \"Basic\"\r\n" + + " }, {\r\n" + + " \"name\" : \"seb-handshake-endpoint\",\r\n" + + " \"descripiton\" : \"endpoint to establish SEB - SEB Server connection\",\r\n" + + " \"location\" : \"/exam-api/v1/handshake\",\r\n" + + " \"authorization\" : \"Bearer\"\r\n" + + " }, {\r\n" + + " \"name\" : \"seb-configuration-endpoint\",\r\n" + + " \"descripiton\" : \"endpoint to get SEB exam configuration in exchange of connection-token and exam identifier\",\r\n" + + + " \"location\" : \"/exam-api/v1/examconfig\",\r\n" + + " \"authorization\" : \"Bearer\"\r\n" + + " }, {\r\n" + + " \"name\" : \"seb-ping-endpoint\",\r\n" + + " \"descripiton\" : \"endpoint to send pings to while running exam\",\r\n" + + " \"location\" : \"/exam-api/v1/sebping\",\r\n" + + " \"authorization\" : \"Bearer\"\r\n" + + " }, {\r\n" + + " \"name\" : \"seb-ping-endpoint\",\r\n" + + " \"descripiton\" : \"endpoint to send log events to while running exam\",\r\n" + + " \"location\" : \"/exam-api/v1/seblog\",\r\n" + + " \"authorization\" : \"Bearer\"\r\n" + + " } ]\r\n" + + " } ]\r\n" + + "}", + this.jsonMapper.writerWithDefaultPrettyPrinter().writeValueAsString(json)); + } + +} diff --git a/src/test/resources/application-test.properties b/src/test/resources/application-test.properties index ded264ae..7f9e4f25 100644 --- a/src/test/resources/application-test.properties +++ b/src/test/resources/application-test.properties @@ -2,6 +2,8 @@ server.address=localhost server.port=8080 server.servlet.context-path=/ +spring.main.allow-bean-definition-overriding=true + spring.h2.console.enabled=true spring.datasource.platform=h2 spring.datasource.url=jdbc:h2:mem:test;MODE=MySQL;DB_CLOSE_DELAY=-1;DB_CLOSE_ON_EXIT=FALSE @@ -15,6 +17,8 @@ sebserver.webservice.api.admin.endpoint=/admin-api sebserver.webservice.api.admin.accessTokenValiditySeconds=1800 sebserver.webservice.api.admin.refreshTokenValiditySeconds=-1 sebserver.webservice.api.exam.endpoint=/exam-api +sebserver.webservice.api.exam.endpoint.discovery=${sebserver.webservice.api.exam.endpoint}/discovery +sebserver.webservice.api.exam.endpoint.v1=${sebserver.webservice.api.exam.endpoint}/v1 sebserver.webservice.api.exam.accessTokenValiditySeconds=1800 sebserver.webservice.api.exam.refreshTokenValiditySeconds=-1 sebserver.webservice.internalSecret=TO_SET