mirror of
https://github.com/Stirling-Tools/Stirling-PDF.git
synced 2025-06-06 18:30:57 +00:00

# Description External DB support for Stirling PDF. You can now choose between the default H2 or PostgreSQL by setting the new `enableCustomDatabase` property to `true` or `false`. To enable your own custom (PostgreSQL) database: - Set `enableCustomDatabase` to `true` - Add your database url to `customDatabaseUrl` - Set your `username` and `password` Closes #2270 ## Checklist - [x] I have read the [Contribution Guidelines](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/CONTRIBUTING.md) - [x] I have performed a self-review of my own code - [x] I have attached images of the change if it is UI based - [x] I have commented my code, particularly in hard-to-understand areas - [ ] If my code has heavily changed functionality I have updated relevant docs on [Stirling-PDFs doc repo](https://github.com/Stirling-Tools/Stirling-Tools.github.io/blob/main/docs/) - [x] My changes generate no new warnings - [x] I have read the section [Add New Translation Tags](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/HowToAddNewLanguage.md#add-new-translation-tags) (for new translation tags only)
444 lines
18 KiB
Java
444 lines
18 KiB
Java
package stirling.software.SPDF.config.security;
|
|
|
|
import java.io.IOException;
|
|
import java.sql.SQLException;
|
|
import java.util.*;
|
|
import java.util.stream.Collectors;
|
|
|
|
import org.springframework.context.MessageSource;
|
|
import org.springframework.context.i18n.LocaleContextHolder;
|
|
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
|
|
import org.springframework.security.core.Authentication;
|
|
import org.springframework.security.core.GrantedAuthority;
|
|
import org.springframework.security.core.authority.SimpleGrantedAuthority;
|
|
import org.springframework.security.core.context.SecurityContextHolder;
|
|
import org.springframework.security.core.session.SessionInformation;
|
|
import org.springframework.security.core.userdetails.UserDetails;
|
|
import org.springframework.security.core.userdetails.UsernameNotFoundException;
|
|
import org.springframework.security.crypto.password.PasswordEncoder;
|
|
import org.springframework.security.oauth2.core.user.OAuth2User;
|
|
import org.springframework.stereotype.Service;
|
|
import org.springframework.transaction.annotation.Transactional;
|
|
|
|
import lombok.extern.slf4j.Slf4j;
|
|
import stirling.software.SPDF.config.interfaces.DatabaseInterface;
|
|
import stirling.software.SPDF.config.security.saml2.CustomSaml2AuthenticatedPrincipal;
|
|
import stirling.software.SPDF.config.security.session.SessionPersistentRegistry;
|
|
import stirling.software.SPDF.controller.api.pipeline.UserServiceInterface;
|
|
import stirling.software.SPDF.model.*;
|
|
import stirling.software.SPDF.model.provider.UnsupportedProviderException;
|
|
import stirling.software.SPDF.repository.AuthorityRepository;
|
|
import stirling.software.SPDF.repository.UserRepository;
|
|
|
|
@Service
|
|
@Slf4j
|
|
public class UserService implements UserServiceInterface {
|
|
|
|
private final UserRepository userRepository;
|
|
|
|
private final AuthorityRepository authorityRepository;
|
|
|
|
private final PasswordEncoder passwordEncoder;
|
|
|
|
private final MessageSource messageSource;
|
|
|
|
private final SessionPersistentRegistry sessionRegistry;
|
|
|
|
private final DatabaseInterface databaseService;
|
|
|
|
private final ApplicationProperties applicationProperties;
|
|
|
|
public UserService(
|
|
UserRepository userRepository,
|
|
AuthorityRepository authorityRepository,
|
|
PasswordEncoder passwordEncoder,
|
|
MessageSource messageSource,
|
|
SessionPersistentRegistry sessionRegistry,
|
|
DatabaseInterface databaseService,
|
|
ApplicationProperties applicationProperties) {
|
|
this.userRepository = userRepository;
|
|
this.authorityRepository = authorityRepository;
|
|
this.passwordEncoder = passwordEncoder;
|
|
this.messageSource = messageSource;
|
|
this.sessionRegistry = sessionRegistry;
|
|
this.databaseService = databaseService;
|
|
this.applicationProperties = applicationProperties;
|
|
}
|
|
|
|
@Transactional
|
|
public void migrateOauth2ToSSO() {
|
|
userRepository
|
|
.findByAuthenticationTypeIgnoreCase("OAUTH2")
|
|
.forEach(
|
|
user -> {
|
|
user.setAuthenticationType(AuthenticationType.SSO);
|
|
userRepository.save(user);
|
|
});
|
|
}
|
|
|
|
// Handle OAUTH2 login and user auto creation.
|
|
public boolean processSSOPostLogin(String username, boolean autoCreateUser)
|
|
throws IllegalArgumentException, SQLException, UnsupportedProviderException {
|
|
if (!isUsernameValid(username)) {
|
|
return false;
|
|
}
|
|
Optional<User> existingUser = findByUsernameIgnoreCase(username);
|
|
if (existingUser.isPresent()) {
|
|
return true;
|
|
}
|
|
if (autoCreateUser) {
|
|
saveUser(username, AuthenticationType.SSO);
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
public Authentication getAuthentication(String apiKey) {
|
|
Optional<User> user = getUserByApiKey(apiKey);
|
|
if (!user.isPresent()) {
|
|
throw new UsernameNotFoundException("API key is not valid");
|
|
}
|
|
// Convert the user into an Authentication object
|
|
return new UsernamePasswordAuthenticationToken( // principal (typically the user)
|
|
user, // credentials (we don't expose the password or API key here)
|
|
null, // user's authorities (roles/permissions)
|
|
getAuthorities(user.get()));
|
|
}
|
|
|
|
private Collection<? extends GrantedAuthority> getAuthorities(User user) {
|
|
// Convert each Authority object into a SimpleGrantedAuthority object.
|
|
return user.getAuthorities().stream()
|
|
.map((Authority authority) -> new SimpleGrantedAuthority(authority.getAuthority()))
|
|
.collect(Collectors.toList());
|
|
}
|
|
|
|
private String generateApiKey() {
|
|
String apiKey;
|
|
do {
|
|
apiKey = UUID.randomUUID().toString();
|
|
} while ( // Ensure uniqueness
|
|
userRepository.findByApiKey(apiKey).isPresent());
|
|
return apiKey;
|
|
}
|
|
|
|
public User addApiKeyToUser(String username) {
|
|
Optional<User> user = findByUsernameIgnoreCase(username);
|
|
if (user.isPresent()) {
|
|
user.get().setApiKey(generateApiKey());
|
|
return userRepository.save(user.get());
|
|
}
|
|
throw new UsernameNotFoundException("User not found");
|
|
}
|
|
|
|
public User refreshApiKeyForUser(String username) {
|
|
// reuse the add API key method for refreshing
|
|
return addApiKeyToUser(username);
|
|
}
|
|
|
|
public String getApiKeyForUser(String username) {
|
|
User user =
|
|
findByUsernameIgnoreCase(username)
|
|
.orElseThrow(() -> new UsernameNotFoundException("User not found"));
|
|
return user.getApiKey();
|
|
}
|
|
|
|
public boolean isValidApiKey(String apiKey) {
|
|
return userRepository.findByApiKey(apiKey).isPresent();
|
|
}
|
|
|
|
public Optional<User> getUserByApiKey(String apiKey) {
|
|
return userRepository.findByApiKey(apiKey);
|
|
}
|
|
|
|
public Optional<User> loadUserByApiKey(String apiKey) {
|
|
Optional<User> user = userRepository.findByApiKey(apiKey);
|
|
if (user.isPresent()) {
|
|
return user;
|
|
}
|
|
// or throw an exception
|
|
return null;
|
|
}
|
|
|
|
public boolean validateApiKeyForUser(String username, String apiKey) {
|
|
Optional<User> userOpt = findByUsernameIgnoreCase(username);
|
|
return userOpt.isPresent() && apiKey.equals(userOpt.get().getApiKey());
|
|
}
|
|
|
|
public void saveUser(String username, AuthenticationType authenticationType)
|
|
throws IllegalArgumentException, SQLException, UnsupportedProviderException {
|
|
saveUser(username, authenticationType, Role.USER.getRoleId());
|
|
}
|
|
|
|
public void saveUser(String username, AuthenticationType authenticationType, String role)
|
|
throws IllegalArgumentException, SQLException, UnsupportedProviderException {
|
|
if (!isUsernameValid(username)) {
|
|
throw new IllegalArgumentException(getInvalidUsernameMessage());
|
|
}
|
|
User user = new User();
|
|
user.setUsername(username);
|
|
user.setEnabled(true);
|
|
user.setFirstLogin(false);
|
|
user.addAuthority(new Authority(role, user));
|
|
user.setAuthenticationType(authenticationType);
|
|
userRepository.save(user);
|
|
databaseService.exportDatabase();
|
|
}
|
|
|
|
public void saveUser(String username, String password)
|
|
throws IllegalArgumentException, SQLException, UnsupportedProviderException {
|
|
if (!isUsernameValid(username)) {
|
|
throw new IllegalArgumentException(getInvalidUsernameMessage());
|
|
}
|
|
User user = new User();
|
|
user.setUsername(username);
|
|
user.setPassword(passwordEncoder.encode(password));
|
|
user.setEnabled(true);
|
|
user.setAuthenticationType(AuthenticationType.WEB);
|
|
userRepository.save(user);
|
|
databaseService.exportDatabase();
|
|
}
|
|
|
|
public void saveUser(String username, String password, String role, boolean firstLogin)
|
|
throws IllegalArgumentException, SQLException, UnsupportedProviderException {
|
|
if (!isUsernameValid(username)) {
|
|
throw new IllegalArgumentException(getInvalidUsernameMessage());
|
|
}
|
|
User user = new User();
|
|
user.setUsername(username);
|
|
user.setPassword(passwordEncoder.encode(password));
|
|
user.addAuthority(new Authority(role, user));
|
|
user.setEnabled(true);
|
|
user.setAuthenticationType(AuthenticationType.WEB);
|
|
user.setFirstLogin(firstLogin);
|
|
userRepository.save(user);
|
|
databaseService.exportDatabase();
|
|
}
|
|
|
|
public void saveUser(String username, String password, String role)
|
|
throws IllegalArgumentException, SQLException, UnsupportedProviderException {
|
|
saveUser(username, password, role, false);
|
|
}
|
|
|
|
public void deleteUser(String username) {
|
|
Optional<User> userOpt = findByUsernameIgnoreCase(username);
|
|
if (userOpt.isPresent()) {
|
|
for (Authority authority : userOpt.get().getAuthorities()) {
|
|
if (authority.getAuthority().equals(Role.INTERNAL_API_USER.getRoleId())) {
|
|
return;
|
|
}
|
|
}
|
|
userRepository.delete(userOpt.get());
|
|
}
|
|
invalidateUserSessions(username);
|
|
}
|
|
|
|
public boolean usernameExists(String username) {
|
|
return findByUsername(username).isPresent();
|
|
}
|
|
|
|
public boolean usernameExistsIgnoreCase(String username) {
|
|
return findByUsernameIgnoreCase(username).isPresent();
|
|
}
|
|
|
|
public boolean hasUsers() {
|
|
long userCount = userRepository.count();
|
|
if (findByUsernameIgnoreCase(Role.INTERNAL_API_USER.getRoleId()).isPresent()) {
|
|
userCount -= 1;
|
|
}
|
|
return userCount > 0;
|
|
}
|
|
|
|
public void updateUserSettings(String username, Map<String, String> updates)
|
|
throws SQLException, UnsupportedProviderException {
|
|
Optional<User> userOpt = findByUsernameIgnoreCaseWithSettings(username);
|
|
if (userOpt.isPresent()) {
|
|
User user = userOpt.get();
|
|
Map<String, String> settingsMap = user.getSettings();
|
|
if (settingsMap == null) {
|
|
settingsMap = new HashMap<>();
|
|
}
|
|
settingsMap.clear();
|
|
settingsMap.putAll(updates);
|
|
user.setSettings(settingsMap);
|
|
userRepository.save(user);
|
|
databaseService.exportDatabase();
|
|
}
|
|
}
|
|
|
|
public Optional<User> findByUsername(String username) {
|
|
return userRepository.findByUsername(username);
|
|
}
|
|
|
|
public Optional<User> findByUsernameIgnoreCase(String username) {
|
|
return userRepository.findByUsernameIgnoreCase(username);
|
|
}
|
|
|
|
public Optional<User> findByUsernameIgnoreCaseWithSettings(String username) {
|
|
return userRepository.findByUsernameIgnoreCaseWithSettings(username);
|
|
}
|
|
|
|
public Authority findRole(User user) {
|
|
return authorityRepository.findByUserId(user.getId());
|
|
}
|
|
|
|
public void changeUsername(User user, String newUsername)
|
|
throws IllegalArgumentException,
|
|
IOException,
|
|
SQLException,
|
|
UnsupportedProviderException {
|
|
if (!isUsernameValid(newUsername)) {
|
|
throw new IllegalArgumentException(getInvalidUsernameMessage());
|
|
}
|
|
user.setUsername(newUsername);
|
|
userRepository.save(user);
|
|
databaseService.exportDatabase();
|
|
}
|
|
|
|
public void changePassword(User user, String newPassword)
|
|
throws SQLException, UnsupportedProviderException {
|
|
user.setPassword(passwordEncoder.encode(newPassword));
|
|
userRepository.save(user);
|
|
databaseService.exportDatabase();
|
|
}
|
|
|
|
public void changeFirstUse(User user, boolean firstUse)
|
|
throws SQLException, UnsupportedProviderException {
|
|
user.setFirstLogin(firstUse);
|
|
userRepository.save(user);
|
|
databaseService.exportDatabase();
|
|
}
|
|
|
|
public void changeRole(User user, String newRole)
|
|
throws SQLException, UnsupportedProviderException {
|
|
Authority userAuthority = this.findRole(user);
|
|
userAuthority.setAuthority(newRole);
|
|
authorityRepository.save(userAuthority);
|
|
databaseService.exportDatabase();
|
|
}
|
|
|
|
public void changeUserEnabled(User user, Boolean enbeled)
|
|
throws SQLException, UnsupportedProviderException {
|
|
user.setEnabled(enbeled);
|
|
userRepository.save(user);
|
|
databaseService.exportDatabase();
|
|
}
|
|
|
|
public boolean isPasswordCorrect(User user, String currentPassword) {
|
|
return passwordEncoder.matches(currentPassword, user.getPassword());
|
|
}
|
|
|
|
public boolean isUsernameValid(String username) {
|
|
// Checks whether the simple username is formatted correctly
|
|
boolean isValidSimpleUsername =
|
|
username.matches("^[a-zA-Z0-9][a-zA-Z0-9@._+-]*[a-zA-Z0-9]$");
|
|
// Checks whether the email address is formatted correctly
|
|
boolean isValidEmail =
|
|
username.matches(
|
|
"^(?=.{1,64}@)[A-Za-z0-9]+(\\.[A-Za-z0-9_+.-]+)*@[^-][A-Za-z0-9-]+(\\.[A-Za-z0-9-]+)*(\\.[A-Za-z]{2,})$");
|
|
List<String> notAllowedUserList = new ArrayList<>();
|
|
notAllowedUserList.add("ALL_USERS".toLowerCase());
|
|
boolean notAllowedUser = notAllowedUserList.contains(username.toLowerCase());
|
|
return (isValidSimpleUsername || isValidEmail) && !notAllowedUser;
|
|
}
|
|
|
|
private String getInvalidUsernameMessage() {
|
|
return messageSource.getMessage(
|
|
"invalidUsernameMessage", null, LocaleContextHolder.getLocale());
|
|
}
|
|
|
|
public boolean hasPassword(String username) {
|
|
Optional<User> user = findByUsernameIgnoreCase(username);
|
|
return user.isPresent() && user.get().hasPassword();
|
|
}
|
|
|
|
public boolean isAuthenticationTypeByUsername(
|
|
String username, AuthenticationType authenticationType) {
|
|
Optional<User> user = findByUsernameIgnoreCase(username);
|
|
return user.isPresent()
|
|
&& authenticationType.name().equalsIgnoreCase(user.get().getAuthenticationType());
|
|
}
|
|
|
|
public boolean isUserDisabled(String username) {
|
|
Optional<User> userOpt = findByUsernameIgnoreCase(username);
|
|
return userOpt.map(user -> !user.isEnabled()).orElse(false);
|
|
}
|
|
|
|
public void invalidateUserSessions(String username) {
|
|
String usernameP = "";
|
|
for (Object principal : sessionRegistry.getAllPrincipals()) {
|
|
for (SessionInformation sessionsInformation :
|
|
sessionRegistry.getAllSessions(principal, false)) {
|
|
if (principal instanceof UserDetails) {
|
|
UserDetails userDetails = (UserDetails) principal;
|
|
usernameP = userDetails.getUsername();
|
|
} else if (principal instanceof OAuth2User) {
|
|
OAuth2User oAuth2User = (OAuth2User) principal;
|
|
usernameP = oAuth2User.getName();
|
|
} else if (principal instanceof CustomSaml2AuthenticatedPrincipal) {
|
|
CustomSaml2AuthenticatedPrincipal saml2User =
|
|
(CustomSaml2AuthenticatedPrincipal) principal;
|
|
usernameP = saml2User.getName();
|
|
} else if (principal instanceof String) {
|
|
usernameP = (String) principal;
|
|
}
|
|
if (usernameP.equalsIgnoreCase(username)) {
|
|
sessionRegistry.expireSession(sessionsInformation.getSessionId());
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
public String getCurrentUsername() {
|
|
Object principal = SecurityContextHolder.getContext().getAuthentication().getPrincipal();
|
|
if (principal instanceof UserDetails) {
|
|
return ((UserDetails) principal).getUsername();
|
|
} else if (principal instanceof OAuth2User) {
|
|
return ((OAuth2User) principal)
|
|
.getAttribute(
|
|
applicationProperties.getSecurity().getOauth2().getUseAsUsername());
|
|
} else if (principal instanceof CustomSaml2AuthenticatedPrincipal) {
|
|
return ((CustomSaml2AuthenticatedPrincipal) principal).getName();
|
|
} else if (principal instanceof String) {
|
|
return (String) principal;
|
|
} else {
|
|
return principal.toString();
|
|
}
|
|
}
|
|
|
|
@Transactional
|
|
public void syncCustomApiUser(String customApiKey)
|
|
throws SQLException, UnsupportedProviderException {
|
|
if (customApiKey == null || customApiKey.trim().length() == 0) {
|
|
return;
|
|
}
|
|
String username = "CUSTOM_API_USER";
|
|
Optional<User> existingUser = findByUsernameIgnoreCase(username);
|
|
if (!existingUser.isPresent()) {
|
|
// Create new user with API role
|
|
User user = new User();
|
|
user.setUsername(username);
|
|
user.setPassword(UUID.randomUUID().toString());
|
|
user.setEnabled(true);
|
|
user.setFirstLogin(false);
|
|
user.setAuthenticationType(AuthenticationType.WEB);
|
|
user.setApiKey(customApiKey);
|
|
user.addAuthority(new Authority(Role.INTERNAL_API_USER.getRoleId(), user));
|
|
userRepository.save(user);
|
|
databaseService.exportDatabase();
|
|
} else {
|
|
// Update API key if it has changed
|
|
User user = existingUser.get();
|
|
if (!customApiKey.equals(user.getApiKey())) {
|
|
user.setApiKey(customApiKey);
|
|
userRepository.save(user);
|
|
databaseService.exportDatabase();
|
|
}
|
|
}
|
|
}
|
|
|
|
@Override
|
|
public long getTotalUsersCount() {
|
|
return userRepository.count();
|
|
}
|
|
}
|