first implementation of Open edX APITemplate
This commit is contained in:
parent
3032720f28
commit
2855f93a55
16 changed files with 725 additions and 134 deletions
|
@ -8,18 +8,32 @@
|
|||
|
||||
package ch.ethz.seb.sebserver;
|
||||
|
||||
import java.io.FileNotFoundException;
|
||||
import java.io.IOException;
|
||||
import java.net.HttpURLConnection;
|
||||
import java.security.KeyManagementException;
|
||||
import java.security.KeyStoreException;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.security.cert.CertificateException;
|
||||
|
||||
import javax.net.ssl.SSLContext;
|
||||
import javax.servlet.ServletException;
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import javax.servlet.http.HttpServletResponse;
|
||||
|
||||
import org.apache.http.client.HttpClient;
|
||||
import org.apache.http.impl.client.HttpClients;
|
||||
import org.apache.http.ssl.SSLContextBuilder;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.boot.web.servlet.error.ErrorController;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.core.annotation.Order;
|
||||
import org.springframework.core.env.Environment;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.http.client.ClientHttpRequestFactory;
|
||||
import org.springframework.http.client.HttpComponentsClientHttpRequestFactory;
|
||||
import org.springframework.http.client.SimpleClientHttpRequestFactory;
|
||||
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
|
||||
import org.springframework.security.config.annotation.web.builders.WebSecurity;
|
||||
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
|
||||
|
@ -28,10 +42,15 @@ import org.springframework.security.core.AuthenticationException;
|
|||
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
|
||||
import org.springframework.security.crypto.password.PasswordEncoder;
|
||||
import org.springframework.security.web.AuthenticationEntryPoint;
|
||||
import org.springframework.util.ResourceUtils;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
import ch.ethz.seb.sebserver.gbl.profile.DevGuiProfile;
|
||||
import ch.ethz.seb.sebserver.gbl.profile.DevWebServiceProfile;
|
||||
import ch.ethz.seb.sebserver.gbl.profile.GuiProfile;
|
||||
import ch.ethz.seb.sebserver.gbl.profile.ProdGuiProfile;
|
||||
import ch.ethz.seb.sebserver.gbl.profile.ProdWebServiceProfile;
|
||||
import ch.ethz.seb.sebserver.gbl.profile.WebServiceProfile;
|
||||
|
||||
/** This is the overall seb-server Spring web-configuration that is loaded for all profiles.
|
||||
|
@ -115,4 +134,64 @@ public class WebSecurityConfig extends WebSecurityConfigurerAdapter implements E
|
|||
return "/error";
|
||||
}
|
||||
|
||||
/** A ClientHttpRequestFactory for development profile with no TSL SSL protocol and
|
||||
* not following redirects on redirect responses.
|
||||
*
|
||||
* @return ClientHttpRequestFactory bean for development profiles */
|
||||
@Bean
|
||||
@DevGuiProfile
|
||||
@DevWebServiceProfile
|
||||
public ClientHttpRequestFactory clientHttpRequestFactory() {
|
||||
// TODO set connection and read timeout!? configurable!?
|
||||
return new SimpleClientHttpRequestFactory() {
|
||||
|
||||
@Override
|
||||
protected void prepareConnection(final HttpURLConnection connection, final String httpMethod)
|
||||
throws IOException {
|
||||
super.prepareConnection(connection, httpMethod);
|
||||
connection.setInstanceFollowRedirects(false);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/** A ClientHttpRequestFactory used in production with TSL SSL configuration.
|
||||
*
|
||||
* NOTE:
|
||||
* environment property: sebserver.gui.truststore.pwd is expected to have the correct truststore password set
|
||||
* environment property: sebserver.gui.truststore.type is expected to set to the correct type of truststore
|
||||
* truststore.jks is expected to be on the classpath containing all trusted certificates for request
|
||||
* to SSL secured SEB Server webservice
|
||||
*
|
||||
* @return ClientHttpRequestFactory with TLS / SSL configuration
|
||||
* @throws IOException
|
||||
* @throws FileNotFoundException
|
||||
* @throws CertificateException
|
||||
* @throws KeyStoreException
|
||||
* @throws NoSuchAlgorithmException
|
||||
* @throws KeyManagementException */
|
||||
@Bean
|
||||
@ProdGuiProfile
|
||||
@ProdWebServiceProfile
|
||||
public ClientHttpRequestFactory clientHttpRequestFactoryTLS(final Environment env) throws KeyManagementException,
|
||||
NoSuchAlgorithmException, KeyStoreException, CertificateException, FileNotFoundException, IOException {
|
||||
|
||||
final char[] password = env
|
||||
.getProperty("sebserver.gui.truststore.pwd")
|
||||
.toCharArray();
|
||||
|
||||
final SSLContext sslContext = SSLContextBuilder
|
||||
.create()
|
||||
.loadTrustMaterial(ResourceUtils.getFile(
|
||||
"classpath:truststore.jks"),
|
||||
password)
|
||||
.build();
|
||||
|
||||
final HttpClient client = HttpClients.custom()
|
||||
.setSSLContext(sslContext)
|
||||
.build();
|
||||
|
||||
// TODO set connection and read timeout!? configurable!?
|
||||
return new HttpComponentsClientHttpRequestFactory(client);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -8,24 +8,93 @@
|
|||
|
||||
package ch.ethz.seb.sebserver.gbl.model.institution;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.Collection;
|
||||
import java.util.Set;
|
||||
|
||||
import javax.validation.constraints.NotNull;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
|
||||
public class LmsSetupTestResult {
|
||||
import ch.ethz.seb.sebserver.gbl.util.Utils;
|
||||
|
||||
public static final String ATTR_OK = "ok";
|
||||
public final class LmsSetupTestResult {
|
||||
|
||||
@JsonProperty(ATTR_OK)
|
||||
public static final String ATTR_OK_STATUS = "okStatus";
|
||||
public static final String ATTR_MISSING_ATTRIBUTE = "missingLMSSetupAttribute";
|
||||
public static final String ATTR_ERROR_TOKEN_REQUEST = "tokenRequestError";
|
||||
public static final String ATTR_ERROR_QUIZ_REQUEST = "quizRequestError";
|
||||
|
||||
@JsonProperty(ATTR_OK_STATUS)
|
||||
@NotNull
|
||||
public final Boolean ok;
|
||||
public final Boolean okStatus;
|
||||
|
||||
// TODO
|
||||
@JsonProperty(ATTR_MISSING_ATTRIBUTE)
|
||||
public final Set<String> missingLMSSetupAttribute;
|
||||
|
||||
@JsonProperty(ATTR_MISSING_ATTRIBUTE)
|
||||
public final String tokenRequestError;
|
||||
|
||||
@JsonProperty(ATTR_MISSING_ATTRIBUTE)
|
||||
public final String quizRequestError;
|
||||
|
||||
public LmsSetupTestResult(
|
||||
@JsonProperty(value = ATTR_OK, required = true) final Boolean ok) {
|
||||
@JsonProperty(value = ATTR_OK_STATUS, required = true) final Boolean ok,
|
||||
@JsonProperty(ATTR_MISSING_ATTRIBUTE) final Collection<String> missingLMSSetupAttribute,
|
||||
@JsonProperty(ATTR_MISSING_ATTRIBUTE) final String tokenRequestError,
|
||||
@JsonProperty(ATTR_MISSING_ATTRIBUTE) final String quizRequestError) {
|
||||
|
||||
this.ok = ok;
|
||||
this.okStatus = ok;
|
||||
// TODO
|
||||
this.missingLMSSetupAttribute = Utils.immutableSetOf(missingLMSSetupAttribute);
|
||||
this.tokenRequestError = tokenRequestError;
|
||||
this.quizRequestError = quizRequestError;
|
||||
}
|
||||
|
||||
public Boolean getOkStatus() {
|
||||
return this.okStatus;
|
||||
}
|
||||
|
||||
public Set<String> getMissingLMSSetupAttribute() {
|
||||
return this.missingLMSSetupAttribute;
|
||||
}
|
||||
|
||||
public String getTokenRequestError() {
|
||||
return this.tokenRequestError;
|
||||
}
|
||||
|
||||
public String getQuizRequestError() {
|
||||
return this.quizRequestError;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "LmsSetupTestResult [okStatus=" + this.okStatus + ", missingLMSSetupAttribute="
|
||||
+ this.missingLMSSetupAttribute
|
||||
+ ", tokenRequestError=" + this.tokenRequestError + ", quizRequestError=" + this.quizRequestError + "]";
|
||||
}
|
||||
|
||||
public static final LmsSetupTestResult ofOkay() {
|
||||
return new LmsSetupTestResult(true, null, null, null);
|
||||
}
|
||||
|
||||
public static final LmsSetupTestResult ofMissingAttributes(final Collection<String> attrs) {
|
||||
return new LmsSetupTestResult(false, attrs, null, null);
|
||||
}
|
||||
|
||||
public static final LmsSetupTestResult ofMissingAttributes(final String... attrs) {
|
||||
if (attrs == null) {
|
||||
return new LmsSetupTestResult(false, null, null, null);
|
||||
}
|
||||
return new LmsSetupTestResult(false, Arrays.asList(attrs), null, null);
|
||||
}
|
||||
|
||||
public static final LmsSetupTestResult ofTokenRequestError(final String message) {
|
||||
return new LmsSetupTestResult(false, null, message, null);
|
||||
}
|
||||
|
||||
public static final LmsSetupTestResult ofQuizRequestError(final String message) {
|
||||
return new LmsSetupTestResult(false, null, null, message);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -21,6 +21,6 @@ import org.springframework.context.annotation.Profile;
|
|||
* and only for production and/or testing */
|
||||
@Target({ ElementType.TYPE, ElementType.METHOD })
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
@Profile({ "prod-gui", "test" })
|
||||
@Profile({ "prod-gui" })
|
||||
public @interface ProdGuiProfile {
|
||||
}
|
||||
|
|
|
@ -21,6 +21,6 @@ import org.springframework.context.annotation.Profile;
|
|||
* and only for production and/or testing */
|
||||
@Target({ ElementType.TYPE, ElementType.METHOD })
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
@Profile({ "prod-ws", "test" })
|
||||
@Profile({ "prod-ws" })
|
||||
public @interface ProdWebServiceProfile {
|
||||
}
|
||||
|
|
|
@ -1,96 +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.gui.service.remote.webservice;
|
||||
|
||||
import java.io.FileNotFoundException;
|
||||
import java.io.IOException;
|
||||
import java.net.HttpURLConnection;
|
||||
import java.security.KeyManagementException;
|
||||
import java.security.KeyStoreException;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.security.cert.CertificateException;
|
||||
|
||||
import javax.net.ssl.SSLContext;
|
||||
|
||||
import org.apache.http.client.HttpClient;
|
||||
import org.apache.http.impl.client.HttpClients;
|
||||
import org.apache.http.ssl.SSLContextBuilder;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.core.env.Environment;
|
||||
import org.springframework.http.client.ClientHttpRequestFactory;
|
||||
import org.springframework.http.client.HttpComponentsClientHttpRequestFactory;
|
||||
import org.springframework.http.client.SimpleClientHttpRequestFactory;
|
||||
import org.springframework.util.ResourceUtils;
|
||||
|
||||
import ch.ethz.seb.sebserver.gbl.profile.DevGuiProfile;
|
||||
import ch.ethz.seb.sebserver.gbl.profile.GuiProfile;
|
||||
import ch.ethz.seb.sebserver.gbl.profile.ProdGuiProfile;
|
||||
|
||||
@Configuration
|
||||
@GuiProfile
|
||||
public class WebserviceConnectionConfig {
|
||||
|
||||
/** A ClientHttpRequestFactory for development profile with no TSL SSL protocol and
|
||||
* not following redirects on redirect responses.
|
||||
*
|
||||
* @return ClientHttpRequestFactory bean for development profiles */
|
||||
@Bean
|
||||
@DevGuiProfile
|
||||
public ClientHttpRequestFactory clientHttpRequestFactory() {
|
||||
return new SimpleClientHttpRequestFactory() {
|
||||
|
||||
@Override
|
||||
protected void prepareConnection(final HttpURLConnection connection, final String httpMethod)
|
||||
throws IOException {
|
||||
super.prepareConnection(connection, httpMethod);
|
||||
connection.setInstanceFollowRedirects(false);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/** A ClientHttpRequestFactory used in production with TSL SSL configuration.
|
||||
*
|
||||
* NOTE:
|
||||
* environment property: sebserver.gui.truststore.pwd is expected to have the correct truststore password set
|
||||
* environment property: sebserver.gui.truststore.type is expected to set to the correct type of truststore
|
||||
* truststore.jks is expected to be on the classpath containing all trusted certificates for request
|
||||
* to SSL secured SEB Server webservice
|
||||
*
|
||||
* @return ClientHttpRequestFactory with TLS / SSL configuration
|
||||
* @throws IOException
|
||||
* @throws FileNotFoundException
|
||||
* @throws CertificateException
|
||||
* @throws KeyStoreException
|
||||
* @throws NoSuchAlgorithmException
|
||||
* @throws KeyManagementException */
|
||||
@Bean
|
||||
@ProdGuiProfile
|
||||
public ClientHttpRequestFactory clientHttpRequestFactoryTLS(final Environment env) throws KeyManagementException,
|
||||
NoSuchAlgorithmException, KeyStoreException, CertificateException, FileNotFoundException, IOException {
|
||||
|
||||
final char[] password = env
|
||||
.getProperty("sebserver.gui.truststore.pwd")
|
||||
.toCharArray();
|
||||
|
||||
final SSLContext sslContext = SSLContextBuilder
|
||||
.create()
|
||||
.loadTrustMaterial(ResourceUtils.getFile(
|
||||
"classpath:truststore.jks"),
|
||||
password)
|
||||
.build();
|
||||
|
||||
final HttpClient client = HttpClients.custom()
|
||||
.setSSLContext(sslContext)
|
||||
.build();
|
||||
|
||||
return new HttpComponentsClientHttpRequestFactory(client);
|
||||
}
|
||||
|
||||
}
|
|
@ -61,8 +61,8 @@ public class OAuth2AuthorizationContextHolder implements AuthorizationContextHol
|
|||
|
||||
@Autowired
|
||||
public OAuth2AuthorizationContextHolder(
|
||||
@Value("${sebserver.gui.webservice.clientId}") final String guiClientId,
|
||||
@Value("${sebserver.gui.webservice.clientSecret}") final String guiClientSecret,
|
||||
@Value("${sebserver.webservice.api.admin.clientId}") final String guiClientId,
|
||||
@Value("${sebserver.webservice.api.admin.clientId}") final String guiClientSecret,
|
||||
final WebserviceURIService webserviceURIService,
|
||||
final ClientHttpRequestFactory clientHttpRequestFactory) {
|
||||
|
||||
|
|
|
@ -0,0 +1,97 @@
|
|||
/*
|
||||
* 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.servicelayer;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.context.annotation.Lazy;
|
||||
import org.springframework.core.env.Environment;
|
||||
import org.springframework.security.crypto.encrypt.Encryptors;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import ch.ethz.seb.sebserver.gbl.profile.WebServiceProfile;
|
||||
|
||||
@Lazy
|
||||
@Service
|
||||
@WebServiceProfile
|
||||
public class InternalEncryptionService {
|
||||
|
||||
private static final Logger log = LoggerFactory.getLogger(InternalEncryptionService.class);
|
||||
|
||||
private static final String NO_SALT = "NO_SALT";
|
||||
|
||||
private final Environment environment;
|
||||
|
||||
protected InternalEncryptionService(final Environment environment) {
|
||||
this.environment = environment;
|
||||
}
|
||||
|
||||
public String encrypt(final String text) {
|
||||
try {
|
||||
return Encryptors.text(
|
||||
this.environment.getRequiredProperty("sebserver.webservice.internalSecret"),
|
||||
NO_SALT).encrypt(text);
|
||||
} catch (final Exception e) {
|
||||
log.error("Failed to encrypt text: ", e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public String decrypt(final String text) {
|
||||
try {
|
||||
return Encryptors.text(
|
||||
this.environment.getRequiredProperty("sebserver.webservice.internalSecret"),
|
||||
NO_SALT).decrypt(text);
|
||||
} catch (final Exception e) {
|
||||
log.error("Failed to decrypt text: ", e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public String encrypt(final String text, final CharSequence salt) {
|
||||
try {
|
||||
return Encryptors.text(
|
||||
this.environment.getRequiredProperty("sebserver.webservice.internalSecret"),
|
||||
salt).encrypt(text);
|
||||
} catch (final Exception e) {
|
||||
log.error("Failed to encrypt text: ", e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public String decrypt(final String text, final CharSequence salt) {
|
||||
try {
|
||||
return Encryptors.text(
|
||||
this.environment.getRequiredProperty("sebserver.webservice.internalSecret"),
|
||||
salt).decrypt(text);
|
||||
} catch (final Exception e) {
|
||||
log.error("Failed to decrypt text: ", e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public String encrypt(final String text, final CharSequence secret, final CharSequence salt) {
|
||||
try {
|
||||
return Encryptors.text(secret, salt).encrypt(text);
|
||||
} catch (final Exception e) {
|
||||
log.error("Failed to encrypt text: ", e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public String decrypt(final String text, final CharSequence secret, final CharSequence salt) {
|
||||
try {
|
||||
return Encryptors.text(secret, salt).decrypt(text);
|
||||
} catch (final Exception e) {
|
||||
log.error("Failed to decrypt text: ", e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -18,12 +18,16 @@ import java.util.function.Predicate;
|
|||
import java.util.stream.Collectors;
|
||||
|
||||
import org.apache.commons.lang3.BooleanUtils;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.mybatis.dynamic.sql.select.MyBatis3SelectModelAdapter;
|
||||
import org.mybatis.dynamic.sql.select.QueryExpressionDSL;
|
||||
import org.springframework.beans.factory.annotation.Qualifier;
|
||||
import org.springframework.context.annotation.Lazy;
|
||||
import org.springframework.security.crypto.password.PasswordEncoder;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
import ch.ethz.seb.sebserver.WebSecurityConfig;
|
||||
import ch.ethz.seb.sebserver.gbl.model.EntityKey;
|
||||
import ch.ethz.seb.sebserver.gbl.model.EntityType;
|
||||
import ch.ethz.seb.sebserver.gbl.model.institution.LmsSetup;
|
||||
|
@ -32,6 +36,7 @@ import ch.ethz.seb.sebserver.gbl.util.Result;
|
|||
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;
|
||||
import ch.ethz.seb.sebserver.webservice.servicelayer.InternalEncryptionService;
|
||||
import ch.ethz.seb.sebserver.webservice.servicelayer.bulkaction.BulkAction;
|
||||
import ch.ethz.seb.sebserver.webservice.servicelayer.dao.DAOLoggingSupport;
|
||||
import ch.ethz.seb.sebserver.webservice.servicelayer.dao.FilterMap;
|
||||
|
@ -44,9 +49,17 @@ import ch.ethz.seb.sebserver.webservice.servicelayer.dao.TransactionHandler;
|
|||
public class LmsSetupDAOImpl implements LmsSetupDAO {
|
||||
|
||||
private final LmsSetupRecordMapper lmsSetupRecordMapper;
|
||||
private final InternalEncryptionService internalEncryptionService;
|
||||
private final PasswordEncoder clientPasswordEncoder;
|
||||
|
||||
protected LmsSetupDAOImpl(
|
||||
final LmsSetupRecordMapper lmsSetupRecordMapper,
|
||||
final InternalEncryptionService internalEncryptionService,
|
||||
@Qualifier(WebSecurityConfig.CLIENT_PASSWORD_ENCODER_BEAN_NAME) final PasswordEncoder clientPasswordEncoder) {
|
||||
|
||||
public LmsSetupDAOImpl(final LmsSetupRecordMapper lmsSetupRecordMapper) {
|
||||
this.lmsSetupRecordMapper = lmsSetupRecordMapper;
|
||||
this.internalEncryptionService = internalEncryptionService;
|
||||
this.clientPasswordEncoder = clientPasswordEncoder;
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -132,10 +145,14 @@ public class LmsSetupDAOImpl implements LmsSetupDAO {
|
|||
(lmsSetup.lmsType != null) ? lmsSetup.lmsType.name() : null,
|
||||
lmsSetup.lmsApiUrl,
|
||||
lmsSetup.lmsAuthName,
|
||||
lmsSetup.lmsAuthSecret,
|
||||
(StringUtils.isNotBlank(lmsSetup.lmsAuthSecret))
|
||||
? this.internalEncryptionService.encrypt(lmsSetup.lmsAuthSecret)
|
||||
: null,
|
||||
lmsSetup.lmsRestApiToken,
|
||||
lmsSetup.sebAuthName,
|
||||
lmsSetup.sebAuthSecret,
|
||||
(StringUtils.isNotBlank(lmsSetup.sebAuthSecret))
|
||||
? this.clientPasswordEncoder.encode(lmsSetup.sebAuthSecret)
|
||||
: null,
|
||||
null);
|
||||
|
||||
this.lmsSetupRecordMapper.updateByPrimaryKeySelective(newRecord);
|
||||
|
@ -277,4 +294,29 @@ public class LmsSetupDAOImpl implements LmsSetupDAO {
|
|||
BooleanUtils.toBooleanObject(record.getActive())));
|
||||
}
|
||||
|
||||
// private LmsSetup handlePasswortReset(LmsSetup lmsSetup) {
|
||||
// String lmsPWDEncrypted = null;
|
||||
// String sebPWDEncrypted = null;
|
||||
// if (StringUtils.isNotBlank(lmsSetup.lmsAuthName) && StringUtils.isNotBlank(lmsSetup.lmsAuthSecret)) {
|
||||
//
|
||||
// }
|
||||
//
|
||||
// if (StringUtils.isNotBlank(lmsSetup.sebAuthName) && StringUtils.isNotBlank(lmsSetup.sebAuthSecret)) {
|
||||
//
|
||||
// }
|
||||
//
|
||||
// return new LmsSetup(
|
||||
// lmsSetup.id,
|
||||
// lmsSetup.institutionId,
|
||||
// lmsSetup.name,
|
||||
// lmsSetup.lmsType,
|
||||
// lmsSetup.lmsAuthName,
|
||||
// lmsPWDEncrypted,
|
||||
// lmsSetup.lmsApiUrl,
|
||||
// lmsSetup.lmsRestApiToken,
|
||||
// lmsSetup.sebAuthName,
|
||||
// sebPWDEncrypted,
|
||||
// lmsSetup.active);
|
||||
// }
|
||||
|
||||
}
|
||||
|
|
|
@ -24,7 +24,7 @@ public interface LmsAPITemplate {
|
|||
|
||||
LmsSetupTestResult testLmsSetup();
|
||||
|
||||
Page<QuizData> getQuizzes(
|
||||
Result<Page<QuizData>> getQuizzes(
|
||||
String name,
|
||||
Long from,
|
||||
String sort,
|
||||
|
|
|
@ -0,0 +1,19 @@
|
|||
/*
|
||||
* 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.servicelayer.lms;
|
||||
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
|
||||
import ch.ethz.seb.sebserver.gbl.profile.WebServiceProfile;
|
||||
|
||||
@Configuration
|
||||
@WebServiceProfile
|
||||
public class LmsBindingConfig {
|
||||
|
||||
}
|
|
@ -10,11 +10,16 @@ package ch.ethz.seb.sebserver.webservice.servicelayer.lms.impl;
|
|||
|
||||
import java.io.InputStream;
|
||||
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.http.client.ClientHttpRequestFactory;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import ch.ethz.seb.sebserver.gbl.Constants;
|
||||
import ch.ethz.seb.sebserver.gbl.model.institution.LmsSetup;
|
||||
import ch.ethz.seb.sebserver.gbl.profile.WebServiceProfile;
|
||||
import ch.ethz.seb.sebserver.gbl.util.Result;
|
||||
import ch.ethz.seb.sebserver.webservice.servicelayer.InternalEncryptionService;
|
||||
import ch.ethz.seb.sebserver.webservice.servicelayer.dao.LmsSetupDAO;
|
||||
import ch.ethz.seb.sebserver.webservice.servicelayer.lms.LmsAPIService;
|
||||
import ch.ethz.seb.sebserver.webservice.servicelayer.lms.LmsAPITemplate;
|
||||
|
@ -24,9 +29,23 @@ import ch.ethz.seb.sebserver.webservice.servicelayer.lms.LmsAPITemplate;
|
|||
public class LmsAPIServiceImpl implements LmsAPIService {
|
||||
|
||||
private final LmsSetupDAO lmsSetupDAO;
|
||||
private final InternalEncryptionService internalEncryptionService;
|
||||
private final ClientHttpRequestFactory clientHttpRequestFactory;
|
||||
private final String[] openEdxAlternativeTokenRequestPaths;
|
||||
|
||||
public LmsAPIServiceImpl(
|
||||
final LmsSetupDAO lmsSetupDAO,
|
||||
final InternalEncryptionService internalEncryptionService,
|
||||
final ClientHttpRequestFactory clientHttpRequestFactory,
|
||||
@Value("${sebserver.lms.openedix.api.token.request.paths}") final String alternativeTokenRequestPaths) {
|
||||
|
||||
public LmsAPIServiceImpl(final LmsSetupDAO lmsSetupDAO) {
|
||||
this.lmsSetupDAO = lmsSetupDAO;
|
||||
this.internalEncryptionService = internalEncryptionService;
|
||||
this.clientHttpRequestFactory = clientHttpRequestFactory;
|
||||
|
||||
this.openEdxAlternativeTokenRequestPaths = (alternativeTokenRequestPaths != null)
|
||||
? StringUtils.split(alternativeTokenRequestPaths, Constants.LIST_SEPARATOR)
|
||||
: null;
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -41,10 +60,17 @@ public class LmsAPIServiceImpl implements LmsAPIService {
|
|||
switch (lmsSetup.lmsType) {
|
||||
case MOCKUP:
|
||||
return Result.of(new MockupLmsAPITemplate(lmsSetup));
|
||||
case OPEN_EDX:
|
||||
return Result.of(new OpenEdxLmsAPITemplate(
|
||||
lmsSetup,
|
||||
this.internalEncryptionService,
|
||||
this.clientHttpRequestFactory,
|
||||
this.openEdxAlternativeTokenRequestPaths));
|
||||
default:
|
||||
return Result.ofError(
|
||||
new UnsupportedOperationException("No support for LMS Type: " + lmsSetup.lmsType));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
|
@ -16,6 +16,7 @@ import java.util.List;
|
|||
import java.util.Set;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import ch.ethz.seb.sebserver.gbl.model.Domain.LMS_SETUP;
|
||||
import ch.ethz.seb.sebserver.gbl.model.Page;
|
||||
import ch.ethz.seb.sebserver.gbl.model.exam.QuizData;
|
||||
import ch.ethz.seb.sebserver.gbl.model.institution.LmsSetup;
|
||||
|
@ -26,13 +27,13 @@ import ch.ethz.seb.sebserver.gbl.util.Result;
|
|||
import ch.ethz.seb.sebserver.webservice.servicelayer.PaginationService.SortOrder;
|
||||
import ch.ethz.seb.sebserver.webservice.servicelayer.lms.LmsAPITemplate;
|
||||
|
||||
public class MockupLmsAPITemplate implements LmsAPITemplate {
|
||||
final class MockupLmsAPITemplate implements LmsAPITemplate {
|
||||
|
||||
private final LmsSetup setup;
|
||||
|
||||
private final Collection<QuizData> mockups;
|
||||
|
||||
public MockupLmsAPITemplate(final LmsSetup setup) {
|
||||
MockupLmsAPITemplate(final LmsSetup setup) {
|
||||
if (!setup.isActive() || setup.lmsType != LmsType.MOCKUP) {
|
||||
throw new IllegalArgumentException();
|
||||
}
|
||||
|
@ -69,13 +70,19 @@ public class MockupLmsAPITemplate implements LmsAPITemplate {
|
|||
|
||||
@Override
|
||||
public LmsSetupTestResult testLmsSetup() {
|
||||
if (this.setup.lmsType != LmsType.MOCKUP) {
|
||||
return LmsSetupTestResult.ofMissingAttributes(LMS_SETUP.ATTR_LMS_TYPE);
|
||||
}
|
||||
if (this.setup.lmsApiUrl.equals("mockup") &&
|
||||
this.setup.lmsAuthName.equals("mockup") &&
|
||||
this.setup.lmsAuthSecret.equals("mockup")) {
|
||||
|
||||
return new LmsSetupTestResult(true);
|
||||
return LmsSetupTestResult.ofOkay();
|
||||
} else {
|
||||
return new LmsSetupTestResult(false);
|
||||
return LmsSetupTestResult.ofMissingAttributes(
|
||||
LMS_SETUP.ATTR_LMS_URL,
|
||||
LMS_SETUP.ATTR_LMS_CLIENTNAME,
|
||||
LMS_SETUP.ATTR_LMS_CLIENTSECRET);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -106,29 +113,31 @@ public class MockupLmsAPITemplate implements LmsAPITemplate {
|
|||
}
|
||||
|
||||
@Override
|
||||
public Page<QuizData> getQuizzes(
|
||||
public Result<Page<QuizData>> getQuizzes(
|
||||
final String name,
|
||||
final Long from,
|
||||
final String sort,
|
||||
final int pageNumber,
|
||||
final int pageSize) {
|
||||
|
||||
final int startIndex = pageNumber * pageSize;
|
||||
final int endIndex = startIndex + pageSize;
|
||||
int index = 0;
|
||||
final Collection<QuizData> quizzes = getQuizzes(name, from, sort);
|
||||
final int numberOfPages = quizzes.size() / pageSize;
|
||||
final Iterator<QuizData> iterator = quizzes.iterator();
|
||||
final List<QuizData> pageContent = new ArrayList<>();
|
||||
while (iterator.hasNext() && index < endIndex) {
|
||||
final QuizData next = iterator.next();
|
||||
if (index >= startIndex) {
|
||||
pageContent.add(next);
|
||||
return Result.tryCatch(() -> {
|
||||
final int startIndex = pageNumber * pageSize;
|
||||
final int endIndex = startIndex + pageSize;
|
||||
int index = 0;
|
||||
final Collection<QuizData> quizzes = getQuizzes(name, from, sort);
|
||||
final int numberOfPages = quizzes.size() / pageSize;
|
||||
final Iterator<QuizData> iterator = quizzes.iterator();
|
||||
final List<QuizData> pageContent = new ArrayList<>();
|
||||
while (iterator.hasNext() && index < endIndex) {
|
||||
final QuizData next = iterator.next();
|
||||
if (index >= startIndex) {
|
||||
pageContent.add(next);
|
||||
}
|
||||
index++;
|
||||
}
|
||||
index++;
|
||||
}
|
||||
|
||||
return new Page<>(numberOfPages, pageNumber, sort, pageContent);
|
||||
return new Page<>(numberOfPages, pageNumber, sort, pageContent);
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
|
@ -0,0 +1,326 @@
|
|||
/*
|
||||
* 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.servicelayer.lms.impl;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collection;
|
||||
import java.util.HashSet;
|
||||
import java.util.Iterator;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.http.HttpEntity;
|
||||
import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.http.HttpMethod;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.http.client.ClientHttpRequestFactory;
|
||||
import org.springframework.security.crypto.encrypt.Encryptors;
|
||||
import org.springframework.security.crypto.encrypt.TextEncryptor;
|
||||
import org.springframework.security.oauth2.client.OAuth2RestTemplate;
|
||||
import org.springframework.security.oauth2.client.token.grant.client.ClientCredentialsResourceDetails;
|
||||
import org.springframework.security.oauth2.common.DefaultOAuth2AccessToken;
|
||||
import org.springframework.security.oauth2.common.OAuth2AccessToken;
|
||||
|
||||
import ch.ethz.seb.sebserver.gbl.model.Domain.LMS_SETUP;
|
||||
import ch.ethz.seb.sebserver.gbl.model.Page;
|
||||
import ch.ethz.seb.sebserver.gbl.model.exam.QuizData;
|
||||
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.model.institution.LmsSetupTestResult;
|
||||
import ch.ethz.seb.sebserver.gbl.model.user.ExamineeAccountDetails;
|
||||
import ch.ethz.seb.sebserver.gbl.util.Result;
|
||||
import ch.ethz.seb.sebserver.webservice.servicelayer.InternalEncryptionService;
|
||||
import ch.ethz.seb.sebserver.webservice.servicelayer.lms.LmsAPITemplate;
|
||||
|
||||
final class OpenEdxLmsAPITemplate implements LmsAPITemplate {
|
||||
|
||||
private static final Logger log = LoggerFactory.getLogger(OpenEdxLmsAPITemplate.class);
|
||||
|
||||
private static final String OPEN_EDX_DEFAULT_TOKEN_REQUEST_PATH = "/oauth2/access_token";
|
||||
private static final String OPEN_EDX_DEFAULT_COURSE_ENDPOINT = "/api/courses/v1/courses/";
|
||||
private static final String OPEN_EDX_DEFAULT_COURSE_START_URL_PREFIX = "/courses/";
|
||||
|
||||
private final LmsSetup lmsSetup;
|
||||
private final ClientHttpRequestFactory clientHttpRequestFactory;
|
||||
private final InternalEncryptionService internalEncryptionService;
|
||||
private final Set<String> knownTokenAccessPaths;
|
||||
|
||||
private OAuth2RestTemplate restTemplate = null;
|
||||
|
||||
OpenEdxLmsAPITemplate(
|
||||
final LmsSetup lmsSetup,
|
||||
final InternalEncryptionService internalEncryptionService,
|
||||
final ClientHttpRequestFactory clientHttpRequestFactory,
|
||||
final String[] alternativeTokenRequestPaths) {
|
||||
|
||||
this.lmsSetup = lmsSetup;
|
||||
this.clientHttpRequestFactory = clientHttpRequestFactory;
|
||||
this.internalEncryptionService = internalEncryptionService;
|
||||
|
||||
this.knownTokenAccessPaths = new HashSet<>();
|
||||
this.knownTokenAccessPaths.add(OPEN_EDX_DEFAULT_TOKEN_REQUEST_PATH);
|
||||
if (alternativeTokenRequestPaths != null) {
|
||||
this.knownTokenAccessPaths.addAll(Arrays.asList(alternativeTokenRequestPaths));
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public LmsSetup lmsSetup() {
|
||||
return this.lmsSetup;
|
||||
}
|
||||
|
||||
@Override
|
||||
public LmsSetupTestResult testLmsSetup() {
|
||||
|
||||
log.info("Test Lms Binding for OpenEdX and LmsSetup: {}", this.lmsSetup);
|
||||
|
||||
// validation of LmsSetup
|
||||
if (this.lmsSetup.lmsType != LmsType.MOCKUP) {
|
||||
return LmsSetupTestResult.ofMissingAttributes(LMS_SETUP.ATTR_LMS_TYPE);
|
||||
}
|
||||
final List<String> missingAttrs = new ArrayList<>();
|
||||
if (StringUtils.isBlank(this.lmsSetup.lmsApiUrl)) {
|
||||
missingAttrs.add(LMS_SETUP.ATTR_LMS_TYPE);
|
||||
}
|
||||
if (StringUtils.isBlank(this.lmsSetup.getLmsAuthName())) {
|
||||
missingAttrs.add(LMS_SETUP.ATTR_LMS_CLIENTNAME);
|
||||
}
|
||||
if (StringUtils.isBlank(this.lmsSetup.getLmsAuthSecret())) {
|
||||
missingAttrs.add(LMS_SETUP.ATTR_LMS_CLIENTSECRET);
|
||||
}
|
||||
|
||||
if (!missingAttrs.isEmpty()) {
|
||||
return LmsSetupTestResult.ofMissingAttributes(missingAttrs);
|
||||
}
|
||||
|
||||
// request OAuth2 access token on OpenEdx API
|
||||
initRestTemplateAndRequestAccessToken();
|
||||
if (this.restTemplate == null) {
|
||||
return LmsSetupTestResult.ofTokenRequestError(
|
||||
"Failed to gain access token form OpenEdX Rest API: tried token endpoints: " +
|
||||
this.knownTokenAccessPaths);
|
||||
}
|
||||
|
||||
// query quizzes TODO!?
|
||||
|
||||
return LmsSetupTestResult.ofOkay();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Result<Page<QuizData>> getQuizzes(
|
||||
final String name,
|
||||
final Long from,
|
||||
final String sort,
|
||||
final int pageNumber,
|
||||
final int pageSize) {
|
||||
|
||||
return Result.tryCatch(() -> {
|
||||
initRestTemplateAndRequestAccessToken();
|
||||
|
||||
// TODO sort and pagination
|
||||
final HttpHeaders httpHeaders = new HttpHeaders();
|
||||
|
||||
final ResponseEntity<EdXPage> response = this.restTemplate.exchange(
|
||||
this.lmsSetup.lmsApiUrl + OPEN_EDX_DEFAULT_COURSE_ENDPOINT,
|
||||
HttpMethod.GET,
|
||||
new HttpEntity<>(httpHeaders),
|
||||
EdXPage.class);
|
||||
final EdXPage edxpage = response.getBody();
|
||||
|
||||
final List<QuizData> content = edxpage.results
|
||||
.stream()
|
||||
.reduce(
|
||||
new ArrayList<QuizData>(),
|
||||
(list, courseData) -> {
|
||||
list.add(quizDataOf(courseData));
|
||||
return list;
|
||||
},
|
||||
(list1, list2) -> {
|
||||
list1.addAll(list2);
|
||||
return list1;
|
||||
});
|
||||
|
||||
return new Page<>(edxpage.num_pages, pageNumber, sort, content);
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public Collection<Result<QuizData>> getQuizzes(final Set<String> ids) {
|
||||
// TODO Auto-generated method stub
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Result<ExamineeAccountDetails> getExamineeAccountDetails(final String examineeUserId) {
|
||||
// TODO Auto-generated method stub
|
||||
return null;
|
||||
}
|
||||
|
||||
private void initRestTemplateAndRequestAccessToken() {
|
||||
|
||||
log.info("Initialize Rest Template for OpenEdX API access. LmsSetup: {}", this.lmsSetup);
|
||||
|
||||
if (this.restTemplate != null) {
|
||||
try {
|
||||
this.restTemplate.getAccessToken();
|
||||
return;
|
||||
} catch (final Exception e) {
|
||||
log.warn(
|
||||
"Error while trying to get access token within already existing OAuth2RestTemplate instance. Try to create new one.",
|
||||
e);
|
||||
this.restTemplate = null;
|
||||
}
|
||||
}
|
||||
|
||||
final Iterator<String> tokenAccessPaths = this.knownTokenAccessPaths.iterator();
|
||||
while (this.restTemplate == null && tokenAccessPaths.hasNext()) {
|
||||
final String accessTokenRequestPath = tokenAccessPaths.next();
|
||||
try {
|
||||
final OAuth2RestTemplate template = createRestTemplate(accessTokenRequestPath);
|
||||
final OAuth2AccessToken accessToken = template.getAccessToken();
|
||||
if (accessToken != null) {
|
||||
this.restTemplate = template;
|
||||
storeAccessToken(accessToken);
|
||||
}
|
||||
} catch (final Exception e) {
|
||||
log.info("Failed to request access token on access token request path: {}", accessTokenRequestPath, e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private OAuth2RestTemplate createRestTemplate(final String accessTokenRequestPath) {
|
||||
|
||||
final String lmsAuthSecret = this.internalEncryptionService.decrypt(this.lmsSetup.lmsAuthSecret);
|
||||
|
||||
final ClientCredentialsResourceDetails details = new ClientCredentialsResourceDetails();
|
||||
details.setAccessTokenUri(this.lmsSetup.lmsApiUrl + accessTokenRequestPath);
|
||||
details.setClientId(this.lmsSetup.lmsAuthName);
|
||||
details.setClientSecret(lmsAuthSecret);
|
||||
details.setGrantType("client_credentials");
|
||||
|
||||
// TODO: accordingly to the documentation (https://course-catalog-api-guide.readthedocs.io/en/latest/authentication/#create-an-account-on-edx-org-for-api-access)
|
||||
// token_type=jwt is needed for token request but is it possible to set this within ClientCredentialsResourceDetails
|
||||
// or within the request header on API call. To clarify
|
||||
|
||||
final OAuth2RestTemplate template = new OAuth2RestTemplate(details);
|
||||
template.setRequestFactory(this.clientHttpRequestFactory);
|
||||
|
||||
final OAuth2AccessToken previousAccessToken = loadPreviousAccessToken();
|
||||
if (previousAccessToken != null) {
|
||||
template.getOAuth2ClientContext().setAccessToken(previousAccessToken);
|
||||
}
|
||||
return template;
|
||||
}
|
||||
|
||||
private void storeAccessToken(final OAuth2AccessToken accessToken) {
|
||||
try {
|
||||
|
||||
final String accessTokenString = accessToken.getValue();
|
||||
final TextEncryptor textEncryptor = Encryptors.text(this.lmsSetup.lmsAuthSecret, this.lmsSetup.lmsAuthName);
|
||||
final String accessTokenEncrypted = textEncryptor.encrypt(accessTokenString);
|
||||
|
||||
// TODO store the accessTokenEncrypted to additional attributes of LmsSetup
|
||||
|
||||
} catch (final Exception e) {
|
||||
log.warn("Failed to store access token for later use.", e);
|
||||
}
|
||||
}
|
||||
|
||||
private OAuth2AccessToken loadPreviousAccessToken() {
|
||||
|
||||
// TODO get the previous token from additional attributes of LmsSetup
|
||||
final String prevTokenEncrypted = null;
|
||||
|
||||
if (StringUtils.isBlank(prevTokenEncrypted)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
|
||||
final TextEncryptor textEncryptor = Encryptors.text(this.lmsSetup.lmsAuthSecret, this.lmsSetup.lmsAuthName);
|
||||
final String prevTokenDecrypt = textEncryptor.decrypt(prevTokenEncrypted);
|
||||
return new DefaultOAuth2AccessToken(prevTokenDecrypt);
|
||||
|
||||
} catch (final Exception e) {
|
||||
log.warn("Failed to decrypt previous access-token.", e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private QuizData quizDataOf(final CourseData courseData) {
|
||||
final String startURI = this.lmsSetup.lmsApiUrl + OPEN_EDX_DEFAULT_COURSE_START_URL_PREFIX + courseData.id;
|
||||
return new QuizData(
|
||||
courseData.id,
|
||||
courseData.name,
|
||||
courseData.short_description,
|
||||
courseData.start,
|
||||
courseData.end,
|
||||
startURI);
|
||||
}
|
||||
|
||||
static final class EdXPage {
|
||||
public Integer count;
|
||||
public Integer previous;
|
||||
public Integer num_pages;
|
||||
public Integer next;
|
||||
public List<CourseData> results;
|
||||
}
|
||||
|
||||
static final class CourseData {
|
||||
public String id;
|
||||
public String course_id;
|
||||
public String name;
|
||||
public String short_description;
|
||||
public String blocks_url;
|
||||
public String start;
|
||||
public String end;
|
||||
}
|
||||
|
||||
/*
|
||||
* pagination
|
||||
* count 2
|
||||
* previous null
|
||||
* num_pages 1
|
||||
* next null
|
||||
* results
|
||||
* 0
|
||||
* blocks_url "http://ralph.ethz.ch:18000/api/courses/v1/blocks/?course_id=course-v1%3AedX%2BDemoX%2BDemo_Course"
|
||||
* effort null
|
||||
* end null
|
||||
* enrollment_start null
|
||||
* enrollment_end null
|
||||
* id "course-v1:edX+DemoX+Demo_Course"
|
||||
* media
|
||||
* course_image
|
||||
* uri "/asset-v1:edX+DemoX+Demo_Course+type@asset+block@images_course_image.jpg"
|
||||
* course_video
|
||||
* uri null
|
||||
* image
|
||||
* raw "http://ralph.ethz.ch:18000/asset-v1:edX+DemoX+Demo_Course+type@asset+block@images_course_image.jpg"
|
||||
* small "http://ralph.ethz.ch:18000/asset-v1:edX+DemoX+Demo_Course+type@asset+block@images_course_image.jpg"
|
||||
* large "http://ralph.ethz.ch:18000/asset-v1:edX+DemoX+Demo_Course+type@asset+block@images_course_image.jpg"
|
||||
* name "edX Demonstration Course"
|
||||
* number "DemoX"
|
||||
* org "edX"
|
||||
* short_description null
|
||||
* start "2013-02-05T05:00:00Z"
|
||||
* start_display "Feb. 5, 2013"
|
||||
* start_type "timestamp"
|
||||
* pacing "instructor"
|
||||
* mobile_available false
|
||||
* hidden false
|
||||
* invitation_only false
|
||||
* course_id "course-v1:edX+DemoX+Demo_Course"
|
||||
*/
|
||||
|
||||
}
|
|
@ -25,6 +25,7 @@ import ch.ethz.seb.sebserver.gbl.api.POSTMapper;
|
|||
import ch.ethz.seb.sebserver.gbl.api.SEBServerRestEndpoints;
|
||||
import ch.ethz.seb.sebserver.gbl.model.EntityType;
|
||||
import ch.ethz.seb.sebserver.gbl.model.institution.LmsSetup;
|
||||
import ch.ethz.seb.sebserver.gbl.model.institution.LmsSetupTestResult;
|
||||
import ch.ethz.seb.sebserver.gbl.profile.WebServiceProfile;
|
||||
import ch.ethz.seb.sebserver.webservice.datalayer.batis.mapper.LmsSetupRecordDynamicSqlSupport;
|
||||
import ch.ethz.seb.sebserver.webservice.servicelayer.PaginationService;
|
||||
|
@ -100,6 +101,21 @@ public class LmsSetupController extends ActivatableEntityController<LmsSetup, Lm
|
|||
}
|
||||
}
|
||||
|
||||
@RequestMapping(
|
||||
path = "/connection_report/{id}",
|
||||
method = RequestMethod.GET,
|
||||
produces = MediaType.APPLICATION_JSON_UTF8_VALUE)
|
||||
public LmsSetupTestResult connectionReport(@PathVariable final Long id) {
|
||||
|
||||
this.authorizationGrantService.checkHasAnyPrivilege(
|
||||
EntityType.LMS_SETUP,
|
||||
PrivilegeType.MODIFY);
|
||||
|
||||
return this.lmsAPIService.createLmsAPITemplate(id)
|
||||
.map(template -> template.testLmsSetup())
|
||||
.getOrThrow();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected LmsSetup createNew(final POSTMapper postParams) {
|
||||
return new LmsSetup(null, postParams);
|
||||
|
|
|
@ -14,8 +14,8 @@ import org.springframework.web.bind.annotation.RequestMethod;
|
|||
import org.springframework.web.bind.annotation.RequestParam;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
import ch.ethz.seb.sebserver.gbl.model.Domain.LMS_SETUP;
|
||||
import ch.ethz.seb.sebserver.gbl.api.SEBServerRestEndpoints;
|
||||
import ch.ethz.seb.sebserver.gbl.model.Domain.LMS_SETUP;
|
||||
import ch.ethz.seb.sebserver.gbl.model.EntityType;
|
||||
import ch.ethz.seb.sebserver.gbl.model.Page;
|
||||
import ch.ethz.seb.sebserver.gbl.model.exam.QuizData;
|
||||
|
@ -78,7 +78,8 @@ public class QuizImportController {
|
|||
? (pageSize <= this.maxPageSize)
|
||||
? pageSize
|
||||
: this.maxPageSize
|
||||
: this.defaultPageSize);
|
||||
: this.defaultPageSize)
|
||||
.getOrThrow();
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -1,4 +1,7 @@
|
|||
spring.application.name=SEB Server
|
||||
spring.profiles.active=dev
|
||||
|
||||
sebserver.version=1.0 beta
|
||||
sebserver.version=1.0 beta
|
||||
|
||||
# comma separated list of known possible OpenEdX API access token request endpoints
|
||||
sebserver.lms.openedix.api.token.request.paths=/oauth2/access_token
|
Loading…
Reference in a new issue