fixed connection config activation and and deleting and improved

OAuth2 error handling for exam API
This commit is contained in:
anhefti 2022-06-21 15:33:46 +02:00
parent 0145658e71
commit 4ab317d763
9 changed files with 86 additions and 63 deletions

View file

@ -42,7 +42,7 @@ public interface ActivatableEntityDAO<T extends Entity, M extends ModelIdAware>
Result<Collection<EntityKey>> setActive(Set<EntityKey> all, boolean active); Result<Collection<EntityKey>> setActive(Set<EntityKey> all, boolean active);
default Result<T> setActive(final T entity, final boolean active) { default Result<T> setActive(final T entity, final boolean active) {
return setActive(new HashSet<>(Arrays.asList(entity.getEntityKey())), true) return setActive(new HashSet<>(Arrays.asList(entity.getEntityKey())), active)
.flatMap(result -> byModelId(result.iterator().next().modelId)); .flatMap(result -> byModelId(result.iterator().next().modelId));
} }

View file

@ -140,6 +140,7 @@ public interface ClientConnectionDAO extends
* @return Result refer to a collection of deleted entities or to an error if happened */ * @return Result refer to a collection of deleted entities or to an error if happened */
@Override @Override
@CacheEvict(cacheNames = CONNECTION_TOKENS_CACHE, allEntries = true) @CacheEvict(cacheNames = CONNECTION_TOKENS_CACHE, allEntries = true)
// TODO this probably is nor working when called from BulkActionSupportDAO
Result<Collection<EntityKey>> delete(Set<EntityKey> all); Result<Collection<EntityKey>> delete(Set<EntityKey> all);
/** Get a ClientConnection by connection token. /** Get a ClientConnection by connection token.

View file

@ -8,17 +8,10 @@
package ch.ethz.seb.sebserver.webservice.servicelayer.dao; package ch.ethz.seb.sebserver.webservice.servicelayer.dao;
import java.util.Collection;
import java.util.Set;
import org.springframework.cache.annotation.CacheEvict;
import ch.ethz.seb.sebserver.gbl.client.ClientCredentials; import ch.ethz.seb.sebserver.gbl.client.ClientCredentials;
import ch.ethz.seb.sebserver.gbl.model.EntityKey;
import ch.ethz.seb.sebserver.gbl.model.sebconfig.SEBClientConfig; import ch.ethz.seb.sebserver.gbl.model.sebconfig.SEBClientConfig;
import ch.ethz.seb.sebserver.gbl.util.Result; 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;
import ch.ethz.seb.sebserver.webservice.servicelayer.sebconfig.ClientConfigService;
/** Concrete EntityDAO interface of SEBClientConfig entities */ /** Concrete EntityDAO interface of SEBClientConfig entities */
public interface SEBClientConfigDAO extends public interface SEBClientConfigDAO extends
@ -54,10 +47,4 @@ public interface SEBClientConfigDAO extends
* @return encrypted configuration password */ * @return encrypted configuration password */
Result<CharSequence> getConfigPasswordCipherByClientName(String clientName); Result<CharSequence> getConfigPasswordCipherByClientName(String clientName);
@Override
@CacheEvict(
cacheNames = ClientConfigService.EXAM_CLIENT_DETAILS_CACHE,
allEntries = true)
Result<Collection<EntityKey>> delete(Set<EntityKey> all);
} }

View file

