SEBSERV-44 added Exam API endpoints and discovery

This commit is contained in:
anhefti 2019-06-03 11:44:55 +02:00
parent 985e1ae386
commit e90bf79034
34 changed files with 499 additions and 84 deletions

View file

@ -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")

View file

@ -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";
}

View file

@ -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<ExamAPIVersion> versions;
public ExamAPIDiscovery(
final String title,
final String description,
final String serverLocation,
final Collection<ExamAPIVersion> 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<Endpoint> endpoints;
public ExamAPIVersion(
final String name,
final Collection<Endpoint> 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;
}
}
}

View file

@ -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;
}
}

View file

@ -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
}

View file

@ -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();
}
}

View file

@ -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,

View file

@ -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),
;

View file

@ -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())));
}

View file

@ -40,6 +40,8 @@ public interface SebClientConfigService {
" </dict>\r\n" +
"</plist>";
String getServerURL();
boolean hasSebClientConfigurationForInstitution(Long institutionId);
Result<SebClientConfig> autoCreateSebClientConfigurationForInstitution(Long institutionId);
@ -48,4 +50,6 @@ public interface SebClientConfigService {
OutputStream out,
final String modelId);
Result<String> getEncodedClientSecret(String clientId);
}

View file

@ -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<String> getEncodedClientSecret(final String clientId) {
return Result.tryCatch(() -> {
final Collection<SebClientConfig> 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<SebClientConfig> 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,

View file

@ -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<ConfigurationAttribute, ConfigurationAttribute> {
protected ConfigurationAttributeController(

View file

@ -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<Configuration, Configuration> {
private final ConfigurationDAO configurationDAO;

View file

@ -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<ConfigurationNode, ConfigurationNode> {
private final ConfigurationDAO configurationDAO;

View file

@ -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<ConfigurationValue, ConfigurationValue> {
private final ConfigurationDAO configurationDAO;

View file

@ -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;
}
}

View file

@ -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<RunningExam> 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<StreamingResponseBody> 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(

View file

@ -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<Exam, Exam> {
private final ExamDAO examDAO;

View file

@ -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<ExamConfigurationMap, ExamConfigurationMap> {
private final ExamDAO examDao;

View file

@ -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<Indicator, Indicator> {
private final ExamDAO examDao;

View file

@ -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;

View file

@ -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<Institution, Institution> {
private final InstitutionDAO institutionDAO;

View file

@ -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<LmsSetup, LmsSetup> {
private final LmsAPIService lmsAPIService;

View file

@ -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<Orientation, Orientation> {
private final ConfigurationAttributeDAO configurationAttributeDAO;

View file

@ -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;

View file

@ -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<SebClientConfig, SebClientConfig> {
private final SebClientConfigService sebClientConfigService;

View file

@ -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;

View file

@ -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<View, View> {
protected ViewController(

View file

@ -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<ClientDetails> 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;
});
}
}

View file

@ -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

View file

@ -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;
}

View file

@ -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 {

View file

@ -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));
}
}

View file

@ -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