fix(auth): OidcAuthController + UserAdminController upsert unprefixed
Follow-up to the UiAuthController fix: every write path that puts a row
into users/user_roles/user_groups must use the bare DB key, because
the env-scoped controllers (Alert, AlertRule, AlertSilence, Outbound)
strip "user:" before using the name as an FK. If the write path stores
prefixed, first-time alerting/outbound writes fail with
alert_rules_created_by_fkey violation.
UiAuthController shipped the model in the prior commit (bare userId
for all DB/RBAC calls, "user:"-namespaced subject for JWT signing).
Bringing the other two write paths in line:
- OidcAuthController.callback:
userId = "oidc:" + oidcUser.subject() // DB key, no "user:"
subject = "user:" + userId // JWT subject (namespaced)
All userRepository / rbacService / applyClaimMappings calls use
userId. Tokens still carry the namespaced subject so
JwtAuthenticationFilter can distinguish user vs agent tokens.
- UserAdminController.createUser: userId = request.username() (bare).
resetPassword: dropped the "user:"-strip fallback that was only
needed because create used to prefix — now dead.
No migration. Greenfield alpha product — any pre-existing prefixed
rows in a dev DB will become orphans on next login (login upserts
the unprefixed row, old prefixed row is harmless but unused).
Operators doing a clean re-index can wipe the DB.
Read-path controllers still strip — harmless for bare DB rows, and
OIDC humans (JWT sub "user:oidc:<s>") still resolve correctly to
the new DB key "oidc:<s>" after stripping.
Verified: 45/45 alerting + outbound ITs pass.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -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<String> violations = PasswordPolicyValidator.validate(request.password(), username);
|
||||
List<String> violations = PasswordPolicyValidator.validate(request.password(), userId);
|
||||
if (!violations.isEmpty()) {
|
||||
throw new ResponseStatusException(HttpStatus.BAD_REQUEST,
|
||||
"Password policy violation: " + String.join("; ", violations));
|
||||
|
||||
@@ -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<UserInfo> 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<String> 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();
|
||||
|
||||
Reference in New Issue
Block a user