@ -24,6 +24,8 @@ import org.apache.commons.lang3.BooleanUtils;
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.joda.time.DateTimeZone;
import org.springframework.cache.CacheManager;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.context.annotation.Lazy; import org.springframework.context.annotation.Lazy;
import org.springframework.stereotype.Component; import org.springframework.stereotype.Component;
import org.springframework.transaction.annotation.Transactional; import org.springframework.transaction.annotation.Transactional;
@ -55,6 +57,8 @@ import ch.ethz.seb.sebserver.webservice.servicelayer.dao.FilterMap;
import ch.ethz.seb.sebserver.webservice.servicelayer.dao.ResourceNotFoundException; import ch.ethz.seb.sebserver.webservice.servicelayer.dao.ResourceNotFoundException;
import ch.ethz.seb.sebserver.webservice.servicelayer.dao.SEBClientConfigDAO; import ch.ethz.seb.sebserver.webservice.servicelayer.dao.SEBClientConfigDAO;
import ch.ethz.seb.sebserver.webservice.servicelayer.dao.TransactionHandler; import ch.ethz.seb.sebserver.webservice.servicelayer.dao.TransactionHandler;
import ch.ethz.seb.sebserver.webservice.servicelayer.sebconfig.ClientConfigService;
import ch.ethz.seb.sebserver.webservice.weblayer.oauth.RevokeTokenEndpoint.RevokeExamTokenEvent;
@Lazy @Lazy
@Component @Component
@ -65,17 +69,23 @@ public class SEBClientConfigDAOImpl implements SEBClientConfigDAO {
private final ClientCredentialService clientCredentialService; private final ClientCredentialService clientCredentialService;
private final AdditionalAttributesDAOImpl additionalAttributesDAO; private final AdditionalAttributesDAOImpl additionalAttributesDAO;
private final DAOUserServcie daoUserServcie; private final DAOUserServcie daoUserServcie;
private final ApplicationEventPublisher applicationEventPublisher;
private final CacheManager cacheManager;
protected SEBClientConfigDAOImpl( protected SEBClientConfigDAOImpl(
final SebClientConfigRecordMapper sebClientConfigRecordMapper, final SebClientConfigRecordMapper sebClientConfigRecordMapper,
final ClientCredentialService clientCredentialService, final ClientCredentialService clientCredentialService,
final AdditionalAttributesDAOImpl additionalAttributesDAO, final AdditionalAttributesDAOImpl additionalAttributesDAO,
final DAOUserServcie daoUserServcie) { final DAOUserServcie daoUserServcie,
final ApplicationEventPublisher applicationEventPublisher,
final CacheManager cacheManager) {
this.sebClientConfigRecordMapper = sebClientConfigRecordMapper; this.sebClientConfigRecordMapper = sebClientConfigRecordMapper;
this.clientCredentialService = clientCredentialService; this.clientCredentialService = clientCredentialService;
this.additionalAttributesDAO = additionalAttributesDAO; this.additionalAttributesDAO = additionalAttributesDAO;
this.daoUserServcie = daoUserServcie; this.daoUserServcie = daoUserServcie;
this.applicationEventPublisher = applicationEventPublisher;
this.cacheManager = cacheManager;
} }
@Override @Override
@ -144,6 +154,7 @@ public class SEBClientConfigDAOImpl implements SEBClientConfigDAO {
} }
@Override @Override
@Transactional(readOnly = true)
public Result<SEBClientConfig> byClientName(final String clientName) { public Result<SEBClientConfig> byClientName(final String clientName) {
return Result.tryCatch(() -> this.sebClientConfigRecordMapper return Result.tryCatch(() -> this.sebClientConfigRecordMapper
.selectByExample() .selectByExample()
@ -217,6 +228,7 @@ public class SEBClientConfigDAOImpl implements SEBClientConfigDAO {
return ids.stream() return ids.stream()
.map(id -> new EntityKey(id, EntityType.SEB_CLIENT_CONFIGURATION)) .map(id -> new EntityKey(id, EntityType.SEB_CLIENT_CONFIGURATION))
.map(this::revokeClientConnection)
.collect(Collectors.toList()); .collect(Collectors.toList());
}); });
} }
@ -303,6 +315,7 @@ public class SEBClientConfigDAOImpl implements SEBClientConfigDAO {
return ids.stream() return ids.stream()
.map(id -> new EntityKey(id, EntityType.SEB_CLIENT_CONFIGURATION)) .map(id -> new EntityKey(id, EntityType.SEB_CLIENT_CONFIGURATION))
.map(this::revokeClientConnection)
.collect(Collectors.toList()); .collect(Collectors.toList());
}); });
} }
@ -657,4 +670,28 @@ public class SEBClientConfigDAOImpl implements SEBClientConfigDAO {
} }
} }
private EntityKey revokeClientConnection(final EntityKey key) {
try {
final SebClientConfigRecord rec = recordById(Long.parseLong(key.modelId))
.getOrThrow();
// revoke token
try {
this.applicationEventPublisher
.publishEvent(new RevokeExamTokenEvent(rec.getClientName()));
} catch (final Exception e) {
log.error("Failed to revoke token for SEB client connection. Connection Configuration: {}", key, e);
}
// clear cache
this.cacheManager.getCache(ClientConfigService.EXAM_CLIENT_DETAILS_CACHE)
.evictIfPresent(rec.getClientName());
} catch (final Exception e) {
log.error("Failed to revoke SEB client connection. Connection Configuration: {}", key, e);
}
return key;
}
} }

