Skip to content

Commit

Permalink
[Enhancement #482] backend endpoint
Browse files Browse the repository at this point in the history
  • Loading branch information
lukaskabc committed Jul 23, 2024
1 parent c3a2ccb commit b08c4c8
Show file tree
Hide file tree
Showing 16 changed files with 573 additions and 342 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
import cz.cvut.kbss.jopa.model.EntityManager;
import cz.cvut.kbss.termit.exception.PersistenceException;
import cz.cvut.kbss.termit.model.PasswordChangeRequest;
import cz.cvut.kbss.termit.util.Configuration;
import cz.cvut.kbss.termit.model.UserAccount;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Repository;

Expand All @@ -13,19 +13,16 @@
@Repository
public class PasswordChangeRequestDao extends BaseDao<PasswordChangeRequest> {

private final Configuration.Persistence persistenceConfig;

@Autowired
public PasswordChangeRequestDao(EntityManager em, Configuration configuration) {
public PasswordChangeRequestDao(EntityManager em) {
super(PasswordChangeRequest.class, em);
this.persistenceConfig = configuration.getPersistence();
}

public List<PasswordChangeRequest> findAllByUsername(String username) {
Objects.requireNonNull(username);
public List<PasswordChangeRequest> findAllByUserAccount(UserAccount userAccount) {
Objects.requireNonNull(userAccount);
try {
return em.createQuery("SELECT DISTINCT t FROM " + type.getSimpleName() + " t WHERE t.userAccount.username = :username", type)
.setParameter("username", username, persistenceConfig.getLanguage())
return em.createQuery("SELECT DISTINCT t FROM " + type.getSimpleName() + " t WHERE t.userAccount = :userAccount", type)
.setParameter("userAccount", userAccount)
.getResultList();
} catch (RuntimeException e) {
throw new PersistenceException(e);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,16 +21,20 @@
import cz.cvut.kbss.termit.model.UserAccount;
import cz.cvut.kbss.termit.security.SecurityConstants;
import cz.cvut.kbss.termit.service.business.UserService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.responses.ApiResponse;
import io.swagger.v3.oas.annotations.responses.ApiResponses;
import io.swagger.v3.oas.annotations.security.SecurityRequirement;
import io.swagger.v3.oas.annotations.tags.Tag;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.context.annotation.Profile;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.PutMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
Expand All @@ -41,12 +45,12 @@
* Available only if internal security is used.
*/
@ConditionalOnProperty(prefix = "termit.security", name = "provider", havingValue = "internal", matchIfMissing = true)
@Tag(name = "Admin User Registration", description = "Allows admins to register new users.")
@RestController
@RequestMapping("/users")
@Profile("admin-registration-only")
public class AdminBasedRegistrationController {

private static final Logger LOG = LoggerFactory.getLogger(FreeRegistrationController.class);
private static final Logger LOG = LoggerFactory.getLogger(AdminBasedRegistrationController.class);

private final UserService userService;

Expand All @@ -56,11 +60,17 @@ public AdminBasedRegistrationController(UserService userService) {
LOG.debug("Instantiating admin-based registration controller.");
}

@Operation(security = {@SecurityRequirement(name="bearer-key")},
description = "Creates a new user account. If the password is blank, the account is locked, and an email will be sent to the new user with a link to create a password.")
@ApiResponses({
@ApiResponse(responseCode = "201", description = "User created"),
@ApiResponse(responseCode = "409", description = "User data are invalid")
})
@PreAuthorize("hasRole('" + SecurityConstants.ROLE_ADMIN + "')")
@PostMapping(consumes = {MediaType.APPLICATION_JSON_VALUE, JsonLd.MEDIA_TYPE})
@PutMapping(consumes = {MediaType.APPLICATION_JSON_VALUE, JsonLd.MEDIA_TYPE})
public ResponseEntity<Void> createUser(@RequestBody UserAccount user) {
userService.persist(user);
LOG.info("User {} successfully registered.", user);
userService.adminCreateUser(user);
LOG.info("User {} successfully registered by {}.", user, userService.getCurrent().getUsername());
return new ResponseEntity<>(HttpStatus.CREATED);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

import cz.cvut.kbss.jsonld.JsonLd;
import cz.cvut.kbss.termit.dto.PasswordChangeDto;
import cz.cvut.kbss.termit.service.business.PasswordChangeService;
import cz.cvut.kbss.termit.service.business.UserService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.responses.ApiResponse;
Expand Down Expand Up @@ -30,11 +30,11 @@
public class PasswordChangeController {
private static final Logger LOG = LoggerFactory.getLogger(PasswordChangeController.class);

private final PasswordChangeService passwordChangeService;
private final UserService userService;

@Autowired
public PasswordChangeController(PasswordChangeService passwordChangeService) {
this.passwordChangeService = passwordChangeService;
public PasswordChangeController(UserService userService) {
this.userService = userService;
LOG.debug("Instantiating password change controller.");
}

Expand All @@ -48,7 +48,7 @@ public PasswordChangeController(PasswordChangeService passwordChangeService) {
public ResponseEntity<Void> requestPasswordReset(
@Parameter(description = "Username of the user") @RequestBody String username) {
LOG.info("Password reset requested for user {}.", username);
passwordChangeService.requestPasswordReset(username);
userService.requestPasswordReset(username);
return new ResponseEntity<>(HttpStatus.NO_CONTENT);
}

Expand All @@ -63,7 +63,7 @@ public ResponseEntity<Void> changePassword(
@Parameter(
description = "Token with URI for password reset") @RequestBody PasswordChangeDto passwordChangeDto) {
LOG.info("Password change requested with token {}", passwordChangeDto.getToken());
passwordChangeService.changePassword(passwordChangeDto);
userService.changePassword(passwordChangeDto);
return new ResponseEntity<>(HttpStatus.NO_CONTENT);
}
}

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -17,19 +17,25 @@
*/
package cz.cvut.kbss.termit.service.business;

import cz.cvut.kbss.termit.dto.PasswordChangeDto;
import cz.cvut.kbss.termit.dto.RdfsResource;
import cz.cvut.kbss.termit.dto.mapper.DtoMapper;
import cz.cvut.kbss.termit.event.LoginAttemptsThresholdExceeded;
import cz.cvut.kbss.termit.exception.AuthorizationException;
import cz.cvut.kbss.termit.exception.InvalidPasswordChangeRequestException;
import cz.cvut.kbss.termit.exception.NotFoundException;
import cz.cvut.kbss.termit.exception.UnsupportedOperationException;
import cz.cvut.kbss.termit.exception.ValidationException;
import cz.cvut.kbss.termit.model.PasswordChangeRequest;
import cz.cvut.kbss.termit.model.UserAccount;
import cz.cvut.kbss.termit.model.UserRole;
import cz.cvut.kbss.termit.rest.dto.UserUpdateDto;
import cz.cvut.kbss.termit.service.notification.PasswordChangeNotifier;
import cz.cvut.kbss.termit.service.repository.PasswordChangeRequestRepositoryService;
import cz.cvut.kbss.termit.service.repository.UserRepositoryService;
import cz.cvut.kbss.termit.service.repository.UserRoleRepositoryService;
import cz.cvut.kbss.termit.service.security.SecurityUtils;
import cz.cvut.kbss.termit.util.Configuration;
import cz.cvut.kbss.termit.util.Utils;
import cz.cvut.kbss.termit.util.Vocabulary;
import org.jetbrains.annotations.NotNull;
Expand All @@ -42,9 +48,12 @@
import org.springframework.transaction.annotation.Transactional;

import java.net.URI;
import java.time.Instant;
import java.util.List;
import java.util.Objects;
import java.util.Optional;
import java.util.UUID;
import java.util.function.Supplier;
import java.util.stream.Collectors;

/**
Expand All @@ -54,6 +63,7 @@
public class UserService {

private static final Logger LOG = LoggerFactory.getLogger(UserService.class);
public static final String INVALID_TOKEN_ERROR_MESSAGE_ID = "resetPassword.invalidToken";

private final UserRepositoryService repositoryService;

Expand All @@ -65,14 +75,26 @@ public class UserService {

private final DtoMapper dtoMapper;

private final Configuration.Security securityConfig;

private final PasswordChangeRequestRepositoryService passwordChangeRequestRepositoryService;

private final PasswordChangeNotifier passwordChangeNotifier;

@Autowired
public UserService(UserRepositoryService repositoryService, UserRoleRepositoryService userRoleRepositoryService,
AccessControlListService aclService, SecurityUtils securityUtils, DtoMapper dtoMapper) {
AccessControlListService aclService, SecurityUtils securityUtils, DtoMapper dtoMapper,
Configuration configuration,
PasswordChangeRequestRepositoryService passwordChangeRequestRepositoryService,
PasswordChangeNotifier passwordChangeNotifier) {
this.repositoryService = repositoryService;
this.userRoleRepositoryService = userRoleRepositoryService;
this.aclService = aclService;
this.securityUtils = securityUtils;
this.dtoMapper = dtoMapper;
this.securityConfig = configuration.getSecurity();
this.passwordChangeRequestRepositoryService = passwordChangeRequestRepositoryService;
this.passwordChangeNotifier = passwordChangeNotifier;
}

/**
Expand Down Expand Up @@ -137,6 +159,27 @@ private void updateLastSeen(UserAccount account) {
repositoryService.update(account);
}

/**
* Persists a new user.
* When a password is null or blank,
* a random password is generated and an email for password creation is sent to the user.
* @param account
*/
@Transactional
public void adminCreateUser(UserAccount account) {
if (account.getPassword() == null || account.getPassword().isBlank()) {
// generate random password
account.setPassword(UUID.randomUUID() + UUID.randomUUID().toString());
account.lock();
persist(account);

PasswordChangeRequest passwordChangeRequest = createPasswordChangeRequest(account);
passwordChangeNotifier.sendCreatePasswordEmail(passwordChangeRequest);
} else {
persist(account);
}
}

/**
* Persists the specified user account.
*
Expand Down Expand Up @@ -215,7 +258,7 @@ public void unlock(UserAccount account, String newPassword) {
}

private void ensureNotOwnAccount(UserAccount account, String operation) {
if (securityUtils.getCurrentUser().equals(account)) {
if (securityUtils.isAuthenticated() && securityUtils.getCurrentUser().equals(account)) {
throw new UnsupportedOperationException("Cannot " + operation + " your own account!");
}
}
Expand Down Expand Up @@ -307,4 +350,47 @@ public List<RdfsResource> getManagedAssets(@NonNull UserAccount user) {
return aclService.findAssetsByAgentWithSecurityAccess(user.toUser()).stream()
.map(dtoMapper::assetToRdfsResource).collect(Collectors.toList());
}

private PasswordChangeRequest createPasswordChangeRequest(UserAccount userAccount) {
// delete any existing request for the user
passwordChangeRequestRepositoryService.findAllByUserAccount(userAccount)
.forEach(passwordChangeRequestRepositoryService::remove);

return passwordChangeRequestRepositoryService.create(userAccount);
}

@Transactional
public void requestPasswordReset(String username) {
final UserAccount account = repositoryService.findByUsername(username)
.orElseThrow(() -> NotFoundException.create(UserAccount.class, username));
PasswordChangeRequest request = createPasswordChangeRequest(account);
passwordChangeNotifier.sendPasswordResetEmail(request);
}

private boolean isValid(PasswordChangeRequest request) {
return request.getCreatedAt().plus(securityConfig.getPasswordChangeRequestValidity()).isAfter(Instant.now());
}

/**
* Changes the user's password if there is a valid password change request.
* Unlocks the user account if it is locked.
*/
@Transactional
public void changePassword(PasswordChangeDto passwordChangeDto) {
Supplier<AuthorizationException> exception = () -> new InvalidPasswordChangeRequestException("Invalid or expired password change link", INVALID_TOKEN_ERROR_MESSAGE_ID);
PasswordChangeRequest request = passwordChangeRequestRepositoryService.find(passwordChangeDto.getUri())
.orElseThrow(exception);

if (!request.getToken().equals(passwordChangeDto.getToken()) || !isValid(request)) {
throw exception.get();
}

UserAccount account = repositoryService.find(request.getUserAccount().getUri())
.orElseThrow(exception);

passwordChangeRequestRepositoryService.remove(request);

unlock(account, passwordChangeDto.getNewPassword());
LOG.info("Password changed for user {}.", account.getUsername());
}
}
Loading

0 comments on commit b08c4c8

Please sign in to comment.