Class CloudAuthManager

java.lang.Object
com.ultikits.ultitools.utils.CloudAuthManager

@Internal public class CloudAuthManager extends Object
The three command-facing entry points behind /ulticloud login|logout|status.

As of plan 16-09 (D-17) this class owns no state of its own and exposes no generation-shaped or TokenEntity-shaped public surface -- everything that used to be a fine-grained public static (the generation triple, saveToken/clearToken/refreshToken/ loadSavedToken, the start/stop pairs for polling and refresh) is gone outright, not deprecated: CloudSession instance methods now carry that behaviour, reached only through login(DeviceLoginListener), logout() and status() below. Keeping a delegating shim for any of them would leave the generation convention callable, which is exactly what issue #298's second acceptance criterion requires to stop being true (see plan 16-08's CloudSession javadoc for the mechanism this replaced).

CredentialStaticSurfaceInvariantTest enforces this structurally: no public static method anywhere in this package may accept or return a TokenEntity or a generation (D-18), so a static bypass of the session's credential commit cannot be reintroduced unnoticed. As of 6.3.1 the 6.3.0 user token is neither stored nor sent (plan 22-24 removed its store); the server credential is committed only through CloudSession.commitCredential(ServerCredential).

Marked ApiStatus.Internal -- this class exists to serve CloudLoginCommand's three @CmdMapping methods and is not part of the framework's public API surface for module authors.

  • Field Details

  • Constructor Details

    • CloudAuthManager

      private CloudAuthManager()
  • Method Details

    • login

      public static void login(CloudAuthManager.DeviceLoginListener listener)
      Links this server to UltiCloud with the RFC 8628 device authorization grant (protocol v2).

      Runs the already-linked check (a credential held by the current session or stored in credentials.json), the rate-limit check, then replaces the current session and returns. The device-code request and the polling run on the new session's own poll executor, so a caller on the server's primary thread is never blocked on the network; their outcome reaches listener on that executor's thread.

      The lock structure is the one the 6.3.0 magic-link login used, unchanged: CloudSession.class, then the captured session's monitor, the re-check, the sessionAtCheck != CloudSession.current() guard, disableCloud(sessionAtCheck), startNew() -- the documented lock order of CloudSession's class javadoc.

      Parameters:
      listener - receives every outcome; must not be null
      Since:
      6.3.1
    • loginWithCode

      public static void loginWithCode(String deviceCode, CloudAuthManager.DeviceLoginListener listener)
      Links this server with a device code UltiCloud has already approved: the single-use code the panel's "Revoke and re-pair" action shows, typed as /ulticloud login <code> (issue #869). No new device code is requested; the code is exchanged through the same POST /v1/device/token poll a normal login uses, and a grant is stored, and the connection started, exactly as for a normal login. The credential it yields belongs to the server record the code was bound to, so the server keeps its id, name and history.

      The already-linked check, the rate limit and the session replacement are those of login(DeviceLoginListener); CloudAuthManager.DeviceLoginListener.onCode(java.lang.String, java.lang.String, java.lang.String, long) is never called. A code that does not have the contract's shape is refused through CloudAuthManager.DeviceLoginListener.onRequestFailed(java.lang.String) before anything else, and never sent. A used, unknown or expired code ends in CloudAuthManager.DeviceLoginListener.onExpired(). The code is a bearer secret: it is never logged or echoed.

      Parameters:
      deviceCode - the code from the panel
      listener - receives every outcome; must not be null
      Since:
      6.4.0
    • isWellFormedRepairCode

      public static boolean isWellFormedRepairCode(String code)
      Parameters:
      code - the text an operator typed after /ulticloud login, or null
      Returns:
      true when it has the shape of an UltiCloud device code (^[A-Za-z0-9_-]{43,128}$)
      Since:
      6.4.0
    • start

      private static void start(CloudAuthManager.DeviceLoginListener listener, Consumer<CloudSession> begin)
      The checks and the session replacement shared by both logins; begin then starts the network part on the new session.
    • linkedCredential

      static ServerCredential linkedCredential(CloudSession session)
      The current credential for status, login and logout. The session is its one owner (plan 22-21 fix): every change to the stored entry -- device login, rotation, logout, rejection -- updates the session's copy in the same step, so the two never diverge. The file is read only when the session holds no credential (it has not adopted one yet, for example while cloud features are off at startup), so logout still revokes and deletes a stored credential the running session never loaded.
      Returns:
      the credential session holds, else the one stored in credentials.json, else null
    • runningFrameworkVersion

      static String runningFrameworkVersion()
      Returns:
      the running framework version, sent verbatim (a -SNAPSHOT suffix included): the plugin description's version, else env.yml's, else unknown
    • suggestedServerName

      static String suggestedServerName()
      Returns:
      the server's MOTD as plain text, at most 64 code points, or null when there is no server or the MOTD is blank (the field is then omitted; the owner names the server on the confirmation page either way)
    • normaliseSuggestedName

      static String normaliseSuggestedName(String motd)
      Parameters:
      motd - a legacy-formatted MOTD, or null
      Returns:
      the MOTD without colour codes, with every run of whitespace or control characters collapsed to one space, trimmed and cut to 64 code points without splitting a surrogate pair; null when nothing remains
    • storedLegacyServerUuid

      static String storedLegacyServerUuid()
      Returns:
      this server's stored install id (uuid in credentials.json) normalised by LegacyServerId.canonical(String), or null; reads only, never writes
    • logout

      public static boolean logout() throws IOException
      Tears down cloud features unconditionally, then asynchronously revokes and deletes the protocol v2 credential. The revoke and delete finish after this method returns; use logoutReporting(Consumer) to observe their outcome. In both forms teardown must run even when there is nothing to clear (an expired-but-still-connected session is the case that most needs logout to take effect), and the credential is read only after teardown so an in-flight login that commits mid-teardown is still caught and cleared -- see CloudLoginCommand.logout(CommandSender)'s own comments for why that order matters.

      CR-01 (16-REVIEW-cloud.md): this method captures CloudSession.current() exactly once -- in the statement below, immediately before teardown begins -- and passes that captured reference to PluginInitiationUtils.disableCloud(CloudSession), which acts on and returns that exact instance. The revoke-and-delete decision then acts on the returned reference, never on a second, independent call to CloudSession.current() taken after teardown returns. Reading current() a second time, afterward, used to be exactly the bug: a concurrent login() (unsynchronized, and reachable from a different thread via @RunAsync) can install a brand-new session with CloudSession.startNew() in the gap while teardown is running, and that second read would then see the NEW session's (always null) credential instead of the one actually being logged out of -- concluding "nothing to clear" and leaving the real credential on disk. Acting on the one reference this method already holds makes that race structurally impossible: there is no second read left to disagree with the first. This method deliberately does not call CloudSession.startNew() itself any more either; clearing the torn-down session in place is sufficient, and the next real login installs a fresh session via its own CloudSession.startNew() call, exactly as it always has.

      Round-1 external review finding: the paragraph above establishes WHICH session this method acts on, but not what its delete is safe to remove. A concurrent login() can write a fresh credential to the same shared document while this session's teardown is in flight, so the delete is a compare-and-delete keyed on the retiring credential's id (ServerCredentialStore.deleteIfMatches(String)): it never removes a credential the torn-down session did not hold. A login() that installs its new session after this method's single read of CloudSession.current() has lost the session race regardless -- CloudSession.startNew() invalidates whatever session it replaces.

      Returns:
      true if a credential existed and cleanup was started, or a pending authentication attempt was cancelled (real-machine UAT fix, plan 16-08 -- see CloudSession.hasAnythingToClear()); false if there was nothing to clear and nothing pending (teardown still ran regardless)
      Throws:
      IOException - never as of 6.3.1 (a failed local delete is reported by the asynchronous task); kept in the 6.3.0 signature
    • logoutReporting

      public static void logoutReporting(Consumer<CloudAuthManager.LogoutOutcome> onDone)
      Stops the captured session synchronously, then revokes and deletes its credential on a Bukkit asynchronous task. The callback runs on that task, or inline when no credential is held. A local storage failure is reported as CloudAuthManager.LogoutOutcome.LOCAL_DELETE_FAILED, never as deletion.
      Parameters:
      onDone - receives the completed outcome
      Since:
      6.3.1
    • beginLogout

      private static boolean beginLogout(Consumer<CloudAuthManager.LogoutOutcome> onDone)
    • deleteLocally

      private static boolean deleteLocally(CloudSession owner, ServerCredential credential)
      Deletes the retired credential from the local store. A storage failure is logged with its cause class only and reported as CloudAuthManager.LogoutOutcome.LOCAL_DELETE_FAILED; the session no longer holds the credential either way, so running logout again retries the delete.
    • reconnect

      public static CloudAuthManager.ReconnectResult reconnect()
      Requests an immediate connection attempt without waiting for the network. The ten-second cooldown applies to every invocation, including one refused by the current connection state.
      Returns:
      whether an attempt was queued, or why the request was refused
      Since:
      6.3.1
    • status

      public static CloudAuthManager.CloudStatus status()
      Returns:
      an immutable snapshot of the current session's connection state, for /ulticloud status to render -- never the token itself (D-18)