View file

@ -205,50 +205,6 @@ public abstract class ActivatableEntityController<T extends GrantEntity & Activa
.getOrThrow(); .getOrThrow();
} }
@RequestMapping(
path = API.TOGGLE_ACTIVITY_PATH_SEGMENT,
method = RequestMethod.POST,
consumes = MediaType.APPLICATION_FORM_URLENCODED_VALUE,
produces = MediaType.APPLICATION_JSON_VALUE)
public EntityProcessingReport toggleActivity(
@RequestParam(name = API.PARAM_MODEL_ID_LIST, required = true) final String ids) {
// TODO
throw new UnsupportedOperationException();
// final EntityType entityType = this.entityDAO.entityType();
// final List<EntityKey> entities = new ArrayList<>();
// final Set<ErrorEntry> errors = new HashSet<>();
// final BulkAction bulkAction = new BulkAction(
// (active) ? BulkActionType.ACTIVATE : BulkActionType.DEACTIVATE,
// entityType,
// entities);
//
// Arrays.asList(StringUtils.split(ids, Constants.LIST_SEPARATOR))
// .stream()
// .forEach(modelId -> {
// this.entityDAO
// .byModelId(modelId)
// .flatMap(this.authorization::checkWrite)
// .flatMap(entity -> validForActivation(entity, active))
// .map(Entity::getEntityKey)
// .onSuccess(entities::add)
// .onError(error -> errors.add(new ErrorEntry(
// new EntityKey(modelId, entityType),
// APIMessage.ErrorMessage.UNAUTHORIZED.of(error))));
// });
//
// return this.bulkActionService
// .createReport(bulkAction)
// .map(report -> {
// if (!errors.isEmpty()) {
// errors.addAll(report.errors);
// return new EntityProcessingReport(report.source, report.results, errors, report.bulkActionType);
// } else {
// return report;
// }
// });
}
private Result<EntityProcessingReport> setActiveSingle(final String modelId, final boolean active) { private Result<EntityProcessingReport> setActiveSingle(final String modelId, final boolean active) {
final EntityType entityType = this.entityDAO.entityType(); final EntityType entityType = this.entityDAO.entityType();

View file

@ -8,11 +8,16 @@
package ch.ethz.seb.sebserver.webservice.weblayer.oauth; package ch.ethz.seb.sebserver.webservice.weblayer.oauth;
import javax.servlet.http.HttpServletResponse;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.beans.factory.annotation.Value; import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Configuration;
import org.springframework.core.annotation.Order; import org.springframework.core.annotation.Order;
import org.springframework.http.MediaType;
import org.springframework.security.authentication.AuthenticationManager; import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.oauth2.config.annotation.configurers.ClientDetailsServiceConfigurer; import org.springframework.security.oauth2.config.annotation.configurers.ClientDetailsServiceConfigurer;
@ -26,6 +31,7 @@ import org.springframework.security.oauth2.provider.token.TokenStore;
import org.springframework.security.oauth2.provider.token.store.JwtAccessTokenConverter; import org.springframework.security.oauth2.provider.token.store.JwtAccessTokenConverter;
import ch.ethz.seb.sebserver.WebSecurityConfig; import ch.ethz.seb.sebserver.WebSecurityConfig;
import ch.ethz.seb.sebserver.gbl.Constants;
import ch.ethz.seb.sebserver.gbl.profile.WebServiceProfile; import ch.ethz.seb.sebserver.gbl.profile.WebServiceProfile;
import ch.ethz.seb.sebserver.webservice.weblayer.WebServiceSecurityConfig; import ch.ethz.seb.sebserver.webservice.weblayer.WebServiceSecurityConfig;
import ch.ethz.seb.sebserver.webservice.weblayer.WebServiceUserDetails; import ch.ethz.seb.sebserver.webservice.weblayer.WebServiceUserDetails;
@ -42,6 +48,8 @@ import ch.ethz.seb.sebserver.webservice.weblayer.WebServiceUserDetails;
@Order(100) @Order(100)
public class AuthorizationServerConfig extends AuthorizationServerConfigurerAdapter { public class AuthorizationServerConfig extends AuthorizationServerConfigurerAdapter {
private static final Logger log = LoggerFactory.getLogger(AuthorizationServerConfig.class);
@Autowired @Autowired
private AccessTokenConverter accessTokenConverter; private AccessTokenConverter accessTokenConverter;
@Autowired(required = true) @Autowired(required = true)
@ -66,7 +74,16 @@ public class AuthorizationServerConfig extends AuthorizationServerConfigurerAdap
oauthServer oauthServer
.tokenKeyAccess("permitAll()") .tokenKeyAccess("permitAll()")
.checkTokenAccess("isAuthenticated()") .checkTokenAccess("isAuthenticated()")
.passwordEncoder(this.clientPasswordEncoder); .passwordEncoder(this.clientPasswordEncoder)
.authenticationEntryPoint((request, response, exception) -> {
response.setContentType(MediaType.APPLICATION_JSON_VALUE);
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
log.warn(
"Unauthorized Request: {}, {}",
request,
exception != null ? exception.getMessage() : Constants.EMPTY_NOTE);
response.getOutputStream().println("{ \"error\": \"" + exception.getMessage() + "\" }");
});
} }
@Override @Override

View file

@ -69,6 +69,18 @@ public class RevokeTokenEndpoint {
} }
} }
@EventListener(RevokeExamTokenEvent.class)
void revokeExamAccessToken(final RevokeExamTokenEvent event) {
final Collection<OAuth2AccessToken> tokens = this.tokenStore
.findTokensByClientId(event.clientId);
if (tokens != null) {
for (final OAuth2AccessToken token : tokens) {
this.tokenStore.removeAccessToken(token);
}
}
}
public static final class RevokeTokenEvent extends ApplicationEvent { public static final class RevokeTokenEvent extends ApplicationEvent {
private static final long serialVersionUID = 5776699085388043743L; private static final long serialVersionUID = 5776699085388043743L;
@ -82,4 +94,17 @@ public class RevokeTokenEndpoint {
} }
public static final class RevokeExamTokenEvent extends ApplicationEvent {
private static final long serialVersionUID = 5776699085388043743L;
public final String clientId;
public RevokeExamTokenEvent(final String clientId) {
super(clientId);
this.clientId = clientId;
}
}
} }

