SEBSERV-418
This commit is contained in:
parent
1ae00cc4ab
commit
9618b942fb
15 changed files with 613 additions and 135 deletions
7
pom.xml
7
pom.xml
|
@ -348,13 +348,18 @@
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>org.apache.commons</groupId>
|
<groupId>org.apache.commons</groupId>
|
||||||
<artifactId>commons-text</artifactId>
|
<artifactId>commons-text</artifactId>
|
||||||
<version>1.8</version>
|
<version>1.10.0</version>
|
||||||
</dependency>
|
</dependency>
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>com.github.vladimir-bukhtoyarov</groupId>
|
<groupId>com.github.vladimir-bukhtoyarov</groupId>
|
||||||
<artifactId>bucket4j-core</artifactId>
|
<artifactId>bucket4j-core</artifactId>
|
||||||
<version>4.10.0</version>
|
<version>4.10.0</version>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>io.jsonwebtoken</groupId>
|
||||||
|
<artifactId>jjwt</artifactId>
|
||||||
|
<version>0.9.1</version>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
|
|
||||||
<!-- Testing -->
|
<!-- Testing -->
|
||||||
|
|
|
@ -64,8 +64,11 @@ public final class API {
|
||||||
|
|
||||||
public static final String OAUTH_ENDPOINT = "/oauth";
|
public static final String OAUTH_ENDPOINT = "/oauth";
|
||||||
public static final String OAUTH_TOKEN_ENDPOINT = OAUTH_ENDPOINT + "/token";
|
public static final String OAUTH_TOKEN_ENDPOINT = OAUTH_ENDPOINT + "/token";
|
||||||
public static final String OAUTH_JWTTOKEN_ENDPOINT = OAUTH_ENDPOINT + "/jwttoken";
|
|
||||||
|
public static final String OAUTH_JWT_TOKEN_ENDPOINT = OAUTH_ENDPOINT + "/jwttoken";
|
||||||
|
public static final String OAUTH_JWT_TOKEN_VERIFY_ENDPOINT = OAUTH_JWT_TOKEN_ENDPOINT + "/verify";
|
||||||
public static final String OAUTH_REVOKE_TOKEN_ENDPOINT = OAUTH_ENDPOINT + "/revoke-token";
|
public static final String OAUTH_REVOKE_TOKEN_ENDPOINT = OAUTH_ENDPOINT + "/revoke-token";
|
||||||
|
public static final String SPS_OAUTH_JWT_TOKEN_ENDPOINT = OAUTH_ENDPOINT + "/jwttoken";
|
||||||
|
|
||||||
public static final String GRANT_TYPE_PASSWORD = "password";
|
public static final String GRANT_TYPE_PASSWORD = "password";
|
||||||
public static final String GRANT_TYPE_CLIENT = "client_credentials";
|
public static final String GRANT_TYPE_CLIENT = "client_credentials";
|
||||||
|
@ -175,6 +178,8 @@ public final class API {
|
||||||
public static final String LMS_FULL_INTEGRATION_EXAM_TEMPLATE_ID = "exam_template_id";
|
public static final String LMS_FULL_INTEGRATION_EXAM_TEMPLATE_ID = "exam_template_id";
|
||||||
public static final String LMS_FULL_INTEGRATION_QUIT_PASSWORD = "quit_password";
|
public static final String LMS_FULL_INTEGRATION_QUIT_PASSWORD = "quit_password";
|
||||||
public static final String LMS_FULL_INTEGRATION_QUIT_LINK = "quit_link";
|
public static final String LMS_FULL_INTEGRATION_QUIT_LINK = "quit_link";
|
||||||
|
public static final String LMS_FULL_INTEGRATION_USER_ID = "user_id";
|
||||||
|
public static final String LMS_FULL_INTEGRATION_USER_NAME = "user_name";
|
||||||
public static final String LMS_FULL_INTEGRATION_TIME_ZONE = "account_time_zone";
|
public static final String LMS_FULL_INTEGRATION_TIME_ZONE = "account_time_zone";
|
||||||
|
|
||||||
public static final String USER_ACCOUNT_ENDPOINT = "/useraccount";
|
public static final String USER_ACCOUNT_ENDPOINT = "/useraccount";
|
||||||
|
|
|
@ -0,0 +1,37 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2019 ETH Zürich, IT Services
|
||||||
|
*
|
||||||
|
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||||
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
|
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package ch.ethz.seb.sebserver.gbl.model.user;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.annotation.JsonCreator;
|
||||||
|
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||||
|
import org.springframework.security.oauth2.common.OAuth2AccessToken;
|
||||||
|
|
||||||
|
public class TokenLoginInfo {
|
||||||
|
@JsonProperty
|
||||||
|
public final String username;
|
||||||
|
@JsonProperty
|
||||||
|
public final String userUUID;
|
||||||
|
@JsonProperty
|
||||||
|
public final String redirect;
|
||||||
|
@JsonProperty
|
||||||
|
public final OAuth2AccessToken login;
|
||||||
|
|
||||||
|
@JsonCreator
|
||||||
|
public TokenLoginInfo(
|
||||||
|
@JsonProperty final String username,
|
||||||
|
@JsonProperty final String userUUID,
|
||||||
|
@JsonProperty final String redirect,
|
||||||
|
@JsonProperty final OAuth2AccessToken login) {
|
||||||
|
|
||||||
|
this.username = username;
|
||||||
|
this.userUUID = userUUID;
|
||||||
|
this.redirect = redirect;
|
||||||
|
this.login = login;
|
||||||
|
}
|
||||||
|
}
|
|
@ -49,6 +49,10 @@ public class Cryptor {
|
||||||
this.internalPWD = environment.getProperty("sebserver.webservice.internalSecret");
|
this.internalPWD = environment.getProperty("sebserver.webservice.internalSecret");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public CharSequence getInternalPWD() {
|
||||||
|
return this.internalPWD;
|
||||||
|
}
|
||||||
|
|
||||||
/** Use this to encrypt a text with the internal password
|
/** Use this to encrypt a text with the internal password
|
||||||
*
|
*
|
||||||
* @param text The text to encrypt with the internal password
|
* @param text The text to encrypt with the internal password
|
||||||
|
|
|
@ -330,7 +330,7 @@ public class MonitoringProctoringService {
|
||||||
String.class);
|
String.class);
|
||||||
|
|
||||||
// JWT token request URL
|
// JWT token request URL
|
||||||
final String jwtTokenURL = settings.spsServiceURL + API.OAUTH_JWTTOKEN_ENDPOINT;
|
final String jwtTokenURL = settings.spsServiceURL + API.SPS_OAUTH_JWT_TOKEN_ENDPOINT;
|
||||||
|
|
||||||
// Basic Auth header and content type header
|
// Basic Auth header and content type header
|
||||||
final HttpHeaders httpHeaders = new HttpHeaders();
|
final HttpHeaders httpHeaders = new HttpHeaders();
|
||||||
|
|
|
@ -0,0 +1,36 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2019 ETH Zürich, IT Services
|
||||||
|
*
|
||||||
|
* 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.authorization;
|
||||||
|
|
||||||
|
import ch.ethz.seb.sebserver.gbl.model.exam.Exam;
|
||||||
|
import ch.ethz.seb.sebserver.gbl.model.user.TokenLoginInfo;
|
||||||
|
import ch.ethz.seb.sebserver.gbl.model.user.UserInfo;
|
||||||
|
import ch.ethz.seb.sebserver.gbl.util.Result;
|
||||||
|
|
||||||
|
public interface TeacherAccountService {
|
||||||
|
|
||||||
|
Result<UserInfo> createNewTeacherAccountForExam(
|
||||||
|
Exam exam,
|
||||||
|
String userId,
|
||||||
|
String username,
|
||||||
|
String timezone);
|
||||||
|
|
||||||
|
Result<Exam> deleteTeacherAccountsForExam(final Exam exam);
|
||||||
|
|
||||||
|
Result<String> getOneTimeTokenForTeacherAccount(
|
||||||
|
Exam exam,
|
||||||
|
String userId,
|
||||||
|
String username,
|
||||||
|
String timezone,
|
||||||
|
final boolean createIfNotExists);
|
||||||
|
|
||||||
|
Result<TokenLoginInfo> verifyOneTimeTokenForTeacherAccount(String token);
|
||||||
|
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,323 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2019 ETH Zürich, IT Services
|
||||||
|
*
|
||||||
|
* 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.authorization.impl;
|
||||||
|
|
||||||
|
import java.util.*;
|
||||||
|
|
||||||
|
import ch.ethz.seb.sebserver.gbl.Constants;
|
||||||
|
import ch.ethz.seb.sebserver.gbl.api.APIMessage;
|
||||||
|
import ch.ethz.seb.sebserver.gbl.api.EntityType;
|
||||||
|
import ch.ethz.seb.sebserver.gbl.model.Domain;
|
||||||
|
import ch.ethz.seb.sebserver.gbl.model.EntityKey;
|
||||||
|
import ch.ethz.seb.sebserver.gbl.model.exam.Exam;
|
||||||
|
import ch.ethz.seb.sebserver.gbl.model.user.TokenLoginInfo;
|
||||||
|
import ch.ethz.seb.sebserver.gbl.model.user.UserInfo;
|
||||||
|
import ch.ethz.seb.sebserver.gbl.model.user.UserMod;
|
||||||
|
import ch.ethz.seb.sebserver.gbl.model.user.UserRole;
|
||||||
|
import ch.ethz.seb.sebserver.gbl.profile.WebServiceProfile;
|
||||||
|
import ch.ethz.seb.sebserver.gbl.util.Cryptor;
|
||||||
|
import ch.ethz.seb.sebserver.gbl.util.Result;
|
||||||
|
import ch.ethz.seb.sebserver.gbl.util.Utils;
|
||||||
|
import ch.ethz.seb.sebserver.webservice.datalayer.batis.model.AdditionalAttributeRecord;
|
||||||
|
import ch.ethz.seb.sebserver.webservice.servicelayer.authorization.TeacherAccountService;
|
||||||
|
import ch.ethz.seb.sebserver.webservice.servicelayer.dao.AdditionalAttributesDAO;
|
||||||
|
import ch.ethz.seb.sebserver.webservice.servicelayer.dao.ExamDAO;
|
||||||
|
import ch.ethz.seb.sebserver.webservice.servicelayer.dao.FilterMap;
|
||||||
|
import ch.ethz.seb.sebserver.webservice.servicelayer.dao.UserDAO;
|
||||||
|
import ch.ethz.seb.sebserver.webservice.servicelayer.session.ScreenProctoringService;
|
||||||
|
import ch.ethz.seb.sebserver.webservice.weblayer.oauth.AdminAPIClientDetails;
|
||||||
|
import io.jsonwebtoken.Claims;
|
||||||
|
import io.jsonwebtoken.Jwts;
|
||||||
|
import io.jsonwebtoken.SignatureAlgorithm;
|
||||||
|
import org.apache.commons.lang3.StringUtils;
|
||||||
|
import org.joda.time.DateTimeZone;
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
import org.springframework.context.annotation.Lazy;
|
||||||
|
import org.springframework.http.ResponseEntity;
|
||||||
|
import org.springframework.security.authentication.BadCredentialsException;
|
||||||
|
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
|
||||||
|
import org.springframework.security.oauth2.common.OAuth2AccessToken;
|
||||||
|
import org.springframework.security.oauth2.provider.endpoint.TokenEndpoint;
|
||||||
|
import org.springframework.security.web.authentication.WebAuthenticationDetails;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
|
@Lazy
|
||||||
|
@Service
|
||||||
|
@WebServiceProfile
|
||||||
|
public class TeacherAccountServiceImpl implements TeacherAccountService {
|
||||||
|
|
||||||
|
private static final Logger log = LoggerFactory.getLogger(TeacherAccountServiceImpl.class);
|
||||||
|
|
||||||
|
private static final String SUBJECT_CLAIM_NAME = "sub";
|
||||||
|
private static final String USER_CLAIM = "usr";
|
||||||
|
private static final String EXAM_ID_CLAIM = "exam";
|
||||||
|
|
||||||
|
private static final String EXAM_OTT_SUBJECT_PREFIX = "EXAM_OTT_SUBJECT_";
|
||||||
|
|
||||||
|
private final UserDAO userDAO;
|
||||||
|
private final ScreenProctoringService screenProctoringService;
|
||||||
|
private final ExamDAO examDAO;
|
||||||
|
private final Cryptor cryptor;
|
||||||
|
private final AdditionalAttributesDAO additionalAttributesDAO;
|
||||||
|
final TokenEndpoint tokenEndpoint;
|
||||||
|
private final AdminAPIClientDetails adminAPIClientDetails;
|
||||||
|
|
||||||
|
public TeacherAccountServiceImpl(
|
||||||
|
final UserDAO userDAO,
|
||||||
|
final ScreenProctoringService screenProctoringService,
|
||||||
|
final ExamDAO examDAO,
|
||||||
|
final Cryptor cryptor,
|
||||||
|
final AdditionalAttributesDAO additionalAttributesDAO,
|
||||||
|
final TokenEndpoint tokenEndpoint,
|
||||||
|
final AdminAPIClientDetails adminAPIClientDetails) {
|
||||||
|
|
||||||
|
this.userDAO = userDAO;
|
||||||
|
this.screenProctoringService = screenProctoringService;
|
||||||
|
this.examDAO = examDAO;
|
||||||
|
this.cryptor = cryptor;
|
||||||
|
this.additionalAttributesDAO = additionalAttributesDAO;
|
||||||
|
this.tokenEndpoint = tokenEndpoint;
|
||||||
|
this.adminAPIClientDetails = adminAPIClientDetails;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Result<UserInfo> createNewTeacherAccountForExam(
|
||||||
|
final Exam exam,
|
||||||
|
final String userId,
|
||||||
|
final String username,
|
||||||
|
final String timezone) {
|
||||||
|
|
||||||
|
return Result.tryCatch(() -> {
|
||||||
|
|
||||||
|
final String uuid = UUID.randomUUID().toString();
|
||||||
|
DateTimeZone dtz = DateTimeZone.UTC;
|
||||||
|
if (StringUtils.isNotBlank(timezone)) {
|
||||||
|
try {
|
||||||
|
dtz = DateTimeZone.forID(timezone);
|
||||||
|
} catch (final Exception e) {
|
||||||
|
log.warn("Failed to set requested time zone for ad-hoc teacher account: {}", timezone);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
final UserMod adHocTeacherUser = new UserMod(
|
||||||
|
uuid,
|
||||||
|
exam.institutionId,
|
||||||
|
userId,
|
||||||
|
getTeacherAccountIdentifier(exam),
|
||||||
|
username,
|
||||||
|
uuid,
|
||||||
|
uuid,
|
||||||
|
null,
|
||||||
|
Locale.ENGLISH,
|
||||||
|
dtz,
|
||||||
|
true,
|
||||||
|
false,
|
||||||
|
Utils.immutableSetOf(UserRole.TEACHER.name()));
|
||||||
|
|
||||||
|
return userDAO.createNew(adHocTeacherUser)
|
||||||
|
.flatMap(account -> userDAO.setActive(account, true))
|
||||||
|
.getOrThrow();
|
||||||
|
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Result<Exam> deleteTeacherAccountsForExam(final Exam exam) {
|
||||||
|
return Result.tryCatch(() -> {
|
||||||
|
|
||||||
|
final String externalId = exam.externalId;
|
||||||
|
final FilterMap filter = new FilterMap();
|
||||||
|
filter.putIfAbsent(Domain.USER.ATTR_SURNAME, getTeacherAccountIdentifier(exam));
|
||||||
|
final Collection<UserInfo> accounts = userDAO.allMatching(filter).getOrThrow();
|
||||||
|
|
||||||
|
if (accounts.isEmpty()) {
|
||||||
|
return exam;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (accounts.size() > 1) {
|
||||||
|
log.error("Too many accounts found!?... ad-hoc teacher account mapping: {}", externalId);
|
||||||
|
return exam;
|
||||||
|
}
|
||||||
|
|
||||||
|
userDAO.delete(Utils.immutableSetOf(new EntityKey(
|
||||||
|
accounts.iterator().next().uuid,
|
||||||
|
EntityType.USER)))
|
||||||
|
.getOrThrow();
|
||||||
|
|
||||||
|
return exam;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Result<String> getOneTimeTokenForTeacherAccount(
|
||||||
|
final Exam exam,
|
||||||
|
final String userId,
|
||||||
|
final String username,
|
||||||
|
final String timezone,
|
||||||
|
final boolean createIfNotExists) {
|
||||||
|
|
||||||
|
return this.userDAO
|
||||||
|
.byModelId(userId)
|
||||||
|
.onErrorDo(error -> handleAccountDoesNotExistYet(createIfNotExists, exam, userId, username, timezone))
|
||||||
|
.map(account -> applySupporter(account, exam))
|
||||||
|
.map(account -> {
|
||||||
|
this.screenProctoringService.synchronizeSPSUserForExam(exam.id);
|
||||||
|
return account;
|
||||||
|
})
|
||||||
|
.map(account -> this.createOneTimeToken(account, exam.id));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Result<TokenLoginInfo> verifyOneTimeTokenForTeacherAccount(final String loginToken) {
|
||||||
|
return Result.tryCatch(() -> {
|
||||||
|
|
||||||
|
final Claims claims = checkJWTValid(loginToken);
|
||||||
|
final String userId = claims.get(USER_CLAIM, String.class);
|
||||||
|
|
||||||
|
// check if requested user exists
|
||||||
|
final UserInfo user = this.userDAO
|
||||||
|
.byModelId(userId)
|
||||||
|
.getOrThrow(error -> new BadCredentialsException("Unknown user claim", error));
|
||||||
|
|
||||||
|
// login the user by getting access token
|
||||||
|
final Map<String, String> params = new HashMap<>();
|
||||||
|
params.put("grant_type", "password");
|
||||||
|
params.put("username", user.username);
|
||||||
|
params.put("password", user.uuid);
|
||||||
|
//final WebAuthenticationDetails details = new WebAuthenticationDetails("localhost", null);
|
||||||
|
final UsernamePasswordAuthenticationToken usernamePasswordAuthenticationToken =
|
||||||
|
new UsernamePasswordAuthenticationToken(
|
||||||
|
this.adminAPIClientDetails, // TODO are this the correct details?
|
||||||
|
null,
|
||||||
|
Collections.emptyList());
|
||||||
|
final ResponseEntity<OAuth2AccessToken> accessToken =
|
||||||
|
this.tokenEndpoint.postAccessToken(usernamePasswordAuthenticationToken, params);
|
||||||
|
final OAuth2AccessToken token = accessToken.getBody();
|
||||||
|
|
||||||
|
return new TokenLoginInfo(user.username, user.uuid, null, token);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private UserInfo handleAccountDoesNotExistYet(
|
||||||
|
final boolean createIfNotExists,
|
||||||
|
final Exam exam,
|
||||||
|
final String userId,
|
||||||
|
final String username,
|
||||||
|
final String timezone) {
|
||||||
|
|
||||||
|
if (createIfNotExists) {
|
||||||
|
return this
|
||||||
|
.createNewTeacherAccountForExam(exam, userId, username, timezone)
|
||||||
|
.getOrThrow();
|
||||||
|
} else {
|
||||||
|
throw new RuntimeException("Teacher Account with userId "+ userId + " and username "+username+" does not exist.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private UserInfo applySupporter(final UserInfo account, final Exam exam) {
|
||||||
|
if (!exam.supporter.contains(account.uuid)) {
|
||||||
|
this.examDAO.applySupporter(exam, account.uuid)
|
||||||
|
.onError(error -> log.error(
|
||||||
|
"Failed to apply ad-hoc-teacher account to supporter list of exam: {} user: {}",
|
||||||
|
exam, account, error));
|
||||||
|
}
|
||||||
|
return account;
|
||||||
|
}
|
||||||
|
|
||||||
|
private String createOneTimeToken(final UserInfo account, final Long examId) {
|
||||||
|
|
||||||
|
// create a subject claim for this token only
|
||||||
|
final String subjectClaim = UUID.randomUUID().toString();
|
||||||
|
this.storeSubjectForExam(examId, account.uuid, subjectClaim);
|
||||||
|
|
||||||
|
final Map<String, Object> claims = new HashMap<>();
|
||||||
|
claims.put(USER_CLAIM, account.uuid);
|
||||||
|
claims.put(EXAM_ID_CLAIM, String.valueOf(examId));
|
||||||
|
|
||||||
|
return createToken(claims, subjectClaim);
|
||||||
|
}
|
||||||
|
|
||||||
|
// NOTE Token is expired in 30 seconds and is signed with internal secret
|
||||||
|
private String createToken(final Map<String, Object> claims, final String subject) {
|
||||||
|
final long millisecondsNow = Utils.getMillisecondsNow();
|
||||||
|
return Jwts.builder()
|
||||||
|
.setClaims(claims)
|
||||||
|
.setSubject(subject)
|
||||||
|
.setIssuedAt(new Date(millisecondsNow))
|
||||||
|
.setExpiration(new Date(millisecondsNow + 30 * Constants.SECOND_IN_MILLIS))
|
||||||
|
.signWith(SignatureAlgorithm.HS256, this.cryptor.getInternalPWD().toString())
|
||||||
|
.compact();
|
||||||
|
}
|
||||||
|
|
||||||
|
private Claims checkJWTValid(final String loginToken) {
|
||||||
|
// decode given JWT
|
||||||
|
final Claims claims = Jwts.parser()
|
||||||
|
.setSigningKey(this.cryptor.getInternalPWD().toString())
|
||||||
|
.parseClaimsJws(loginToken)
|
||||||
|
.getBody();
|
||||||
|
|
||||||
|
// check expiration date
|
||||||
|
final long expirationTime = claims.getExpiration().getTime();
|
||||||
|
final long now = Utils.getMillisecondsNow();
|
||||||
|
if (expirationTime < now) {
|
||||||
|
throw new APIMessage.APIMessageException(APIMessage.ErrorMessage.UNAUTHORIZED.of("Token expired"));
|
||||||
|
}
|
||||||
|
|
||||||
|
// check user claim
|
||||||
|
final String userId = claims.get(USER_CLAIM, String.class);
|
||||||
|
if (StringUtils.isBlank(userId)) {
|
||||||
|
throw new APIMessage.APIMessageException(APIMessage.ErrorMessage.UNAUTHORIZED.of("User not found"));
|
||||||
|
}
|
||||||
|
|
||||||
|
// get exam id
|
||||||
|
final String examId = claims.get(EXAM_ID_CLAIM, String.class);
|
||||||
|
if (StringUtils.isBlank(examId)) {
|
||||||
|
throw new APIMessage.APIMessageException(APIMessage.ErrorMessage.UNAUTHORIZED.of("Exam id not found"));
|
||||||
|
}
|
||||||
|
final Long examPK = Long.parseLong(examId);
|
||||||
|
|
||||||
|
// check subject
|
||||||
|
final String subjectClaim = getSubjectForExam(examPK, userId);
|
||||||
|
if (StringUtils.isBlank(subjectClaim)) {
|
||||||
|
throw new APIMessage.APIMessageException(APIMessage.ErrorMessage.UNAUTHORIZED.of("Subject not found"));
|
||||||
|
}
|
||||||
|
final String subject = claims.get(SUBJECT_CLAIM_NAME, String.class);
|
||||||
|
if (!subjectClaim.equals(subject)) {
|
||||||
|
throw new APIMessage.APIMessageException(APIMessage.ErrorMessage.UNAUTHORIZED.of("Token subject mismatch"));
|
||||||
|
}
|
||||||
|
return claims;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void storeSubjectForExam(final Long examId, final String userId, final String subject) {
|
||||||
|
additionalAttributesDAO.saveAdditionalAttribute(
|
||||||
|
EntityType.EXAM,
|
||||||
|
examId,
|
||||||
|
EXAM_OTT_SUBJECT_PREFIX + userId,
|
||||||
|
subject)
|
||||||
|
.getOrThrow();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void deleteSubjectForExam(final Long examId, final String userId) {
|
||||||
|
additionalAttributesDAO.delete(EntityType.EXAM, examId, EXAM_OTT_SUBJECT_PREFIX + userId);
|
||||||
|
}
|
||||||
|
|
||||||
|
private String getSubjectForExam(final Long examId, final String userId) {
|
||||||
|
return additionalAttributesDAO
|
||||||
|
.getAdditionalAttribute(EntityType.EXAM, examId, EXAM_OTT_SUBJECT_PREFIX + userId)
|
||||||
|
.map(AdditionalAttributeRecord::getValue)
|
||||||
|
.onError(error -> log.warn("Failed to get OTT subject from exam: {}", error.getMessage()))
|
||||||
|
.getOrElse(null);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String getTeacherAccountIdentifier(final Exam exam) {
|
||||||
|
return "AdHoc-Teacher-Account-" + exam.id;
|
||||||
|
}
|
||||||
|
}
|
|
@ -228,6 +228,11 @@ public interface ExamDAO extends ActivatableEntityDAO<Exam, Exam>, BulkActionSup
|
||||||
key = "#examId")
|
key = "#examId")
|
||||||
void markUpdate(Long examId);
|
void markUpdate(Long examId);
|
||||||
|
|
||||||
|
@CacheEvict(
|
||||||
|
cacheNames = ExamSessionCacheService.CACHE_NAME_RUNNING_EXAM,
|
||||||
|
key = "#examId")
|
||||||
|
Result<Exam> applySupporter(Exam exam, String userUUID);
|
||||||
|
|
||||||
/** This is used by the internal update process to mark exams for which the LMS related data availability
|
/** This is used by the internal update process to mark exams for which the LMS related data availability
|
||||||
*
|
*
|
||||||
* @param externalQuizId The exams external UUID or quiz id of the exam to mark
|
* @param externalQuizId The exams external UUID or quiz id of the exam to mark
|
||||||
|
@ -238,4 +243,5 @@ public interface ExamDAO extends ActivatableEntityDAO<Exam, Exam>, BulkActionSup
|
||||||
void updateQuitPassword(Exam exam, String quitPassword);
|
void updateQuitPassword(Exam exam, String quitPassword);
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -12,6 +12,7 @@ import static ch.ethz.seb.sebserver.webservice.datalayer.batis.mapper.ExamRecord
|
||||||
import static ch.ethz.seb.sebserver.webservice.datalayer.batis.mapper.ExamRecordDynamicSqlSupport.examRecord;
|
import static ch.ethz.seb.sebserver.webservice.datalayer.batis.mapper.ExamRecordDynamicSqlSupport.examRecord;
|
||||||
import static org.mybatis.dynamic.sql.SqlBuilder.*;
|
import static org.mybatis.dynamic.sql.SqlBuilder.*;
|
||||||
|
|
||||||
|
import java.sql.Array;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.Arrays;
|
import java.util.Arrays;
|
||||||
import java.util.Collection;
|
import java.util.Collection;
|
||||||
|
@ -189,8 +190,7 @@ public class ExamDAOImpl implements ExamDAO {
|
||||||
UpdateDSL.updateWithMapper(
|
UpdateDSL.updateWithMapper(
|
||||||
this.examRecordMapper::update,
|
this.examRecordMapper::update,
|
||||||
ExamRecordDynamicSqlSupport.examRecord)
|
ExamRecordDynamicSqlSupport.examRecord)
|
||||||
.set(ExamRecordDynamicSqlSupport.lastModified)
|
.set(ExamRecordDynamicSqlSupport.lastModified).equalTo(millisecondsNow)
|
||||||
.equalTo(millisecondsNow)
|
|
||||||
.where(ExamRecordDynamicSqlSupport.id, isEqualTo(examId))
|
.where(ExamRecordDynamicSqlSupport.id, isEqualTo(examId))
|
||||||
.build()
|
.build()
|
||||||
.execute();
|
.execute();
|
||||||
|
@ -200,6 +200,30 @@ public class ExamDAOImpl implements ExamDAO {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
@Transactional
|
||||||
|
public Result<Exam> applySupporter(final Exam exam, final String userUUID) {
|
||||||
|
return Result.tryCatch(() -> {
|
||||||
|
|
||||||
|
final long millisecondsNow = Utils.getMillisecondsNow();
|
||||||
|
final List<String> newSupporter = new ArrayList<>(exam.supporter);
|
||||||
|
newSupporter.add(userUUID);
|
||||||
|
|
||||||
|
UpdateDSL.updateWithMapper(
|
||||||
|
this.examRecordMapper::update,
|
||||||
|
ExamRecordDynamicSqlSupport.examRecord)
|
||||||
|
.set(supporter).equalTo(StringUtils.join(newSupporter, Constants.LIST_SEPARATOR_CHAR))
|
||||||
|
.set(ExamRecordDynamicSqlSupport.lastModified).equalTo(millisecondsNow)
|
||||||
|
.where(ExamRecordDynamicSqlSupport.id, isEqualTo(exam.id))
|
||||||
|
.build()
|
||||||
|
.execute();
|
||||||
|
|
||||||
|
return examRecordMapper.selectByPrimaryKey(exam.id);
|
||||||
|
})
|
||||||
|
.flatMap(this::toDomainModel)
|
||||||
|
.onError(TransactionHandler::rollback);
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void markLMSAvailability(final String externalQuizId, final boolean available, final String updateId) {
|
public void markLMSAvailability(final String externalQuizId, final boolean available, final String updateId) {
|
||||||
|
|
||||||
|
|
|
@ -10,7 +10,6 @@ package ch.ethz.seb.sebserver.webservice.servicelayer.lms;
|
||||||
|
|
||||||
import java.io.OutputStream;
|
import java.io.OutputStream;
|
||||||
import java.util.Collection;
|
import java.util.Collection;
|
||||||
import java.util.Map;
|
|
||||||
|
|
||||||
import ch.ethz.seb.sebserver.gbl.model.EntityKey;
|
import ch.ethz.seb.sebserver.gbl.model.EntityKey;
|
||||||
import ch.ethz.seb.sebserver.gbl.model.exam.Exam;
|
import ch.ethz.seb.sebserver.gbl.model.exam.Exam;
|
||||||
|
@ -42,8 +41,7 @@ public interface FullLmsIntegrationService {
|
||||||
String quizId,
|
String quizId,
|
||||||
String examTemplateId,
|
String examTemplateId,
|
||||||
String quitPassword,
|
String quitPassword,
|
||||||
String quitLink,
|
String quitLink);
|
||||||
String timezone);
|
|
||||||
|
|
||||||
Result<EntityKey> deleteExam(
|
Result<EntityKey> deleteExam(
|
||||||
String lmsUUID,
|
String lmsUUID,
|
||||||
|
@ -59,6 +57,14 @@ public interface FullLmsIntegrationService {
|
||||||
String quizId,
|
String quizId,
|
||||||
OutputStream out);
|
OutputStream out);
|
||||||
|
|
||||||
|
Result<String> getOneTimeLoginToken(
|
||||||
|
String lmsUUId,
|
||||||
|
String courseId,
|
||||||
|
String quizId,
|
||||||
|
String userId,
|
||||||
|
String username,
|
||||||
|
String timezone);
|
||||||
|
|
||||||
@JsonIgnoreProperties(ignoreUnknown = true)
|
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||||
final class IntegrationData {
|
final class IntegrationData {
|
||||||
@JsonProperty("id")
|
@JsonProperty("id")
|
||||||
|
|
|
@ -12,15 +12,12 @@ import java.io.OutputStream;
|
||||||
import java.nio.charset.StandardCharsets;
|
import java.nio.charset.StandardCharsets;
|
||||||
import java.util.Arrays;
|
import java.util.Arrays;
|
||||||
import java.util.Collection;
|
import java.util.Collection;
|
||||||
import java.util.Locale;
|
|
||||||
import java.util.UUID;
|
|
||||||
import java.util.function.Function;
|
import java.util.function.Function;
|
||||||
import java.util.stream.Collectors;
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
import ch.ethz.seb.sebserver.ClientHttpRequestFactoryService;
|
import ch.ethz.seb.sebserver.ClientHttpRequestFactoryService;
|
||||||
import ch.ethz.seb.sebserver.gbl.api.API;
|
import ch.ethz.seb.sebserver.gbl.api.API;
|
||||||
import ch.ethz.seb.sebserver.gbl.api.APIMessage;
|
import ch.ethz.seb.sebserver.gbl.api.APIMessage;
|
||||||
import ch.ethz.seb.sebserver.gbl.api.EntityType;
|
|
||||||
import ch.ethz.seb.sebserver.gbl.api.POSTMapper;
|
import ch.ethz.seb.sebserver.gbl.api.POSTMapper;
|
||||||
import ch.ethz.seb.sebserver.gbl.model.Domain;
|
import ch.ethz.seb.sebserver.gbl.model.Domain;
|
||||||
import ch.ethz.seb.sebserver.gbl.model.EntityKey;
|
import ch.ethz.seb.sebserver.gbl.model.EntityKey;
|
||||||
|
@ -29,15 +26,12 @@ import ch.ethz.seb.sebserver.gbl.model.exam.ExamTemplate;
|
||||||
import ch.ethz.seb.sebserver.gbl.model.exam.QuizData;
|
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;
|
||||||
import ch.ethz.seb.sebserver.gbl.model.sebconfig.SEBClientConfig;
|
import ch.ethz.seb.sebserver.gbl.model.sebconfig.SEBClientConfig;
|
||||||
import ch.ethz.seb.sebserver.gbl.model.user.UserInfo;
|
|
||||||
import ch.ethz.seb.sebserver.gbl.model.user.UserMod;
|
|
||||||
import ch.ethz.seb.sebserver.gbl.model.user.UserRole;
|
|
||||||
import ch.ethz.seb.sebserver.gbl.profile.WebServiceProfile;
|
import ch.ethz.seb.sebserver.gbl.profile.WebServiceProfile;
|
||||||
import ch.ethz.seb.sebserver.gbl.util.Result;
|
import ch.ethz.seb.sebserver.gbl.util.Result;
|
||||||
import ch.ethz.seb.sebserver.gbl.util.Utils;
|
|
||||||
import ch.ethz.seb.sebserver.webservice.WebserviceInfo;
|
import ch.ethz.seb.sebserver.webservice.WebserviceInfo;
|
||||||
import ch.ethz.seb.sebserver.webservice.servicelayer.authorization.UserService;
|
import ch.ethz.seb.sebserver.webservice.servicelayer.authorization.UserService;
|
||||||
import ch.ethz.seb.sebserver.webservice.servicelayer.authorization.impl.SEBServerUser;
|
import ch.ethz.seb.sebserver.webservice.servicelayer.authorization.impl.SEBServerUser;
|
||||||
|
import ch.ethz.seb.sebserver.webservice.servicelayer.authorization.impl.TeacherAccountServiceImpl;
|
||||||
import ch.ethz.seb.sebserver.webservice.servicelayer.bulkaction.impl.DeleteExamAction;
|
import ch.ethz.seb.sebserver.webservice.servicelayer.bulkaction.impl.DeleteExamAction;
|
||||||
import ch.ethz.seb.sebserver.webservice.servicelayer.dao.*;
|
import ch.ethz.seb.sebserver.webservice.servicelayer.dao.*;
|
||||||
import ch.ethz.seb.sebserver.webservice.servicelayer.dao.impl.ExamDeletionEvent;
|
import ch.ethz.seb.sebserver.webservice.servicelayer.dao.impl.ExamDeletionEvent;
|
||||||
|
@ -49,9 +43,9 @@ import ch.ethz.seb.sebserver.webservice.servicelayer.lms.LmsAPITemplate;
|
||||||
import ch.ethz.seb.sebserver.webservice.servicelayer.lms.impl.moodle.MoodleUtils;
|
import ch.ethz.seb.sebserver.webservice.servicelayer.lms.impl.moodle.MoodleUtils;
|
||||||
import ch.ethz.seb.sebserver.webservice.servicelayer.sebconfig.ClientConfigService;
|
import ch.ethz.seb.sebserver.webservice.servicelayer.sebconfig.ClientConfigService;
|
||||||
import ch.ethz.seb.sebserver.webservice.servicelayer.session.ExamSessionService;
|
import ch.ethz.seb.sebserver.webservice.servicelayer.session.ExamSessionService;
|
||||||
|
import ch.ethz.seb.sebserver.webservice.servicelayer.session.ScreenProctoringService;
|
||||||
import org.apache.commons.lang3.StringUtils;
|
import org.apache.commons.lang3.StringUtils;
|
||||||
import org.joda.time.DateTime;
|
import org.joda.time.DateTime;
|
||||||
import org.joda.time.DateTimeZone;
|
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
import org.springframework.beans.factory.annotation.Value;
|
import org.springframework.beans.factory.annotation.Value;
|
||||||
|
@ -70,7 +64,7 @@ public class FullLmsIntegrationServiceImpl implements FullLmsIntegrationService
|
||||||
|
|
||||||
private final LmsSetupDAO lmsSetupDAO;
|
private final LmsSetupDAO lmsSetupDAO;
|
||||||
private final UserActivityLogDAO userActivityLogDAO;
|
private final UserActivityLogDAO userActivityLogDAO;
|
||||||
private final UserDAO userDAO;
|
private final TeacherAccountServiceImpl teacherAccountServiceImpl;
|
||||||
private final SEBClientConfigDAO sebClientConfigDAO;
|
private final SEBClientConfigDAO sebClientConfigDAO;
|
||||||
private final ClientConfigService clientConfigService;
|
private final ClientConfigService clientConfigService;
|
||||||
private final DeleteExamAction deleteExamAction;
|
private final DeleteExamAction deleteExamAction;
|
||||||
|
@ -90,6 +84,7 @@ public class FullLmsIntegrationServiceImpl implements FullLmsIntegrationService
|
||||||
final UserActivityLogDAO userActivityLogDAO,
|
final UserActivityLogDAO userActivityLogDAO,
|
||||||
final UserDAO userDAO,
|
final UserDAO userDAO,
|
||||||
final SEBClientConfigDAO sebClientConfigDAO,
|
final SEBClientConfigDAO sebClientConfigDAO,
|
||||||
|
final ScreenProctoringService screenProctoringService,
|
||||||
final ClientConfigService clientConfigService,
|
final ClientConfigService clientConfigService,
|
||||||
final DeleteExamAction deleteExamAction,
|
final DeleteExamAction deleteExamAction,
|
||||||
final LmsAPIService lmsAPIService,
|
final LmsAPIService lmsAPIService,
|
||||||
|
@ -100,13 +95,14 @@ public class FullLmsIntegrationServiceImpl implements FullLmsIntegrationService
|
||||||
final WebserviceInfo webserviceInfo,
|
final WebserviceInfo webserviceInfo,
|
||||||
final ClientHttpRequestFactoryService clientHttpRequestFactoryService,
|
final ClientHttpRequestFactoryService clientHttpRequestFactoryService,
|
||||||
final UserService userService,
|
final UserService userService,
|
||||||
|
final TeacherAccountServiceImpl teacherAccountServiceImpl,
|
||||||
@Value("${sebserver.webservice.lms.api.endpoint}") final String lmsAPIEndpoint,
|
@Value("${sebserver.webservice.lms.api.endpoint}") final String lmsAPIEndpoint,
|
||||||
@Value("${sebserver.webservice.lms.api.clientId}") final String clientId,
|
@Value("${sebserver.webservice.lms.api.clientId}") final String clientId,
|
||||||
@Value("${sebserver.webservice.api.admin.clientSecret}") final String clientSecret) {
|
@Value("${sebserver.webservice.api.admin.clientSecret}") final String clientSecret) {
|
||||||
|
|
||||||
this.lmsSetupDAO = lmsSetupDAO;
|
this.lmsSetupDAO = lmsSetupDAO;
|
||||||
this.userActivityLogDAO = userActivityLogDAO;
|
this.userActivityLogDAO = userActivityLogDAO;
|
||||||
this.userDAO = userDAO;
|
this.teacherAccountServiceImpl = teacherAccountServiceImpl;
|
||||||
this.sebClientConfigDAO = sebClientConfigDAO;
|
this.sebClientConfigDAO = sebClientConfigDAO;
|
||||||
this.clientConfigService = clientConfigService;
|
this.clientConfigService = clientConfigService;
|
||||||
this.deleteExamAction = deleteExamAction;
|
this.deleteExamAction = deleteExamAction;
|
||||||
|
@ -139,7 +135,11 @@ public class FullLmsIntegrationServiceImpl implements FullLmsIntegrationService
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void notifyExamDeletion(final ExamDeletionEvent event) {
|
public void notifyExamDeletion(final ExamDeletionEvent event) {
|
||||||
event.ids.forEach(this::deleteAdHocAccount);
|
event.ids.forEach( examId -> {
|
||||||
|
this.examDAO.byPK(examId)
|
||||||
|
.map(this.teacherAccountServiceImpl::deleteTeacherAccountsForExam)
|
||||||
|
.onError(error -> log.warn("Failed delete teacher accounts for exam: {}", examId));
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@ -255,14 +255,13 @@ public class FullLmsIntegrationServiceImpl implements FullLmsIntegrationService
|
||||||
final String quizId,
|
final String quizId,
|
||||||
final String examTemplateId,
|
final String examTemplateId,
|
||||||
final String quitPassword,
|
final String quitPassword,
|
||||||
final String quitLink,
|
final String quitLink) {
|
||||||
final String timezone) {
|
|
||||||
|
|
||||||
return lmsSetupDAO
|
return lmsSetupDAO
|
||||||
.getLmsSetupIdByConnectionId(lmsUUID)
|
.getLmsSetupIdByConnectionId(lmsUUID)
|
||||||
.flatMap(lmsAPIService::getLmsAPITemplate)
|
.flatMap(lmsAPIService::getLmsAPITemplate)
|
||||||
.map(findQuizData(courseId, quizId))
|
.map(findQuizData(courseId, quizId))
|
||||||
.map(createAccountAndExam(examTemplateId, quitPassword, timezone));
|
.map(createExam(examTemplateId, quitPassword));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@ -278,7 +277,7 @@ public class FullLmsIntegrationServiceImpl implements FullLmsIntegrationService
|
||||||
.flatMap(this::findExam)
|
.flatMap(this::findExam)
|
||||||
.map(this::checkDeletion)
|
.map(this::checkDeletion)
|
||||||
.map(this::logExamDeleted)
|
.map(this::logExamDeleted)
|
||||||
.map(this::deleteAdHocAccount)
|
.flatMap(teacherAccountServiceImpl::deleteTeacherAccountsForExam)
|
||||||
.flatMap(deleteExamAction::deleteExamFromLMSIntegration);
|
.flatMap(deleteExamAction::deleteExamFromLMSIntegration);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -341,6 +340,25 @@ public class FullLmsIntegrationServiceImpl implements FullLmsIntegrationService
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Result<String> getOneTimeLoginToken(
|
||||||
|
final String lmsUUID,
|
||||||
|
final String courseId,
|
||||||
|
final String quizId,
|
||||||
|
final String userId,
|
||||||
|
final String username,
|
||||||
|
final String timezone) {
|
||||||
|
|
||||||
|
return lmsSetupDAO
|
||||||
|
.getLmsSetupIdByConnectionId(lmsUUID)
|
||||||
|
.flatMap(lmsAPIService::getLmsAPITemplate)
|
||||||
|
.map(findQuizData(courseId, quizId))
|
||||||
|
.flatMap(this::findExam)
|
||||||
|
.flatMap(exam -> this.teacherAccountServiceImpl
|
||||||
|
.getOneTimeTokenForTeacherAccount(exam, userId, username, timezone, true));
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
private Function<LmsAPITemplate, QuizData> findQuizData(
|
private Function<LmsAPITemplate, QuizData> findQuizData(
|
||||||
final String courseId,
|
final String courseId,
|
||||||
final String quizId) {
|
final String quizId) {
|
||||||
|
@ -375,10 +393,9 @@ public class FullLmsIntegrationServiceImpl implements FullLmsIntegrationService
|
||||||
return examDAO.byExternalIdLike(quizData.id);
|
return examDAO.byExternalIdLike(quizData.id);
|
||||||
}
|
}
|
||||||
|
|
||||||
private Function<QuizData, Exam> createAccountAndExam(
|
private Function<QuizData, Exam> createExam(
|
||||||
final String examTemplateId,
|
final String examTemplateId,
|
||||||
final String quitPassword,
|
final String quitPassword) {
|
||||||
final String timezone) {
|
|
||||||
|
|
||||||
return quizData -> {
|
return quizData -> {
|
||||||
|
|
||||||
|
@ -398,14 +415,6 @@ public class FullLmsIntegrationServiceImpl implements FullLmsIntegrationService
|
||||||
post.putIfAbsent(Domain.EXAM.ATTR_QUIT_PASSWORD, quitPassword);
|
post.putIfAbsent(Domain.EXAM.ATTR_QUIT_PASSWORD, quitPassword);
|
||||||
}
|
}
|
||||||
|
|
||||||
final String accountUUID = createAdHocSupporterAccount(quizData, timezone);
|
|
||||||
if (accountUUID != null) {
|
|
||||||
post.putIfAbsent(Domain.EXAM.ATTR_OWNER, accountUUID);
|
|
||||||
// TODO do we need to apply the ad-hoc teacher account also as supporter?
|
|
||||||
} else {
|
|
||||||
post.putIfAbsent(Domain.EXAM.ATTR_OWNER, userService.getCurrentUser().uuid());
|
|
||||||
}
|
|
||||||
|
|
||||||
final Exam exam = new Exam(null, quizData, post);
|
final Exam exam = new Exam(null, quizData, post);
|
||||||
return examDAO
|
return examDAO
|
||||||
.createNew(exam)
|
.createNew(exam)
|
||||||
|
@ -415,83 +424,82 @@ public class FullLmsIntegrationServiceImpl implements FullLmsIntegrationService
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
private String createAdHocSupporterAccount(final QuizData data, final String timezone) {
|
// private UserInfo createNewAdHocAccount(
|
||||||
try {
|
// final Exam exam,
|
||||||
|
// final String userId,
|
||||||
|
// final String username,
|
||||||
|
// final String timezone) {
|
||||||
|
// try {
|
||||||
|
//
|
||||||
|
// final String uuid = UUID.randomUUID().toString();
|
||||||
|
// DateTimeZone dtz = DateTimeZone.UTC;
|
||||||
|
// if (StringUtils.isNotBlank(timezone)) {
|
||||||
|
// try {
|
||||||
|
// dtz = DateTimeZone.forID(timezone);
|
||||||
|
// } catch (final Exception e) {
|
||||||
|
// log.warn("Failed to set requested time zone for ad-hoc teacher account: {}", timezone);
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// final UserMod adHocTeacherUser = new UserMod(
|
||||||
|
// uuid,
|
||||||
|
// exam.institutionId,
|
||||||
|
// userId,
|
||||||
|
// getTeacherAccountIdentifier(exam),
|
||||||
|
// username,
|
||||||
|
// uuid,
|
||||||
|
// uuid,
|
||||||
|
// null,
|
||||||
|
// Locale.ENGLISH,
|
||||||
|
// dtz,
|
||||||
|
// true,
|
||||||
|
// false,
|
||||||
|
// Utils.immutableSetOf(UserRole.TEACHER.name()));
|
||||||
|
//
|
||||||
|
// return userDAO.createNew(adHocTeacherUser)
|
||||||
|
// .flatMap(account -> userDAO.setActive(account, true))
|
||||||
|
// .getOrThrow();
|
||||||
|
//
|
||||||
|
// } catch (final Exception e) {
|
||||||
|
// log.error("Failed to create ad-hoc teacher account for importing exam: {}", exam, e);
|
||||||
|
// return null;
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
|
||||||
final String uuid = UUID.randomUUID().toString();
|
|
||||||
final String name = "teacher-" + uuid;
|
|
||||||
DateTimeZone dtz = DateTimeZone.UTC;
|
|
||||||
if (StringUtils.isNotBlank(timezone)) {
|
|
||||||
try {
|
|
||||||
dtz = DateTimeZone.forID(timezone);
|
|
||||||
} catch (final Exception e) {
|
|
||||||
log.warn("Failed to set requested time zone for ad-hoc teacher account: {}", timezone);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
final UserMod adHocTeacherUser = new UserMod(
|
|
||||||
uuid,
|
|
||||||
data.institutionId,
|
|
||||||
name,
|
|
||||||
data.id,
|
|
||||||
name,
|
|
||||||
uuid,
|
|
||||||
uuid,
|
|
||||||
null,
|
|
||||||
Locale.ENGLISH,
|
|
||||||
dtz,
|
|
||||||
true,
|
|
||||||
false,
|
|
||||||
Utils.immutableSetOf(UserRole.TEACHER.name()));
|
|
||||||
|
|
||||||
userDAO.createNew(adHocTeacherUser)
|
// private Exam deleteAdHocAccounts(final Exam exam) {
|
||||||
.flatMap(account -> userDAO.setActive(account, true))
|
// try {
|
||||||
.getOrThrow();
|
//
|
||||||
|
// final String externalId = exam.externalId;
|
||||||
|
// final FilterMap filter = new FilterMap();
|
||||||
|
// filter.putIfAbsent(Domain.USER.ATTR_SURNAME, getTeacherAccountIdentifier(exam));
|
||||||
|
// final Collection<UserInfo> accounts = userDAO.allMatching(filter).getOrThrow();
|
||||||
|
//
|
||||||
|
// if (accounts.isEmpty()) {
|
||||||
|
// return exam;
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// if (accounts.size() > 1) {
|
||||||
|
// log.error("Too many accounts found!?... ad-hoc teacher account mapping: {}", externalId);
|
||||||
|
// return exam;
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// userDAO.delete(Utils.immutableSetOf(new EntityKey(
|
||||||
|
// accounts.iterator().next().uuid,
|
||||||
|
// EntityType.USER)))
|
||||||
|
// .getOrThrow();
|
||||||
|
//
|
||||||
|
// } catch (final Exception e) {
|
||||||
|
// log.error("Failed to delete ad-hoc account for exam: {}", exam, e);
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// return exam;
|
||||||
|
// }
|
||||||
|
|
||||||
return uuid;
|
|
||||||
} catch (final Exception e) {
|
|
||||||
log.error("Failed to create ad-hoc teacher account for importing exam: {}", data, e);
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private Exam deleteAdHocAccount(final Exam exam) {
|
|
||||||
deleteAdHocAccount(exam.id);
|
|
||||||
return exam;
|
|
||||||
}
|
|
||||||
|
|
||||||
private void deleteAdHocAccount(final Long examId) {
|
|
||||||
try {
|
|
||||||
|
|
||||||
final Result<Exam> examResult = examDAO.byPK(examId);
|
|
||||||
if (examResult.hasError()) {
|
|
||||||
log.warn("Failed to get exam for id: {}", examId);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
final String externalId = examResult.get().externalId;
|
|
||||||
final FilterMap filter = new FilterMap();
|
|
||||||
filter.putIfAbsent(Domain.USER.ATTR_SURNAME, externalId);
|
|
||||||
final Collection<UserInfo> accounts = userDAO.allMatching(filter).getOrThrow();
|
|
||||||
|
|
||||||
if (accounts.isEmpty()) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (accounts.size() > 1) {
|
|
||||||
log.error("Too many accounts found!?... ad-hoc teacher account mapping: {}", externalId);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
userDAO.delete(Utils.immutableSetOf(new EntityKey(
|
|
||||||
accounts.iterator().next().uuid,
|
|
||||||
EntityType.USER)))
|
|
||||||
.getOrThrow();
|
|
||||||
|
|
||||||
} catch (final Exception e) {
|
|
||||||
log.error("Failed to delete ad-hoc account for exam: {}", examId, e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private Collection<ExamTemplateSelection> getIntegrationTemplates(final Long institutionId) {
|
private Collection<ExamTemplateSelection> getIntegrationTemplates(final Long institutionId) {
|
||||||
return examTemplateDAO
|
return examTemplateDAO
|
||||||
|
|
|
@ -91,6 +91,9 @@ public interface ScreenProctoringService extends SessionUpdateTask {
|
||||||
@Async(AsyncServiceSpringConfig.EXECUTOR_BEAN_NAME)
|
@Async(AsyncServiceSpringConfig.EXECUTOR_BEAN_NAME)
|
||||||
void synchronizeSPSUser(final String userUUID);
|
void synchronizeSPSUser(final String userUUID);
|
||||||
|
|
||||||
|
@Async(AsyncServiceSpringConfig.EXECUTOR_BEAN_NAME)
|
||||||
|
void synchronizeSPSUserForExam(final Long examId);
|
||||||
|
|
||||||
@Async(AsyncServiceSpringConfig.EXECUTOR_BEAN_NAME)
|
@Async(AsyncServiceSpringConfig.EXECUTOR_BEAN_NAME)
|
||||||
void deleteSPSUser(String userUUID);
|
void deleteSPSUser(String userUUID);
|
||||||
|
|
||||||
|
|
|
@ -1004,7 +1004,7 @@ class ScreenProctoringAPIBinding {
|
||||||
10 * Constants.SECOND_IN_MILLIS,
|
10 * Constants.SECOND_IN_MILLIS,
|
||||||
10 * Constants.SECOND_IN_MILLIS);
|
10 * Constants.SECOND_IN_MILLIS);
|
||||||
|
|
||||||
ClientCredentials clientCredentials = new ClientCredentials(
|
final ClientCredentials clientCredentials = new ClientCredentials(
|
||||||
spsAPIAccessData.getSpsAPIKey(),
|
spsAPIAccessData.getSpsAPIKey(),
|
||||||
spsAPIAccessData.getSpsAPISecret());
|
spsAPIAccessData.getSpsAPISecret());
|
||||||
|
|
||||||
|
|
|
@ -268,6 +268,13 @@ public class ScreenProctoringServiceImpl implements ScreenProctoringService {
|
||||||
this.screenProctoringAPIBinding.synchronizeUserAccount(userUUID);
|
this.screenProctoringAPIBinding.synchronizeUserAccount(userUUID);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void synchronizeSPSUserForExam(final Long examId) {
|
||||||
|
this.examDAO.byPK(examId)
|
||||||
|
.onSuccess(this.screenProctoringAPIBinding::synchronizeUserAccounts)
|
||||||
|
.onError(error -> log.error("Failed to synchronize SPS user accounts for exam: {}", examId, error));
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@Async(AsyncServiceSpringConfig.EXECUTOR_BEAN_NAME)
|
@Async(AsyncServiceSpringConfig.EXECUTOR_BEAN_NAME)
|
||||||
public void deleteSPSUser(final String userUUID) {
|
public void deleteSPSUser(final String userUUID) {
|
||||||
|
|
|
@ -9,7 +9,6 @@
|
||||||
package ch.ethz.seb.sebserver.webservice.weblayer.api;
|
package ch.ethz.seb.sebserver.webservice.weblayer.api;
|
||||||
|
|
||||||
import javax.servlet.ServletOutputStream;
|
import javax.servlet.ServletOutputStream;
|
||||||
import javax.servlet.http.HttpServletRequest;
|
|
||||||
import javax.servlet.http.HttpServletResponse;
|
import javax.servlet.http.HttpServletResponse;
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
|
@ -18,11 +17,9 @@ import java.io.PipedOutputStream;
|
||||||
|
|
||||||
import ch.ethz.seb.sebserver.gbl.api.API;
|
import ch.ethz.seb.sebserver.gbl.api.API;
|
||||||
import ch.ethz.seb.sebserver.gbl.api.APIMessage;
|
import ch.ethz.seb.sebserver.gbl.api.APIMessage;
|
||||||
import ch.ethz.seb.sebserver.gbl.model.Entity;
|
|
||||||
import ch.ethz.seb.sebserver.gbl.model.EntityKey;
|
import ch.ethz.seb.sebserver.gbl.model.EntityKey;
|
||||||
import ch.ethz.seb.sebserver.gbl.model.exam.Exam;
|
import ch.ethz.seb.sebserver.gbl.model.exam.Exam;
|
||||||
import ch.ethz.seb.sebserver.gbl.profile.WebServiceProfile;
|
import ch.ethz.seb.sebserver.gbl.profile.WebServiceProfile;
|
||||||
import ch.ethz.seb.sebserver.gbl.util.Result;
|
|
||||||
import ch.ethz.seb.sebserver.webservice.servicelayer.lms.FullLmsIntegrationService;
|
import ch.ethz.seb.sebserver.webservice.servicelayer.lms.FullLmsIntegrationService;
|
||||||
import org.apache.commons.io.IOUtils;
|
import org.apache.commons.io.IOUtils;
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
|
@ -58,7 +55,6 @@ public class LmsIntegrationController {
|
||||||
@RequestParam(name = API.LMS_FULL_INTEGRATION_EXAM_TEMPLATE_ID, required = true) final String templateId,
|
@RequestParam(name = API.LMS_FULL_INTEGRATION_EXAM_TEMPLATE_ID, required = true) final String templateId,
|
||||||
@RequestParam(name = API.LMS_FULL_INTEGRATION_QUIT_PASSWORD, required = false) final String quitPassword,
|
@RequestParam(name = API.LMS_FULL_INTEGRATION_QUIT_PASSWORD, required = false) final String quitPassword,
|
||||||
@RequestParam(name = API.LMS_FULL_INTEGRATION_QUIT_LINK, required = false) final String quitLink,
|
@RequestParam(name = API.LMS_FULL_INTEGRATION_QUIT_LINK, required = false) final String quitLink,
|
||||||
@RequestParam(name = API.LMS_FULL_INTEGRATION_TIME_ZONE, required = false) final String timezone,
|
|
||||||
final HttpServletResponse response) {
|
final HttpServletResponse response) {
|
||||||
|
|
||||||
final Exam exam = fullLmsIntegrationService.importExam(
|
final Exam exam = fullLmsIntegrationService.importExam(
|
||||||
|
@ -67,8 +63,7 @@ public class LmsIntegrationController {
|
||||||
quizId,
|
quizId,
|
||||||
templateId,
|
templateId,
|
||||||
quitPassword,
|
quitPassword,
|
||||||
quitLink,
|
quitLink)
|
||||||
timezone)
|
|
||||||
.onError(e -> log.error(
|
.onError(e -> log.error(
|
||||||
"Failed to create/import exam: lmsId:{}, courseId: {}, quizId: {}, templateId: {}",
|
"Failed to create/import exam: lmsId:{}, courseId: {}, quizId: {}, templateId: {}",
|
||||||
lmsUUId, courseId, quizId, templateId, e))
|
lmsUUId, courseId, quizId, templateId, e))
|
||||||
|
@ -107,33 +102,52 @@ public class LmsIntegrationController {
|
||||||
@RequestParam(name = API.LMS_FULL_INTEGRATION_QUIZ_ID, required = true) final String quizId,
|
@RequestParam(name = API.LMS_FULL_INTEGRATION_QUIZ_ID, required = true) final String quizId,
|
||||||
final HttpServletResponse response) throws IOException {
|
final HttpServletResponse response) throws IOException {
|
||||||
|
|
||||||
final ServletOutputStream outputStream = response.getOutputStream();
|
final ServletOutputStream outputStream = response.getOutputStream();
|
||||||
final PipedOutputStream pout;
|
final PipedOutputStream pout;
|
||||||
final PipedInputStream pin;
|
final PipedInputStream pin;
|
||||||
try {
|
try {
|
||||||
pout = new PipedOutputStream();
|
pout = new PipedOutputStream();
|
||||||
pin = new PipedInputStream(pout);
|
pin = new PipedInputStream(pout);
|
||||||
|
|
||||||
fullLmsIntegrationService
|
fullLmsIntegrationService
|
||||||
.streamConnectionConfiguration(lmsUUId, courseId, quizId, pout)
|
.streamConnectionConfiguration(lmsUUId, courseId, quizId, pout)
|
||||||
.getOrThrow();
|
.getOrThrow();
|
||||||
|
|
||||||
IOUtils.copyLarge(pin, outputStream);
|
IOUtils.copyLarge(pin, outputStream);
|
||||||
|
|
||||||
response.setStatus(HttpStatus.OK.value());
|
response.setStatus(HttpStatus.OK.value());
|
||||||
outputStream.flush();
|
outputStream.flush();
|
||||||
|
|
||||||
} catch (final APIMessage.APIMessageException me) {
|
} catch (final APIMessage.APIMessageException me) {
|
||||||
response.setStatus(HttpStatus.BAD_REQUEST.value());
|
response.setStatus(HttpStatus.BAD_REQUEST.value());
|
||||||
throw me;
|
throw me;
|
||||||
} catch (final Exception e) {
|
} catch (final Exception e) {
|
||||||
log.error(
|
log.error(
|
||||||
"Failed to stream connection configuration for exam: lmsId:{}, courseId: {}, quizId: {}",
|
"Failed to stream connection configuration for exam: lmsId:{}, courseId: {}, quizId: {}",
|
||||||
lmsUUId, courseId, quizId, e);
|
lmsUUId, courseId, quizId, e);
|
||||||
response.setStatus(HttpStatus.INTERNAL_SERVER_ERROR.value());
|
response.setStatus(HttpStatus.INTERNAL_SERVER_ERROR.value());
|
||||||
} finally {
|
} finally {
|
||||||
outputStream.flush();
|
outputStream.flush();
|
||||||
outputStream.close();
|
outputStream.close();
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@RequestMapping(
|
||||||
|
path = API.LMS_FULL_INTEGRATION_LOGIN_TOKEN_ENDPOINT,
|
||||||
|
method = RequestMethod.POST,
|
||||||
|
consumes = MediaType.APPLICATION_FORM_URLENCODED_VALUE,
|
||||||
|
produces = MediaType.APPLICATION_OCTET_STREAM_VALUE)
|
||||||
|
public String getOneTimeLoginToken(
|
||||||
|
@RequestParam(name = API.LMS_FULL_INTEGRATION_LMS_UUID, required = true) final String lmsUUId,
|
||||||
|
@RequestParam(name = API.LMS_FULL_INTEGRATION_COURSE_ID, required = true) final String courseId,
|
||||||
|
@RequestParam(name = API.LMS_FULL_INTEGRATION_QUIZ_ID, required = true) final String quizId,
|
||||||
|
@RequestParam(name = API.LMS_FULL_INTEGRATION_USER_ID, required = true) final String userId,
|
||||||
|
@RequestParam(name = API.LMS_FULL_INTEGRATION_USER_NAME, required = false) final String username,
|
||||||
|
@RequestParam(name = API.LMS_FULL_INTEGRATION_TIME_ZONE, required = false) final String timezone,
|
||||||
|
final HttpServletResponse response) throws IOException {
|
||||||
|
|
||||||
|
return this.fullLmsIntegrationService
|
||||||
|
.getOneTimeLoginToken(lmsUUId, courseId, quizId, userId, username, timezone)
|
||||||
|
.getOrThrow();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue