OpenEdX API Setup plus tests
This commit is contained in:
parent
44dbe1e714
commit
cda50a15e7
9 changed files with 260 additions and 131 deletions
|
@ -24,7 +24,8 @@ public class InternalEncryptionService {
|
||||||
|
|
||||||
private static final Logger log = LoggerFactory.getLogger(InternalEncryptionService.class);
|
private static final Logger log = LoggerFactory.getLogger(InternalEncryptionService.class);
|
||||||
|
|
||||||
private static final String NO_SALT = "NO_SALT";
|
static final String SEBSERVER_WEBSERVICE_INTERNAL_SECRET_KEY = "sebserver.webservice.internalSecret";
|
||||||
|
private static final String DEFAULT_SALT = "b7dbe99bbfa3e21e";
|
||||||
|
|
||||||
private final Environment environment;
|
private final Environment environment;
|
||||||
|
|
||||||
|
@ -35,8 +36,8 @@ public class InternalEncryptionService {
|
||||||
public String encrypt(final String text) {
|
public String encrypt(final String text) {
|
||||||
try {
|
try {
|
||||||
return Encryptors.text(
|
return Encryptors.text(
|
||||||
this.environment.getRequiredProperty("sebserver.webservice.internalSecret"),
|
this.environment.getRequiredProperty(SEBSERVER_WEBSERVICE_INTERNAL_SECRET_KEY),
|
||||||
NO_SALT).encrypt(text);
|
DEFAULT_SALT).encrypt(text);
|
||||||
} catch (final Exception e) {
|
} catch (final Exception e) {
|
||||||
log.error("Failed to encrypt text: ", e);
|
log.error("Failed to encrypt text: ", e);
|
||||||
return text;
|
return text;
|
||||||
|
@ -46,8 +47,8 @@ public class InternalEncryptionService {
|
||||||
public String decrypt(final String text) {
|
public String decrypt(final String text) {
|
||||||
try {
|
try {
|
||||||
return Encryptors.text(
|
return Encryptors.text(
|
||||||
this.environment.getRequiredProperty("sebserver.webservice.internalSecret"),
|
this.environment.getRequiredProperty(SEBSERVER_WEBSERVICE_INTERNAL_SECRET_KEY),
|
||||||
NO_SALT).decrypt(text);
|
DEFAULT_SALT).decrypt(text);
|
||||||
} catch (final Exception e) {
|
} catch (final Exception e) {
|
||||||
log.error("Failed to decrypt text: ", e);
|
log.error("Failed to decrypt text: ", e);
|
||||||
return text;
|
return text;
|
||||||
|
@ -57,7 +58,7 @@ public class InternalEncryptionService {
|
||||||
public String encrypt(final String text, final CharSequence salt) {
|
public String encrypt(final String text, final CharSequence salt) {
|
||||||
try {
|
try {
|
||||||
return Encryptors.text(
|
return Encryptors.text(
|
||||||
this.environment.getRequiredProperty("sebserver.webservice.internalSecret"),
|
this.environment.getRequiredProperty(SEBSERVER_WEBSERVICE_INTERNAL_SECRET_KEY),
|
||||||
salt).encrypt(text);
|
salt).encrypt(text);
|
||||||
} catch (final Exception e) {
|
} catch (final Exception e) {
|
||||||
log.error("Failed to encrypt text: ", e);
|
log.error("Failed to encrypt text: ", e);
|
||||||
|
@ -68,7 +69,7 @@ public class InternalEncryptionService {
|
||||||
public String decrypt(final String text, final CharSequence salt) {
|
public String decrypt(final String text, final CharSequence salt) {
|
||||||
try {
|
try {
|
||||||
return Encryptors.text(
|
return Encryptors.text(
|
||||||
this.environment.getRequiredProperty("sebserver.webservice.internalSecret"),
|
this.environment.getRequiredProperty(SEBSERVER_WEBSERVICE_INTERNAL_SECRET_KEY),
|
||||||
salt).decrypt(text);
|
salt).decrypt(text);
|
||||||
} catch (final Exception e) {
|
} catch (final Exception e) {
|
||||||
log.error("Failed to decrypt text: ", e);
|
log.error("Failed to decrypt text: ", e);
|
||||||
|
|
|
@ -9,8 +9,25 @@
|
||||||
package ch.ethz.seb.sebserver.webservice.servicelayer.dao;
|
package ch.ethz.seb.sebserver.webservice.servicelayer.dao;
|
||||||
|
|
||||||
import ch.ethz.seb.sebserver.gbl.model.institution.LmsSetup;
|
import ch.ethz.seb.sebserver.gbl.model.institution.LmsSetup;
|
||||||
|
import ch.ethz.seb.sebserver.gbl.util.Result;
|
||||||
import ch.ethz.seb.sebserver.webservice.servicelayer.bulkaction.BulkActionSupportDAO;
|
import ch.ethz.seb.sebserver.webservice.servicelayer.bulkaction.BulkActionSupportDAO;
|
||||||
|
|
||||||
public interface LmsSetupDAO extends ActivatableEntityDAO<LmsSetup, LmsSetup>, BulkActionSupportDAO<LmsSetup> {
|
public interface LmsSetupDAO extends ActivatableEntityDAO<LmsSetup, LmsSetup>, BulkActionSupportDAO<LmsSetup> {
|
||||||
|
|
||||||
|
Result<Credentials> getLmsAPIAccessCredentials(String lmsSetupId);
|
||||||
|
|
||||||
|
Result<Credentials> getSEBClientCredentials(String lmsSetupId);
|
||||||
|
|
||||||
|
final class Credentials {
|
||||||
|
public final String clientId;
|
||||||
|
public final String secret;
|
||||||
|
public final String accessToken;
|
||||||
|
|
||||||
|
public Credentials(final String clientId, final String secret, final String accessToken) {
|
||||||
|
super();
|
||||||
|
this.clientId = clientId;
|
||||||
|
this.secret = secret;
|
||||||
|
this.accessToken = accessToken;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -254,6 +254,34 @@ public class LmsSetupDAOImpl implements LmsSetupDAO {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
@Transactional(readOnly = true)
|
||||||
|
public Result<Credentials> getLmsAPIAccessCredentials(final String lmsSetupId) {
|
||||||
|
return Result.tryCatch(() -> {
|
||||||
|
final LmsSetupRecord record = this.lmsSetupRecordMapper
|
||||||
|
.selectByPrimaryKey(Long.parseLong(lmsSetupId));
|
||||||
|
|
||||||
|
return new Credentials(
|
||||||
|
record.getLmsClientname(),
|
||||||
|
record.getLmsClientsecret(),
|
||||||
|
record.getLmsRestApiToken());
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
@Transactional(readOnly = true)
|
||||||
|
public Result<Credentials> getSEBClientCredentials(final String lmsSetupId) {
|
||||||
|
return Result.tryCatch(() -> {
|
||||||
|
final LmsSetupRecord record = this.lmsSetupRecordMapper
|
||||||
|
.selectByPrimaryKey(Long.parseLong(lmsSetupId));
|
||||||
|
|
||||||
|
return new Credentials(
|
||||||
|
record.getSebClientname(),
|
||||||
|
record.getSebClientsecret(),
|
||||||
|
null);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
private Result<Collection<EntityKey>> allIdsOfInstitution(final EntityKey institutionKey) {
|
private Result<Collection<EntityKey>> allIdsOfInstitution(final EntityKey institutionKey) {
|
||||||
return Result.tryCatch(() -> {
|
return Result.tryCatch(() -> {
|
||||||
return this.lmsSetupRecordMapper.selectIdsByExample()
|
return this.lmsSetupRecordMapper.selectIdsByExample()
|
||||||
|
|
|
@ -20,7 +20,7 @@ import ch.ethz.seb.sebserver.gbl.util.Result;
|
||||||
|
|
||||||
public interface LmsAPITemplate {
|
public interface LmsAPITemplate {
|
||||||
|
|
||||||
LmsSetup lmsSetup();
|
Result<LmsSetup> lmsSetup();
|
||||||
|
|
||||||
LmsSetupTestResult testLmsSetup();
|
LmsSetupTestResult testLmsSetup();
|
||||||
|
|
||||||
|
@ -35,4 +35,6 @@ public interface LmsAPITemplate {
|
||||||
|
|
||||||
Result<ExamineeAccountDetails> getExamineeAccountDetails(String examineeUserId);
|
Result<ExamineeAccountDetails> getExamineeAccountDetails(String examineeUserId);
|
||||||
|
|
||||||
|
void reset();
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -33,6 +33,8 @@ public class LmsAPIServiceImpl implements LmsAPIService {
|
||||||
private final ClientHttpRequestFactory clientHttpRequestFactory;
|
private final ClientHttpRequestFactory clientHttpRequestFactory;
|
||||||
private final String[] openEdxAlternativeTokenRequestPaths;
|
private final String[] openEdxAlternativeTokenRequestPaths;
|
||||||
|
|
||||||
|
// TODO internal caching of LmsAPITemplate per LmsSetup (Id)
|
||||||
|
|
||||||
public LmsAPIServiceImpl(
|
public LmsAPIServiceImpl(
|
||||||
final LmsSetupDAO lmsSetupDAO,
|
final LmsSetupDAO lmsSetupDAO,
|
||||||
final InternalEncryptionService internalEncryptionService,
|
final InternalEncryptionService internalEncryptionService,
|
||||||
|
@ -59,10 +61,14 @@ public class LmsAPIServiceImpl implements LmsAPIService {
|
||||||
public Result<LmsAPITemplate> createLmsAPITemplate(final LmsSetup lmsSetup) {
|
public Result<LmsAPITemplate> createLmsAPITemplate(final LmsSetup lmsSetup) {
|
||||||
switch (lmsSetup.lmsType) {
|
switch (lmsSetup.lmsType) {
|
||||||
case MOCKUP:
|
case MOCKUP:
|
||||||
return Result.of(new MockupLmsAPITemplate(lmsSetup));
|
return Result.of(new MockupLmsAPITemplate(
|
||||||
|
this.lmsSetupDAO,
|
||||||
|
lmsSetup,
|
||||||
|
this.internalEncryptionService));
|
||||||
case OPEN_EDX:
|
case OPEN_EDX:
|
||||||
return Result.of(new OpenEdxLmsAPITemplate(
|
return Result.of(new OpenEdxLmsAPITemplate(
|
||||||
lmsSetup,
|
lmsSetup.getModelId(),
|
||||||
|
this.lmsSetupDAO,
|
||||||
this.internalEncryptionService,
|
this.internalEncryptionService,
|
||||||
this.clientHttpRequestFactory,
|
this.clientHttpRequestFactory,
|
||||||
this.openEdxAlternativeTokenRequestPaths));
|
this.openEdxAlternativeTokenRequestPaths));
|
||||||
|
|
|
@ -24,16 +24,30 @@ 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.institution.LmsSetupTestResult;
|
||||||
import ch.ethz.seb.sebserver.gbl.model.user.ExamineeAccountDetails;
|
import ch.ethz.seb.sebserver.gbl.model.user.ExamineeAccountDetails;
|
||||||
import ch.ethz.seb.sebserver.gbl.util.Result;
|
import ch.ethz.seb.sebserver.gbl.util.Result;
|
||||||
|
import ch.ethz.seb.sebserver.webservice.servicelayer.InternalEncryptionService;
|
||||||
import ch.ethz.seb.sebserver.webservice.servicelayer.PaginationService.SortOrder;
|
import ch.ethz.seb.sebserver.webservice.servicelayer.PaginationService.SortOrder;
|
||||||
|
import ch.ethz.seb.sebserver.webservice.servicelayer.dao.LmsSetupDAO;
|
||||||
import ch.ethz.seb.sebserver.webservice.servicelayer.lms.LmsAPITemplate;
|
import ch.ethz.seb.sebserver.webservice.servicelayer.lms.LmsAPITemplate;
|
||||||
|
|
||||||
final class MockupLmsAPITemplate implements LmsAPITemplate {
|
final class MockupLmsAPITemplate implements LmsAPITemplate {
|
||||||
|
|
||||||
|
public static final String MOCKUP_LMS_CLIENT_NAME = "mockupLmsClientName";
|
||||||
|
public static final String MOCKUP_LMS_CLIENT_SECRET = "mockupLmsClientSecret";
|
||||||
|
|
||||||
|
private final InternalEncryptionService internalEncryptionService;
|
||||||
|
private final LmsSetupDAO lmsSetupDao;
|
||||||
private final LmsSetup setup;
|
private final LmsSetup setup;
|
||||||
|
|
||||||
|
private LmsSetupDAO.Credentials credentials = null;
|
||||||
private final Collection<QuizData> mockups;
|
private final Collection<QuizData> mockups;
|
||||||
|
|
||||||
MockupLmsAPITemplate(final LmsSetup setup) {
|
MockupLmsAPITemplate(
|
||||||
|
final LmsSetupDAO lmsSetupDao,
|
||||||
|
final LmsSetup setup,
|
||||||
|
final InternalEncryptionService internalEncryptionService) {
|
||||||
|
|
||||||
|
this.lmsSetupDao = lmsSetupDao;
|
||||||
|
this.internalEncryptionService = internalEncryptionService;
|
||||||
if (!setup.isActive() || setup.lmsType != LmsType.MOCKUP) {
|
if (!setup.isActive() || setup.lmsType != LmsType.MOCKUP) {
|
||||||
throw new IllegalArgumentException();
|
throw new IllegalArgumentException();
|
||||||
}
|
}
|
||||||
|
@ -64,8 +78,8 @@ final class MockupLmsAPITemplate implements LmsAPITemplate {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public LmsSetup lmsSetup() {
|
public Result<LmsSetup> lmsSetup() {
|
||||||
return this.setup;
|
return Result.of(this.setup);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@ -73,10 +87,8 @@ final class MockupLmsAPITemplate implements LmsAPITemplate {
|
||||||
if (this.setup.lmsType != LmsType.MOCKUP) {
|
if (this.setup.lmsType != LmsType.MOCKUP) {
|
||||||
return LmsSetupTestResult.ofMissingAttributes(LMS_SETUP.ATTR_LMS_TYPE);
|
return LmsSetupTestResult.ofMissingAttributes(LMS_SETUP.ATTR_LMS_TYPE);
|
||||||
}
|
}
|
||||||
if (this.setup.lmsApiUrl.equals("mockup") &&
|
initCredentials();
|
||||||
this.setup.lmsAuthName.equals("mockup") &&
|
if (this.credentials != null) {
|
||||||
this.setup.lmsAuthSecret.equals("mockup")) {
|
|
||||||
|
|
||||||
return LmsSetupTestResult.ofOkay();
|
return LmsSetupTestResult.ofOkay();
|
||||||
} else {
|
} else {
|
||||||
return LmsSetupTestResult.ofMissingAttributes(
|
return LmsSetupTestResult.ofMissingAttributes(
|
||||||
|
@ -121,6 +133,11 @@ final class MockupLmsAPITemplate implements LmsAPITemplate {
|
||||||
final int pageSize) {
|
final int pageSize) {
|
||||||
|
|
||||||
return Result.tryCatch(() -> {
|
return Result.tryCatch(() -> {
|
||||||
|
initCredentials();
|
||||||
|
if (this.credentials == null) {
|
||||||
|
throw new IllegalArgumentException("Wrong clientId or secret");
|
||||||
|
}
|
||||||
|
|
||||||
final int startIndex = pageNumber * pageSize;
|
final int startIndex = pageNumber * pageSize;
|
||||||
final int endIndex = startIndex + pageSize;
|
final int endIndex = startIndex + pageSize;
|
||||||
int index = 0;
|
int index = 0;
|
||||||
|
@ -142,6 +159,11 @@ final class MockupLmsAPITemplate implements LmsAPITemplate {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Collection<Result<QuizData>> getQuizzes(final Set<String> ids) {
|
public Collection<Result<QuizData>> getQuizzes(final Set<String> ids) {
|
||||||
|
initCredentials();
|
||||||
|
if (this.credentials == null) {
|
||||||
|
throw new IllegalArgumentException("Wrong clientId or secret");
|
||||||
|
}
|
||||||
|
|
||||||
return this.mockups.stream()
|
return this.mockups.stream()
|
||||||
.filter(mockup -> ids.contains(mockup.id))
|
.filter(mockup -> ids.contains(mockup.id))
|
||||||
.map(mockup -> Result.of(mockup))
|
.map(mockup -> Result.of(mockup))
|
||||||
|
@ -150,7 +172,31 @@ final class MockupLmsAPITemplate implements LmsAPITemplate {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Result<ExamineeAccountDetails> getExamineeAccountDetails(final String examineeUserId) {
|
public Result<ExamineeAccountDetails> getExamineeAccountDetails(final String examineeUserId) {
|
||||||
|
initCredentials();
|
||||||
|
if (this.credentials == null) {
|
||||||
|
throw new IllegalArgumentException("Wrong clientId or secret");
|
||||||
|
}
|
||||||
|
|
||||||
return Result.of(new ExamineeAccountDetails(examineeUserId, "mockup", "mockup", "mockup"));
|
return Result.of(new ExamineeAccountDetails(examineeUserId, "mockup", "mockup", "mockup"));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void reset() {
|
||||||
|
this.credentials = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void initCredentials() {
|
||||||
|
try {
|
||||||
|
this.credentials = this.lmsSetupDao
|
||||||
|
.getLmsAPIAccessCredentials(this.setup.getModelId())
|
||||||
|
.getOrThrow();
|
||||||
|
|
||||||
|
assert (this.credentials.clientId.equals("lmsMockupClientId")) : "wrong clientId";
|
||||||
|
assert (this.internalEncryptionService.decrypt(this.credentials.clientId)
|
||||||
|
.equals("lmsMockupSecret")) : "wrong clientId";
|
||||||
|
} catch (final Exception e) {
|
||||||
|
this.credentials = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -24,11 +24,8 @@ import org.springframework.http.HttpHeaders;
|
||||||
import org.springframework.http.HttpMethod;
|
import org.springframework.http.HttpMethod;
|
||||||
import org.springframework.http.ResponseEntity;
|
import org.springframework.http.ResponseEntity;
|
||||||
import org.springframework.http.client.ClientHttpRequestFactory;
|
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.OAuth2RestTemplate;
|
||||||
import org.springframework.security.oauth2.client.token.grant.client.ClientCredentialsResourceDetails;
|
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 org.springframework.security.oauth2.common.OAuth2AccessToken;
|
||||||
|
|
||||||
import ch.ethz.seb.sebserver.gbl.model.Domain.LMS_SETUP;
|
import ch.ethz.seb.sebserver.gbl.model.Domain.LMS_SETUP;
|
||||||
|
@ -40,6 +37,8 @@ import ch.ethz.seb.sebserver.gbl.model.institution.LmsSetupTestResult;
|
||||||
import ch.ethz.seb.sebserver.gbl.model.user.ExamineeAccountDetails;
|
import ch.ethz.seb.sebserver.gbl.model.user.ExamineeAccountDetails;
|
||||||
import ch.ethz.seb.sebserver.gbl.util.Result;
|
import ch.ethz.seb.sebserver.gbl.util.Result;
|
||||||
import ch.ethz.seb.sebserver.webservice.servicelayer.InternalEncryptionService;
|
import ch.ethz.seb.sebserver.webservice.servicelayer.InternalEncryptionService;
|
||||||
|
import ch.ethz.seb.sebserver.webservice.servicelayer.dao.LmsSetupDAO;
|
||||||
|
import ch.ethz.seb.sebserver.webservice.servicelayer.dao.LmsSetupDAO.Credentials;
|
||||||
import ch.ethz.seb.sebserver.webservice.servicelayer.lms.LmsAPITemplate;
|
import ch.ethz.seb.sebserver.webservice.servicelayer.lms.LmsAPITemplate;
|
||||||
|
|
||||||
final class OpenEdxLmsAPITemplate implements LmsAPITemplate {
|
final class OpenEdxLmsAPITemplate implements LmsAPITemplate {
|
||||||
|
@ -50,7 +49,8 @@ final class OpenEdxLmsAPITemplate implements LmsAPITemplate {
|
||||||
private static final String OPEN_EDX_DEFAULT_COURSE_ENDPOINT = "/api/courses/v1/courses/";
|
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 static final String OPEN_EDX_DEFAULT_COURSE_START_URL_PREFIX = "/courses/";
|
||||||
|
|
||||||
private final LmsSetup lmsSetup;
|
private final String lmsSetupId;
|
||||||
|
private final LmsSetupDAO lmsSetupDAO;
|
||||||
private final ClientHttpRequestFactory clientHttpRequestFactory;
|
private final ClientHttpRequestFactory clientHttpRequestFactory;
|
||||||
private final InternalEncryptionService internalEncryptionService;
|
private final InternalEncryptionService internalEncryptionService;
|
||||||
private final Set<String> knownTokenAccessPaths;
|
private final Set<String> knownTokenAccessPaths;
|
||||||
|
@ -58,12 +58,14 @@ final class OpenEdxLmsAPITemplate implements LmsAPITemplate {
|
||||||
private OAuth2RestTemplate restTemplate = null;
|
private OAuth2RestTemplate restTemplate = null;
|
||||||
|
|
||||||
OpenEdxLmsAPITemplate(
|
OpenEdxLmsAPITemplate(
|
||||||
final LmsSetup lmsSetup,
|
final String lmsSetupId,
|
||||||
|
final LmsSetupDAO lmsSetupDAO,
|
||||||
final InternalEncryptionService internalEncryptionService,
|
final InternalEncryptionService internalEncryptionService,
|
||||||
final ClientHttpRequestFactory clientHttpRequestFactory,
|
final ClientHttpRequestFactory clientHttpRequestFactory,
|
||||||
final String[] alternativeTokenRequestPaths) {
|
final String[] alternativeTokenRequestPaths) {
|
||||||
|
|
||||||
this.lmsSetup = lmsSetup;
|
this.lmsSetupId = lmsSetupId;
|
||||||
|
this.lmsSetupDAO = lmsSetupDAO;
|
||||||
this.clientHttpRequestFactory = clientHttpRequestFactory;
|
this.clientHttpRequestFactory = clientHttpRequestFactory;
|
||||||
this.internalEncryptionService = internalEncryptionService;
|
this.internalEncryptionService = internalEncryptionService;
|
||||||
|
|
||||||
|
@ -75,27 +77,30 @@ final class OpenEdxLmsAPITemplate implements LmsAPITemplate {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public LmsSetup lmsSetup() {
|
public Result<LmsSetup> lmsSetup() {
|
||||||
return this.lmsSetup;
|
return this.lmsSetupDAO
|
||||||
|
.byModelId(this.lmsSetupId);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public LmsSetupTestResult testLmsSetup() {
|
public LmsSetupTestResult testLmsSetup() {
|
||||||
|
|
||||||
log.info("Test Lms Binding for OpenEdX and LmsSetup: {}", this.lmsSetup);
|
final LmsSetup lmsSetup = lmsSetup().getOrThrow();
|
||||||
|
|
||||||
|
log.info("Test Lms Binding for OpenEdX and LmsSetup: {}", lmsSetup);
|
||||||
|
|
||||||
// validation of LmsSetup
|
// validation of LmsSetup
|
||||||
if (this.lmsSetup.lmsType != LmsType.MOCKUP) {
|
if (lmsSetup.lmsType != LmsType.MOCKUP) {
|
||||||
return LmsSetupTestResult.ofMissingAttributes(LMS_SETUP.ATTR_LMS_TYPE);
|
return LmsSetupTestResult.ofMissingAttributes(LMS_SETUP.ATTR_LMS_TYPE);
|
||||||
}
|
}
|
||||||
final List<String> missingAttrs = new ArrayList<>();
|
final List<String> missingAttrs = new ArrayList<>();
|
||||||
if (StringUtils.isBlank(this.lmsSetup.lmsApiUrl)) {
|
if (StringUtils.isBlank(lmsSetup.lmsApiUrl)) {
|
||||||
missingAttrs.add(LMS_SETUP.ATTR_LMS_TYPE);
|
missingAttrs.add(LMS_SETUP.ATTR_LMS_TYPE);
|
||||||
}
|
}
|
||||||
if (StringUtils.isBlank(this.lmsSetup.getLmsAuthName())) {
|
if (StringUtils.isBlank(lmsSetup.getLmsAuthName())) {
|
||||||
missingAttrs.add(LMS_SETUP.ATTR_LMS_CLIENTNAME);
|
missingAttrs.add(LMS_SETUP.ATTR_LMS_CLIENTNAME);
|
||||||
}
|
}
|
||||||
if (StringUtils.isBlank(this.lmsSetup.getLmsAuthSecret())) {
|
if (StringUtils.isBlank(lmsSetup.getLmsAuthSecret())) {
|
||||||
missingAttrs.add(LMS_SETUP.ATTR_LMS_CLIENTSECRET);
|
missingAttrs.add(LMS_SETUP.ATTR_LMS_CLIENTSECRET);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -104,7 +109,7 @@ final class OpenEdxLmsAPITemplate implements LmsAPITemplate {
|
||||||
}
|
}
|
||||||
|
|
||||||
// request OAuth2 access token on OpenEdx API
|
// request OAuth2 access token on OpenEdx API
|
||||||
initRestTemplateAndRequestAccessToken();
|
initRestTemplateAndRequestAccessToken(lmsSetup);
|
||||||
if (this.restTemplate == null) {
|
if (this.restTemplate == null) {
|
||||||
return LmsSetupTestResult.ofTokenRequestError(
|
return LmsSetupTestResult.ofTokenRequestError(
|
||||||
"Failed to gain access token form OpenEdX Rest API: tried token endpoints: " +
|
"Failed to gain access token form OpenEdX Rest API: tried token endpoints: " +
|
||||||
|
@ -124,34 +129,35 @@ final class OpenEdxLmsAPITemplate implements LmsAPITemplate {
|
||||||
final int pageNumber,
|
final int pageNumber,
|
||||||
final int pageSize) {
|
final int pageSize) {
|
||||||
|
|
||||||
return Result.tryCatch(() -> {
|
return this.lmsSetup()
|
||||||
initRestTemplateAndRequestAccessToken();
|
.flatMap(this::initRestTemplateAndRequestAccessToken)
|
||||||
|
.map(lmsSetup -> {
|
||||||
|
|
||||||
// TODO sort and pagination
|
// TODO sort and pagination
|
||||||
final HttpHeaders httpHeaders = new HttpHeaders();
|
final HttpHeaders httpHeaders = new HttpHeaders();
|
||||||
|
|
||||||
final ResponseEntity<EdXPage> response = this.restTemplate.exchange(
|
final ResponseEntity<EdXPage> response = this.restTemplate.exchange(
|
||||||
this.lmsSetup.lmsApiUrl + OPEN_EDX_DEFAULT_COURSE_ENDPOINT,
|
lmsSetup.lmsApiUrl + OPEN_EDX_DEFAULT_COURSE_ENDPOINT,
|
||||||
HttpMethod.GET,
|
HttpMethod.GET,
|
||||||
new HttpEntity<>(httpHeaders),
|
new HttpEntity<>(httpHeaders),
|
||||||
EdXPage.class);
|
EdXPage.class);
|
||||||
final EdXPage edxpage = response.getBody();
|
final EdXPage edxpage = response.getBody();
|
||||||
|
|
||||||
final List<QuizData> content = edxpage.results
|
final List<QuizData> content = edxpage.results
|
||||||
.stream()
|
.stream()
|
||||||
.reduce(
|
.reduce(
|
||||||
new ArrayList<QuizData>(),
|
new ArrayList<QuizData>(),
|
||||||
(list, courseData) -> {
|
(list, courseData) -> {
|
||||||
list.add(quizDataOf(courseData));
|
list.add(quizDataOf(lmsSetup, courseData));
|
||||||
return list;
|
return list;
|
||||||
},
|
},
|
||||||
(list1, list2) -> {
|
(list1, list2) -> {
|
||||||
list1.addAll(list2);
|
list1.addAll(list2);
|
||||||
return list1;
|
return list1;
|
||||||
});
|
});
|
||||||
|
|
||||||
return new Page<>(edxpage.num_pages, pageNumber, sort, content);
|
return new Page<>(edxpage.num_pages, pageNumber, sort, content);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@ -166,45 +172,67 @@ final class OpenEdxLmsAPITemplate implements LmsAPITemplate {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void initRestTemplateAndRequestAccessToken() {
|
@Override
|
||||||
|
public void reset() {
|
||||||
log.info("Initialize Rest Template for OpenEdX API access. LmsSetup: {}", this.lmsSetup);
|
this.restTemplate = null;
|
||||||
|
|
||||||
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) {
|
private Result<LmsSetup> initRestTemplateAndRequestAccessToken(final LmsSetup lmsSetup) {
|
||||||
|
|
||||||
final String lmsAuthSecret = this.internalEncryptionService.decrypt(this.lmsSetup.lmsAuthSecret);
|
log.info("Initialize Rest Template for OpenEdX API access. LmsSetup: {}", lmsSetup);
|
||||||
|
|
||||||
|
return Result.tryCatch(() -> {
|
||||||
|
if (this.restTemplate != null) {
|
||||||
|
try {
|
||||||
|
this.restTemplate.getAccessToken();
|
||||||
|
return lmsSetup;
|
||||||
|
} 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 Credentials credentials = this.lmsSetupDAO
|
||||||
|
.getLmsAPIAccessCredentials(this.lmsSetupId)
|
||||||
|
.getOrThrow();
|
||||||
|
|
||||||
|
final Iterator<String> tokenAccessPaths = this.knownTokenAccessPaths.iterator();
|
||||||
|
while (tokenAccessPaths.hasNext()) {
|
||||||
|
final String accessTokenRequestPath = tokenAccessPaths.next();
|
||||||
|
try {
|
||||||
|
|
||||||
|
final OAuth2RestTemplate template = createRestTemplate(
|
||||||
|
lmsSetup,
|
||||||
|
credentials,
|
||||||
|
accessTokenRequestPath);
|
||||||
|
|
||||||
|
final OAuth2AccessToken accessToken = template.getAccessToken();
|
||||||
|
if (accessToken != null) {
|
||||||
|
this.restTemplate = template;
|
||||||
|
return lmsSetup;
|
||||||
|
}
|
||||||
|
} catch (final Exception e) {
|
||||||
|
log.info("Failed to request access token on access token request path: {}", accessTokenRequestPath,
|
||||||
|
e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new IllegalArgumentException("Unable to establish OpenEdX API connection for lmsSetup: " + lmsSetup);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private OAuth2RestTemplate createRestTemplate(
|
||||||
|
final LmsSetup lmsSetup,
|
||||||
|
final LmsSetupDAO.Credentials credentials,
|
||||||
|
final String accessTokenRequestPath) {
|
||||||
|
|
||||||
|
final String lmsAuthSecret = this.internalEncryptionService.decrypt(credentials.secret);
|
||||||
|
|
||||||
final ClientCredentialsResourceDetails details = new ClientCredentialsResourceDetails();
|
final ClientCredentialsResourceDetails details = new ClientCredentialsResourceDetails();
|
||||||
details.setAccessTokenUri(this.lmsSetup.lmsApiUrl + accessTokenRequestPath);
|
details.setAccessTokenUri(lmsSetup.lmsApiUrl + accessTokenRequestPath);
|
||||||
details.setClientId(this.lmsSetup.lmsAuthName);
|
details.setClientId(credentials.clientId);
|
||||||
details.setClientSecret(lmsAuthSecret);
|
details.setClientSecret(lmsAuthSecret);
|
||||||
details.setGrantType("client_credentials");
|
details.setGrantType("client_credentials");
|
||||||
|
|
||||||
|
@ -214,51 +242,14 @@ final class OpenEdxLmsAPITemplate implements LmsAPITemplate {
|
||||||
|
|
||||||
final OAuth2RestTemplate template = new OAuth2RestTemplate(details);
|
final OAuth2RestTemplate template = new OAuth2RestTemplate(details);
|
||||||
template.setRequestFactory(this.clientHttpRequestFactory);
|
template.setRequestFactory(this.clientHttpRequestFactory);
|
||||||
|
|
||||||
final OAuth2AccessToken previousAccessToken = loadPreviousAccessToken();
|
|
||||||
if (previousAccessToken != null) {
|
|
||||||
template.getOAuth2ClientContext().setAccessToken(previousAccessToken);
|
|
||||||
}
|
|
||||||
return template;
|
return template;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void storeAccessToken(final OAuth2AccessToken accessToken) {
|
private QuizData quizDataOf(
|
||||||
try {
|
final LmsSetup lmsSetup,
|
||||||
|
final CourseData courseData) {
|
||||||
|
|
||||||
final String accessTokenString = accessToken.getValue();
|
final String startURI = lmsSetup.lmsApiUrl + OPEN_EDX_DEFAULT_COURSE_START_URL_PREFIX + courseData.id;
|
||||||
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(
|
return new QuizData(
|
||||||
courseData.id,
|
courseData.id,
|
||||||
courseData.name,
|
courseData.name,
|
||||||
|
|
|
@ -16,6 +16,7 @@ import org.springframework.web.bind.annotation.RestController;
|
||||||
|
|
||||||
import ch.ethz.seb.sebserver.gbl.api.SEBServerRestEndpoints;
|
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.Domain.LMS_SETUP;
|
||||||
|
import ch.ethz.seb.sebserver.gbl.model.Entity;
|
||||||
import ch.ethz.seb.sebserver.gbl.model.EntityType;
|
import ch.ethz.seb.sebserver.gbl.model.EntityType;
|
||||||
import ch.ethz.seb.sebserver.gbl.model.Page;
|
import ch.ethz.seb.sebserver.gbl.model.Page;
|
||||||
import ch.ethz.seb.sebserver.gbl.model.exam.QuizData;
|
import ch.ethz.seb.sebserver.gbl.model.exam.QuizData;
|
||||||
|
@ -23,6 +24,7 @@ import ch.ethz.seb.sebserver.gbl.profile.WebServiceProfile;
|
||||||
import ch.ethz.seb.sebserver.gbl.util.Utils;
|
import ch.ethz.seb.sebserver.gbl.util.Utils;
|
||||||
import ch.ethz.seb.sebserver.webservice.servicelayer.authorization.AuthorizationGrantService;
|
import ch.ethz.seb.sebserver.webservice.servicelayer.authorization.AuthorizationGrantService;
|
||||||
import ch.ethz.seb.sebserver.webservice.servicelayer.authorization.PrivilegeType;
|
import ch.ethz.seb.sebserver.webservice.servicelayer.authorization.PrivilegeType;
|
||||||
|
import ch.ethz.seb.sebserver.webservice.servicelayer.authorization.UserService;
|
||||||
import ch.ethz.seb.sebserver.webservice.servicelayer.lms.LmsAPIService;
|
import ch.ethz.seb.sebserver.webservice.servicelayer.lms.LmsAPIService;
|
||||||
import ch.ethz.seb.sebserver.webservice.servicelayer.lms.LmsAPITemplate;
|
import ch.ethz.seb.sebserver.webservice.servicelayer.lms.LmsAPITemplate;
|
||||||
|
|
||||||
|
@ -51,6 +53,10 @@ public class QuizImportController {
|
||||||
|
|
||||||
@RequestMapping(method = RequestMethod.GET)
|
@RequestMapping(method = RequestMethod.GET)
|
||||||
public Page<QuizData> search(
|
public Page<QuizData> search(
|
||||||
|
@RequestParam(
|
||||||
|
name = Entity.FILTER_ATTR_INSTITUTION,
|
||||||
|
required = true,
|
||||||
|
defaultValue = UserService.USERS_INSTITUTION_AS_DEFAULT) final Long institutionId,
|
||||||
@RequestParam(name = LMS_SETUP.ATTR_ID, required = true) final Long lmsSetupId,
|
@RequestParam(name = LMS_SETUP.ATTR_ID, required = true) final Long lmsSetupId,
|
||||||
@RequestParam(name = QuizData.FILTER_ATTR_NAME, required = false) final String nameLike,
|
@RequestParam(name = QuizData.FILTER_ATTR_NAME, required = false) final String nameLike,
|
||||||
@RequestParam(name = QuizData.FILTER_ATTR_START_TIME, required = false) final String startTime,
|
@RequestParam(name = QuizData.FILTER_ATTR_START_TIME, required = false) final String startTime,
|
||||||
|
@ -65,7 +71,7 @@ public class QuizImportController {
|
||||||
this.authorizationGrantService.checkPrivilege(
|
this.authorizationGrantService.checkPrivilege(
|
||||||
EntityType.EXAM,
|
EntityType.EXAM,
|
||||||
PrivilegeType.READ_ONLY,
|
PrivilegeType.READ_ONLY,
|
||||||
lmsAPITemplate.lmsSetup().institutionId);
|
institutionId);
|
||||||
|
|
||||||
return lmsAPITemplate.getQuizzes(
|
return lmsAPITemplate.getQuizzes(
|
||||||
nameLike,
|
nameLike,
|
||||||
|
|
|
@ -0,0 +1,32 @@
|
||||||
|
/*
|
||||||
|
* 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 static org.junit.Assert.assertEquals;
|
||||||
|
import static org.mockito.Mockito.mock;
|
||||||
|
import static org.mockito.Mockito.when;
|
||||||
|
|
||||||
|
import org.junit.Test;
|
||||||
|
import org.springframework.core.env.Environment;
|
||||||
|
|
||||||
|
public class InternalEncryptionServiceTest {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testEncryptSimpleSecret() {
|
||||||
|
final Environment envMock = mock(Environment.class);
|
||||||
|
when(envMock.getRequiredProperty(InternalEncryptionService.SEBSERVER_WEBSERVICE_INTERNAL_SECRET_KEY))
|
||||||
|
.thenReturn("secret1");
|
||||||
|
|
||||||
|
final InternalEncryptionService service = new InternalEncryptionService(envMock);
|
||||||
|
final String encrypt = service.encrypt("text1");
|
||||||
|
final String decrypt = service.decrypt(encrypt);
|
||||||
|
assertEquals("text1", decrypt);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
Loading…
Reference in a new issue