View file

@ -11,7 +11,7 @@ package ch.ethz.seb.sebserver.webservice.weblayer.oauth;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import org.springframework.context.annotation.Lazy; import org.springframework.context.annotation.Lazy;
import org.springframework.security.access.AccessDeniedException; import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.security.oauth2.provider.ClientDetails; import org.springframework.security.oauth2.provider.ClientDetails;
import org.springframework.security.oauth2.provider.ClientDetailsService; import org.springframework.security.oauth2.provider.ClientDetailsService;
import org.springframework.security.oauth2.provider.ClientRegistrationException; import org.springframework.security.oauth2.provider.ClientRegistrationException;
@ -67,7 +67,7 @@ public class WebClientDetailsService implements ClientDetailsService {
return getForExamClientAPI(clientId) return getForExamClientAPI(clientId)
.get(t -> { .get(t -> {
log.error("Active ClientConfig not found: {} cause: {}", clientId, t.getMessage()); log.error("Active ClientConfig not found: {} cause: {}", clientId, t.getMessage());
throw new AccessDeniedException(t.getMessage()); throw new UsernameNotFoundException(t.getMessage());
}); });
} }

View file

@ -10,7 +10,7 @@ server.tomcat.uri-encoding=UTF-8
logging.level.ROOT=INFO logging.level.ROOT=INFO
logging.level.ch=INFO logging.level.ch=INFO
logging.level.ch.ethz.seb.sebserver.webservice.datalayer=INFO logging.level.ch.ethz.seb.sebserver.webservice.datalayer=INFO
logging.level.org.springframework.cache=INFO logging.level.org.springframework.cache=DEBUG
logging.level.ch.ethz.seb.sebserver.webservice.servicelayer.lms.impl=DEBUG logging.level.ch.ethz.seb.sebserver.webservice.servicelayer.lms.impl=DEBUG
logging.level.ch.ethz.seb.sebserver.webservice.servicelayer.session=DEBUG logging.level.ch.ethz.seb.sebserver.webservice.servicelayer.session=DEBUG
logging.level.ch.ethz.seb.sebserver.webservice.servicelayer.session.impl.proctoring=INFO logging.level.ch.ethz.seb.sebserver.webservice.servicelayer.session.impl.proctoring=INFO