SEBSERV-301 finished implementation and testing with token

This commit is contained in:
anhefti 2023-01-25 13:30:53 +01:00
parent b97152f91a
commit a5bab8fc9f
12 changed files with 95 additions and 106 deletions

View file

@ -664,7 +664,7 @@ public final class Utils {
if (sb.length() > 0) {
sb.append(Constants.AMPERSAND);
}
if (sb.length() == 1) {
if (values.size() == 1) {
return sb.append(name).append(Constants.EQUALITY_SIGN).append(values.get(0));
}
return sb.append(toAppFormUrlEncodedBody(name, values));

View file

@ -221,19 +221,16 @@ public class LmsSetupForm implements TemplateComposer {
lmsSetup.getLmsAuthName())
.mandatory(!readonly))
.addFieldIf(
isEdit,
() -> FormBuilder.text(
Domain.LMS_SETUP.ATTR_LMS_CLIENTSECRET,
FORM_SECRET_LMS_TEXT_KEY)
.asPasswordField()
.mandatory(!readonly))
.addField(FormBuilder.password(
Domain.LMS_SETUP.ATTR_LMS_CLIENTSECRET,
FORM_SECRET_LMS_TEXT_KEY,
lmsSetup.getLmsAuthSecret())
.mandatory(!readonly))
.addFieldIf(
isEdit,
() -> FormBuilder.text(
Domain.LMS_SETUP.ATTR_LMS_REST_API_TOKEN,
FORM_TOKEN_LMS_TEXT_KEY))
.addField(FormBuilder.password(
Domain.LMS_SETUP.ATTR_LMS_REST_API_TOKEN,
FORM_TOKEN_LMS_TEXT_KEY,
lmsSetup.lmsRestApiToken))
.addFieldIf(
isEdit,
@ -277,10 +274,10 @@ public class LmsSetupForm implements TemplateComposer {
.withEmptyCellSpan(0))
.addFieldIf(
() -> !readonly,
() -> FormBuilder.text(
() -> FormBuilder.password(
Domain.LMS_SETUP.ATTR_LMS_PROXY_AUTH_SECRET,
FORM_PROXY_PWD_KEY)
.asPasswordField()
FORM_PROXY_PWD_KEY,
lmsSetup.proxyAuthSecret)
.withInputSpan(3)
.withLabelSpan(2)
.withEmptyCellSeparation(true)

View file

@ -40,7 +40,6 @@ 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.InstitutionRecordDynamicSqlSupport;
import ch.ethz.seb.sebserver.webservice.datalayer.batis.mapper.LmsSetupRecordDynamicSqlSupport;
import ch.ethz.seb.sebserver.webservice.datalayer.batis.mapper.LmsSetupRecordMapper;
@ -171,32 +170,28 @@ public class LmsSetupDAOImpl implements LmsSetupDAO {
checkUniqueName(lmsSetup);
final LmsSetupRecord savedRecord = recordById(lmsSetup.id)
.getOrThrow();
// final LmsSetupRecord savedRecord = recordById(lmsSetup.id)
// .getOrThrow();
final ClientCredentials lmsCredentials = createAPIClientCredentials(lmsSetup);
final ClientCredentials proxyCredentials = createProxyClientCredentials(lmsSetup);
// final ClientCredentials lmsCredentials = createAPIClientCredentials(lmsSetup);
// final ClientCredentials proxyCredentials = createProxyClientCredentials(lmsSetup);
final LmsSetupRecord newRecord = new LmsSetupRecord(
lmsSetup.id,
lmsSetup.institutionId,
lmsSetup.name,
(lmsSetup.lmsType != null) ? lmsSetup.lmsType.name() : null,
lmsSetup.lmsApiUrl,
lmsCredentials.clientIdAsString(),
(lmsCredentials.hasSecret())
? lmsCredentials.secretAsString()
: savedRecord.getLmsClientsecret(),
(lmsCredentials.hasAccessToken())
? lmsCredentials.accessTokenAsString()
: savedRecord.getLmsRestApiToken(),
lmsSetup.lmsAuthName,
this.encryptForSave(lmsSetup.lmsAuthSecret),
this.encryptForSave(lmsSetup.lmsRestApiToken),
lmsSetup.getProxyHost(),
lmsSetup.getProxyPort(),
proxyCredentials.clientIdAsString(),
proxyCredentials.secretAsString(),
lmsSetup.proxyAuthUsername,
this.encryptForSave(lmsSetup.proxyAuthSecret),
System.currentTimeMillis(),
savedRecord.getActive());
null);
this.lmsSetupRecordMapper.updateByPrimaryKey(newRecord);
this.lmsSetupRecordMapper.updateByPrimaryKeySelective(newRecord);
return this.lmsSetupRecordMapper.selectByPrimaryKey(lmsSetup.id);
})
.flatMap(this::toDomainModel)
@ -210,21 +205,19 @@ public class LmsSetupDAOImpl implements LmsSetupDAO {
checkUniqueName(lmsSetup);
final ClientCredentials lmsCredentials = createAPIClientCredentials(lmsSetup);
final ClientCredentials proxyCredentials = createProxyClientCredentials(lmsSetup);
final LmsSetupRecord newRecord = new LmsSetupRecord(
null,
lmsSetup.institutionId,
lmsSetup.name,
(lmsSetup.lmsType != null) ? lmsSetup.lmsType.name() : null,
lmsSetup.lmsApiUrl,
lmsCredentials.clientIdAsString(),
lmsCredentials.secretAsString(),
lmsCredentials.accessTokenAsString(),
lmsSetup.lmsAuthName,
this.encryptForSave(lmsSetup.lmsAuthSecret),
this.encryptForSave(lmsSetup.lmsRestApiToken),
lmsSetup.getProxyHost(),
lmsSetup.getProxyPort(),
proxyCredentials.clientIdAsString(),
proxyCredentials.secretAsString(),
lmsSetup.proxyAuthUsername,
this.encryptForSave(lmsSetup.proxyAuthSecret),
System.currentTimeMillis(),
BooleanUtils.toInteger(false));
@ -390,35 +383,35 @@ public class LmsSetupDAOImpl implements LmsSetupDAO {
private Result<LmsSetup> toDomainModel(final LmsSetupRecord record) {
final ClientCredentials clientCredentials = new ClientCredentials(
record.getLmsClientname(),
record.getLmsClientsecret(),
record.getLmsRestApiToken());
final ClientCredentials proxyCredentials = new ClientCredentials(
record.getLmsProxyAuthUsername(),
record.getLmsProxyAuthSecret());
return Result.tryCatch(() -> new LmsSetup(
record.getId(),
record.getInstitutionId(),
record.getName(),
LmsType.valueOf(record.getLmsType()),
Utils.toString(clientCredentials.clientId),
null,
record.getLmsClientname(),
record.getLmsClientsecret(),
record.getLmsUrl(),
Utils.toString(
this.clientCredentialService
.getPlainAccessToken(clientCredentials)
.getOr(null)),
record.getLmsRestApiToken(),
record.getLmsProxyHost(),
record.getLmsProxyPort(),
Utils.toString(proxyCredentials.clientId),
Utils.toString(proxyCredentials.secret),
record.getLmsProxyAuthUsername(),
record.getLmsProxyAuthSecret(),
BooleanUtils.toBooleanObject(record.getActive()),
record.getUpdateTime()));
}
private String encryptForSave(final String input) {
if (StringUtils.isBlank(input)) {
return input;
}
// check if input is already encrypted and possible to decrypt
if (this.clientCredentialService.decrypt(input).hasError()) {
return this.clientCredentialService.encrypt(input).get().toString();
} else {
return input;
}
}
// check if same name already exists for the same institution
// if true an APIMessageException with a field validation error is thrown
private void checkUniqueName(final LmsSetup lmsSetup) {
@ -438,21 +431,4 @@ public class LmsSetupDAOImpl implements LmsSetupDAO {
}
}
private ClientCredentials createProxyClientCredentials(final LmsSetup lmsSetup) {
return (StringUtils.isBlank(lmsSetup.proxyAuthUsername))
? new ClientCredentials(null, null)
: this.clientCredentialService.encryptClientCredentials(
lmsSetup.proxyAuthUsername,
lmsSetup.proxyAuthSecret)
.getOrThrow();
}
private ClientCredentials createAPIClientCredentials(final LmsSetup lmsSetup) {
return this.clientCredentialService.encryptClientCredentials(
lmsSetup.lmsAuthName,
lmsSetup.lmsAuthSecret,
lmsSetup.lmsRestApiToken)
.getOrThrow();
}
}

View file

@ -291,7 +291,8 @@ public class LmsAPIServiceImpl implements LmsAPIService {
public ClientCredentials getLmsClientCredentials() {
return this.clientCredentialService.encryptClientCredentials(
this.lmsSetup.getLmsAuthName(),
this.lmsSetup.getLmsAuthSecret())
this.lmsSetup.getLmsAuthSecret(),
this.lmsSetup.lmsRestApiToken)
.getOrThrow();
}
@ -303,7 +304,8 @@ public class LmsAPIServiceImpl implements LmsAPIService {
this.lmsSetup.proxyPort,
this.clientCredentialService.encryptClientCredentials(
this.lmsSetup.proxyAuthUsername,
this.lmsSetup.proxyAuthSecret)
this.lmsSetup.proxyAuthSecret,
this.lmsSetup.lmsRestApiToken)
.getOrThrow())
: null;
}

