feat: add password support for local user creation and per-user login

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
hsiegeln
2026-03-17 19:08:19 +01:00
parent 653ef958ed
commit 6f5b5b8655
7 changed files with 71 additions and 30 deletions

View File

@@ -21,6 +21,7 @@ import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
@@ -30,6 +31,7 @@ import org.springframework.web.server.ResponseStatusException;
import java.time.Instant;
import java.util.List;
import java.util.Map;
import java.util.Optional;
/**
* Authentication endpoints for the UI (local credentials).
@@ -44,6 +46,7 @@ import java.util.Map;
public class UiAuthController {
private static final Logger log = LoggerFactory.getLogger(UiAuthController.class);
private static final BCryptPasswordEncoder passwordEncoder = new BCryptPasswordEncoder();
private final JwtService jwtService;
private final SecurityProperties properties;
@@ -70,38 +73,41 @@ public class UiAuthController {
HttpServletRequest httpRequest) {
String configuredUser = properties.getUiUser();
String configuredPassword = properties.getUiPassword();
if (configuredUser == null || configuredUser.isBlank()
|| configuredPassword == null || configuredPassword.isBlank()) {
log.warn("UI authentication attempted but CAMELEER_UI_USER / CAMELEER_UI_PASSWORD not configured");
auditService.log(request.username(), "login_failed", AuditCategory.AUTH, null,
Map.of("reason", "UI authentication not configured"), AuditResult.FAILURE, httpRequest);
throw new ResponseStatusException(HttpStatus.UNAUTHORIZED, "UI authentication not configured");
}
if (!configuredUser.equals(request.username())
|| !configuredPassword.equals(request.password())) {
log.debug("UI login failed for user: {}", request.username());
auditService.log(request.username(), "login_failed", AuditCategory.AUTH, null,
Map.of("reason", "Invalid credentials"), AuditResult.FAILURE, httpRequest);
throw new ResponseStatusException(HttpStatus.UNAUTHORIZED, "Invalid credentials");
}
String subject = "user:" + request.username();
// Upsert local user into store (without roles — roles are in user_roles table)
try {
userRepository.upsert(new UserInfo(
subject, "local", "", request.username(), Instant.now()));
rbacService.assignRoleToUser(subject, SystemRole.ADMIN_ID);
rbacService.addUserToGroup(subject, SystemRole.ADMINS_GROUP_ID);
} catch (Exception e) {
log.warn("Failed to upsert local user to store (login continues): {}", e.getMessage());
// Try env-var admin first
boolean envMatch = configuredUser != null && !configuredUser.isBlank()
&& configuredPassword != null && !configuredPassword.isBlank()
&& configuredUser.equals(request.username())
&& configuredPassword.equals(request.password());
if (!envMatch) {
// Try per-user password
Optional<String> hash = userRepository.getPasswordHash(subject);
if (hash.isEmpty() || !passwordEncoder.matches(request.password(), hash.get())) {
log.debug("UI login failed for user: {}", request.username());
auditService.log(request.username(), "login_failed", AuditCategory.AUTH, null,
Map.of("reason", "Invalid credentials"), AuditResult.FAILURE, httpRequest);
throw new ResponseStatusException(HttpStatus.UNAUTHORIZED, "Invalid credentials");
}
}
if (envMatch) {
// Env-var admin: upsert and ensure ADMIN role + Admins group
try {
userRepository.upsert(new UserInfo(
subject, "local", "", request.username(), Instant.now()));
rbacService.assignRoleToUser(subject, SystemRole.ADMIN_ID);
rbacService.addUserToGroup(subject, SystemRole.ADMINS_GROUP_ID);
} catch (Exception e) {
log.warn("Failed to upsert local admin to store (login continues): {}", e.getMessage());
}
}
// Per-user logins: user already exists in DB (created by admin)
List<String> roles = rbacService.getSystemRoleNames(subject);
if (roles.isEmpty()) {
roles = List.of("ADMIN");
roles = List.of("VIEWER");
}
String accessToken = jwtService.createAccessToken(subject, "user", roles);