AuthBackend.java
package com.cloudforge.core.manager.auth;
import java.util.List;
import java.util.Optional;
/**
* CloudForge Manager's Users-page CRUD, abstracted over whichever directory of accounts is
* currently authoritative — the local H2/Postgres DB, or an AWS Cognito User Pool. Replaces a
* single persisted boolean flag ({@code AuthBackendStore.Config#cognitoEnabled()}) that used to be
* branched on individually inside every one of {@code AuthService}'s five user-management methods
* ({@code listUsers}/{@code createLocalUser}/{@code updateUser}/{@code revokeSessions}/{@code
* deleteUser}) — {@code AuthService} now resolves one {@code AuthBackend} implementation per call
* and delegates through it uniformly.
*
* <p><b>Deliberately scoped to exactly this — Users-page CRUD — and nothing else.</b> Caller/session
* resolution during normal request handling ({@code AuthService.resolveCaller}/{@code
* ensureOidcUser}) branches only on {@code authMode()} (the *sign-in* mechanism: none/alb-oidc/
* application-oidc) and always resolves through the local {@code manager_user} table regardless of
* which {@code AuthBackend} is active for account CRUD — a Cognito-backend-enabled account still
* signs in via whatever OIDC flow {@code authMode} configures, gets upserted into the local table
* on each successful principal, and its {@code Caller}/role come from that local row. Expanding
* this interface to also own caller/session resolution would be an unrelated behavior change no
* one asked for; if that ever needs to change, it's a deliberate, separate decision, not a
* consequence of this interface's existence.</p>
*
* <p>Implementations: {@code LocalH2AuthBackend} (wraps {@code UserStore}+{@code SessionManager})
* and {@code CognitoAuthBackend} (wraps {@code CognitoUserManagementService}), both in {@code
* cloudforge-manager} — this interface and its DTOs ({@link AuthAccount}, {@link
* AuthAccountRequest}, {@link AuthAccountUpdate}) live in {@code cloudforge-core} because they're
* plain contracts with no Spring/persistence dependency, following the same split {@code
* ApplicationSpec}/{@code DatabaseSpec} already establish for this module.</p>
*/
public interface AuthBackend {
List<AuthAccount> listAccounts();
AuthAccount createAccount(AuthAccountRequest request);
AuthAccount updateAccount(String accountId, AuthAccountUpdate update);
/** Invalidates every active session/token for this account without deleting it — the local
* equivalent of Cognito's {@code AdminUserGlobalSignOut}. Returns the (unchanged) account so
* callers can render an up-to-date view without a second lookup. */
AuthAccount revokeSessions(String accountId);
void deleteAccount(String accountId);
/**
* One-time setup a backend needs before it can accept migrated-in accounts (e.g. Cognito's IAM
* role groups must exist before a migration starts adding users to them). Called once per
* migration run, before the first {@link #createAccount}. No-op by default — {@code
* LocalH2AuthBackend} has nothing to prepare.
*/
default void prepareForIncomingMigration() {
}
/**
* Finds one account by its {@link AuthAccount#id()}, or empty if this backend has no such
* account. Exists so callers that only have an id — e.g. Access Control's policy-override
* editor, given whatever id {@link #listAccounts()} last handed the frontend — can resolve a
* username/role for display without requiring the account to also exist somewhere else (the
* bug this closes: Access Control used to look a Cognito-backend account's id up in Manager's
* own local user table only, which a pure-Cognito account with no local row — never having
* signed in through application-oidc — was never going to be in, producing "user not found"
* for an account {@link #listAccounts()} had just shown a moment earlier).
*
* <p>Default implementation scans {@link #listAccounts()} — correct for any backend, just not
* the cheapest possible lookup; override when a backend can look up a single account more
* directly (see {@code CognitoAuthBackend}).
*/
default Optional<AuthAccount> findAccount(String accountId) {
if (accountId == null || accountId.isBlank()) {
return Optional.empty();
}
return listAccounts().stream().filter(account -> accountId.equals(account.id())).findFirst();
}
}