diff --git a/cameleer-server-app/src/main/java/com/cameleer/server/app/controller/UserAdminController.java b/cameleer-server-app/src/main/java/com/cameleer/server/app/controller/UserAdminController.java index 501e4522..063219b1 100644 --- a/cameleer-server-app/src/main/java/com/cameleer/server/app/controller/UserAdminController.java +++ b/cameleer-server-app/src/main/java/com/cameleer/server/app/controller/UserAdminController.java @@ -93,7 +93,9 @@ public class UserAdminController { return ResponseEntity.badRequest() .body(Map.of("error", "Local user creation is disabled when OIDC is enabled. Users are provisioned automatically via SSO.")); } - String userId = "user:" + request.username(); + // DB key is the bare username (matches alert_rules.created_by FK shape used by + // the env-scoped read-path controllers, which strip "user:" from JWT subjects). + String userId = request.username(); UserInfo user = new UserInfo(userId, "local", request.email() != null ? request.email() : "", request.displayName() != null ? request.displayName() : request.username(), @@ -215,9 +217,7 @@ public class UserAdminController { return ResponseEntity.badRequest().build(); } } - // Extract bare username from "user:username" format for policy check - String username = userId.startsWith("user:") ? userId.substring(5) : userId; - List violations = PasswordPolicyValidator.validate(request.password(), username); + List violations = PasswordPolicyValidator.validate(request.password(), userId); if (!violations.isEmpty()) { throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "Password policy violation: " + String.join("; ", violations)); diff --git a/cameleer-server-app/src/main/java/com/cameleer/server/app/security/OidcAuthController.java b/cameleer-server-app/src/main/java/com/cameleer/server/app/security/OidcAuthController.java index f37feca1..72d8298d 100644 --- a/cameleer-server-app/src/main/java/com/cameleer/server/app/security/OidcAuthController.java +++ b/cameleer-server-app/src/main/java/com/cameleer/server/app/security/OidcAuthController.java @@ -140,28 +140,29 @@ public class OidcAuthController { OidcTokenExchanger.OidcUserInfo oidcUser = tokenExchanger.exchange(request.code(), request.redirectUri()); - String userId = "user:oidc:" + oidcUser.subject(); + // DB key is unprefixed (matches alert_rules.created_by FK shape used by the + // env-scoped read-path controllers). JWT subject keeps the "user:" namespace + // so JwtAuthenticationFilter can still distinguish user vs agent tokens. + String userId = "oidc:" + oidcUser.subject(); + String subject = "user:" + userId; String issuerHost = URI.create(config.get().issuerUri()).getHost(); String provider = "oidc:" + issuerHost; - // Check auto-signup gate: if disabled, user must already exist Optional existingUser = userRepository.findById(userId); if (!config.get().autoSignup() && existingUser.isEmpty()) { throw new ResponseStatusException(HttpStatus.FORBIDDEN, "Account not provisioned. Contact your administrator."); } - // Upsert user (without roles -- roles are in user_roles table) userRepository.upsert(new UserInfo( userId, provider, oidcUser.email(), oidcUser.name(), Instant.now())); - // Apply claim mapping rules to assign managed roles/groups from JWT claims applyClaimMappings(userId, oidcUser.allClaims(), oidcUser.roles(), config.get()); List roles = rbacService.getSystemRoleNames(userId); - String accessToken = jwtService.createAccessToken(userId, "user", roles); - String refreshToken = jwtService.createRefreshToken(userId, "user", roles); + String accessToken = jwtService.createAccessToken(subject, "user", roles); + String refreshToken = jwtService.createRefreshToken(subject, "user", roles); String displayName = oidcUser.name() != null && !oidcUser.name().isBlank() ? oidcUser.name() : oidcUser.email();