View file

@ -344,8 +344,6 @@ public class MoodleRestTemplateFactoryImpl implements MoodleRestTemplateFactory
functionReqEntity,
String.class);
System.out.println("*************** response: " + response);
final LmsSetup lmsSetup = this.apiTemplateDataSupplier.getLmsSetup();
if (response.getStatusCode() != HttpStatus.OK) {
throw new RuntimeException(
@ -358,6 +356,10 @@ public class MoodleRestTemplateFactoryImpl implements MoodleRestTemplateFactory
// NOTE: for some unknown reason, Moodles API error responses come with a 200 OK response HTTP Status
// So this is a special Moodle specific error handling here...
if (body.startsWith("{exception") || body.contains("\"exception\":")) {
// if no courses has been found for this page, just return (Plugin)
if (body.contains("nocoursefound")) {
return body;
}
// Reset access token to get new on next call (fix access if token is expired)
// NOTE: find a way to verify token invalidity response from Moodle.
// Unfortunately there is not a lot of Moodle documentation for the API error handling around.

View file

@ -510,16 +510,16 @@ public abstract class MoodleUtils {
@JsonIgnoreProperties(ignoreUnknown = true)
public static final class MoodleQuizRestriction {
public final String quizid;
public final String configkeys;
public final String browserkeys;
public final List<String> configkeys;
public final List<String> browserkeys;
public final String quitlink;
public final String quitsecret;
@JsonCreator
public MoodleQuizRestriction(
@JsonProperty("quizid") final String quizid,
@JsonProperty("configkeys") final String configkeys,
@JsonProperty("browserkeys") final String browserkeys,
@JsonProperty("configkeys") final List<String> configkeys,
@JsonProperty("browserkeys") final List<String> browserkeys,
@JsonProperty("quitlink") final String quitlink,
@JsonProperty("quitsecret") final String quitsecret) {

View file

@ -72,6 +72,7 @@ public class MoodlePluginCourseAccess extends AbstractCachedCourseAccess impleme
public static final String ATTR_FIELD = "field";
public static final String ATTR_VALUE = "value";
public static final String ATTR_VALUE_ARRAY = "values[]";
public static final String ATTR_ID = "id";
public static final String ATTR_SHORTNAME = "shortname";
@ -300,7 +301,7 @@ public class MoodlePluginCourseAccess extends AbstractCachedCourseAccess impleme
final MultiValueMap<String, String> queryAttributes = new LinkedMultiValueMap<>();
queryAttributes.add(ATTR_FIELD, ATTR_ID);
queryAttributes.add(ATTR_VALUE, examineeSessionId);
queryAttributes.add(ATTR_VALUE_ARRAY, examineeSessionId);
final String userDetailsJSON = template.callMoodleAPIFunction(
USERS_API_FUNCTION_NAME,
@ -442,6 +443,21 @@ public class MoodlePluginCourseAccess extends AbstractCachedCourseAccess impleme
MoodleUtils.checkJSONFormat(courseKeyPageJSON);
if (courseKeyPageJSON.startsWith("{\"exception\":")) {
if (courseKeyPageJSON.contains("nocoursefound")) {
if (log.isDebugEnabled()) {
log.debug(
"Got nocoursefound exception from Moodle for page: {}. "
+ "Assuming that there are no more courses and stop fetching.",
page);
}
return Collections.emptyList();
}
log.error("Moodle exception while page fetching on page: {}, response: {}", page, courseKeyPageJSON);
log.info("Stop fetching because of Moodle error response");
return Collections.emptyList();
}
final CoursesPlugin coursePage = this.jsonMapper.readValue(courseKeyPageJSON, CoursesPlugin.class);
if (coursePage == null) {

View file

@ -9,10 +9,8 @@
package ch.ethz.seb.sebserver.webservice.servicelayer.lms.impl.moodle.plugin;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import org.apache.commons.lang3.StringUtils;
@ -20,7 +18,6 @@ import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.util.LinkedMultiValueMap;
import ch.ethz.seb.sebserver.gbl.Constants;
import ch.ethz.seb.sebserver.gbl.api.JSONMapper;
import ch.ethz.seb.sebserver.gbl.model.exam.Exam;
import ch.ethz.seb.sebserver.gbl.model.exam.SEBRestriction;
@ -254,24 +251,14 @@ public class MoodlePluginCourseRestriction implements SEBRestrictionAPI {
final Exam exam,
final MoodleQuizRestriction moodleRestriction) {
final List<String> configKeys = StringUtils.isNoneBlank(moodleRestriction.configkeys)
? Arrays.asList(StringUtils.split(
moodleRestriction.configkeys,
Constants.LIST_SEPARATOR))
: Collections.emptyList();
final List<String> browserExamKeys = StringUtils.isNoneBlank(moodleRestriction.browserkeys)
? new ArrayList<>(Arrays.asList(StringUtils.split(
moodleRestriction.browserkeys,
Constants.LIST_SEPARATOR)))
: Collections.emptyList();
final Map<String, String> additionalProperties = new HashMap<>();
additionalProperties.put(ATTRIBUTE_QUIT_URL, moodleRestriction.quitlink);
additionalProperties.put(ATTRIBUTE_QUIT_SECRET, moodleRestriction.quitsecret);
return new SEBRestriction(
exam.id,
configKeys,
browserExamKeys,
moodleRestriction.configkeys,
moodleRestriction.browserkeys,
additionalProperties);
}

View file

@ -92,6 +92,9 @@ public class SEBClientSessionServiceImpl implements SEBClientSessionService {
@Override
public void updateASKGrants() {
// TODO check only for exams with enabled ASK check!!!
this.clientConnectionDAO
.getAllActiveNotGranted()
.onError(error -> log.error("Failed to get none granted active client connections: ", error))

View file

@ -103,8 +103,7 @@ public class LmsSetupAPITest extends AdministrationAPIIntegrationTester {
assertEquals("new LmsSetup 1", lmsSetup.name);
assertTrue(LmsType.MOCKUP == lmsSetup.lmsType);
assertEquals("lms1Name", lmsSetup.lmsAuthName);
// secrets, once set are not exposed
assertEquals(null, lmsSetup.lmsAuthSecret);
assertNotNull(lmsSetup.lmsAuthSecret);
assertFalse(lmsSetup.active);
// activate

View file

@ -10,6 +10,7 @@ package ch.ethz.seb.sebserver.webservice.servicelayer.lms.impl.moodle;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
@ -29,7 +30,6 @@ import org.springframework.web.util.UriComponentsBuilder;
import com.fasterxml.jackson.core.JsonProcessingException;
import ch.ethz.seb.sebserver.gbl.Constants;
import ch.ethz.seb.sebserver.gbl.api.JSONMapper;
import ch.ethz.seb.sebserver.gbl.model.institution.LmsSetup.LmsType;
import ch.ethz.seb.sebserver.gbl.model.institution.LmsSetupTestResult;
@ -286,8 +286,8 @@ public class MoodleMockupRestTemplateFactory implements MoodleRestTemplateFactor
final MoodleQuizRestriction moodleQuizRestriction = new MoodleQuizRestriction(
quizId,
StringUtils.join(configKeys, Constants.LIST_SEPARATOR),
StringUtils.join(beks, Constants.LIST_SEPARATOR),
trimList(configKeys),
trimList(beks),
quitURL,
quitSecret);
@ -303,6 +303,13 @@ public class MoodleMockupRestTemplateFactory implements MoodleRestTemplateFactor
}
}
private List<String> trimList(final List<String> list) {
if (list.size() == 1 && StringUtils.isBlank(list.get(0))) {
return Collections.emptyList();
}
return list;
}
private String respondGetRestriction(final String quizId, final MultiValueMap<String, String> queryAttributes) {
final MoodleQuizRestrictions moodleQuizRestriction = this.restrcitions.get(quizId);
if (moodleQuizRestriction != null) {
@ -328,7 +335,7 @@ public class MoodleMockupRestTemplateFactory implements MoodleRestTemplateFactor
}
private String respondUsers(final MultiValueMap<String, String> queryAttributes) {
final String id = queryAttributes.getFirst(MoodlePluginCourseAccess.ATTR_VALUE);
final String id = queryAttributes.getFirst(MoodlePluginCourseAccess.ATTR_VALUE_ARRAY);
final String field = queryAttributes.getFirst(MoodlePluginCourseAccess.ATTR_FIELD);
if (!field.equals(MoodlePluginCourseAccess.ATTR_ID)) {

View file

@ -260,7 +260,7 @@ public class MoodlePluginCourseAccessTest {
+ "testLog=["
+ "callMoodleAPIFunction: core_user_get_users_by_field], "
+ "callLog=["
+ "<field=id&value=2,[Content-Type:\"application/x-www-form-urlencoded\"]>]]]",
+ "<field=id&values[]=2,[Content-Type:\"application/x-www-form-urlencoded\"]>]]]",
candidate.toTestString());
}
@ -277,7 +277,7 @@ public class MoodlePluginCourseAccessTest {
"MoodlePluginCourseAccess [pageSize=500, maxSize=10000, cutoffTimeOffset=3, "
+ "restTemplate=MockupMoodleRestTemplate [accessToken=MockupMoodleRestTemplate-Test-Token, url=https://test.org/, "
+ "testLog=[callMoodleAPIFunction: core_user_get_users_by_field], "
+ "callLog=[<field=id&value=1,[Content-Type:\"application/x-www-form-urlencoded\"]>]]]",
+ "callLog=[<field=id&values[]=1,[Content-Type:\"application/x-www-form-urlencoded\"]>]]]",
candidate.toTestString());
}
@ -306,7 +306,7 @@ public class MoodlePluginCourseAccessTest {
@Override
public ClientCredentials getLmsClientCredentials() {
return new ClientCredentials("lms-user", "lms-user-secret");
return new ClientCredentials("lms-user", "lms-user-secret", "lms-user-token");
}
@Override