001package gudusoft.gsqlparser.runtime;
002
003import gudusoft.gsqlparser.util.Logger;
004import gudusoft.gsqlparser.util.LoggerFactory;
005
006import java.security.SecureRandom;
007
008/**
009 * Entry point for activating this GSP runtime with a signed entitlement.
010 *
011 * <p>Replaces the former {@code TSQLEnv.usedBySqlflow} guard, which was a
012 * private boolean a host set by reflection: anyone who read the bytecode knew
013 * the secret, and the failure mode for everyone else was silently wrong
014 * results. This is a signed, expiring, revocable, auditable grant instead.</p>
015 *
016 * <h3>Usage</h3>
017 * <pre>
018 * GspActivation result = GspRuntime.activate(Files.readAllBytes(path));
019 * if (!result.isValid()) {
020 *     throw new IllegalStateException("GSP entitlement rejected: "
021 *             + result.reasonCode() + " audit=" + result.auditId());
022 * }
023 * </pre>
024 *
025 * <p>Activate once per process, before the service reports ready, and treat
026 * failure as a startup failure rather than a per-request one.</p>
027 *
028 * <p><b>There is deliberately no {@code setActivated(true)}.</b> The only way
029 * to reach {@link GspActivationStatus#ACTIVE} is to present bytes carrying a
030 * valid signature from a private key this build does not contain.</p>
031 *
032 * <h3>What this build does and does not do</h3>
033 *
034 * <p>{@code activate} performs full verification and reports an accurate
035 * verdict. <b>Nothing yet refuses to produce results when the runtime is not
036 * activated</b> — wiring enforcement into the result-producing entry points is
037 * the next step. Until then this API is observable but not enforcing, and
038 * saying otherwise would overstate what the jar protects.</p>
039 */
040public final class GspRuntime {
041
042    private static final Logger log = LoggerFactory.getLogger(GspRuntime.class);
043
044    private static final SecureRandom AUDIT_IDS = new SecureRandom();
045
046    /**
047     * Current activation. Volatile because activation happens on a startup
048     * thread while reads come from parsing threads.
049     *
050     * <p>Never null: before any attempt it reports
051     * {@link GspActivationStatus#MISSING}, so a caller that skipped activation
052     * and one that failed it are both plainly not activated.</p>
053     */
054    private static volatile GspActivation current =
055            new GspActivation(GspActivationStatus.MISSING, null, null, 0L);
056
057    private GspRuntime() {
058        // static entry point only
059    }
060
061    /**
062     * Verifies a signed entitlement and records the outcome.
063     *
064     * <p>Every refusal is logged at ERROR with a greppable marker and an audit
065     * id matching the returned object. That pairing is a condition of the
066     * agreed design: a covert refusal is only acceptable when an operator can
067     * still find out why from the host's own logs.</p>
068     *
069     * @param signedEntitlement compact JWS bytes; null or empty yields
070     *                          {@link GspActivationStatus#MISSING}
071     * @return the outcome; never null, never throws
072     */
073    public static GspActivation activate(byte[] signedEntitlement) {
074        String auditId = newAuditId();
075        GspActivation result;
076        try {
077            result = EntitlementVerifier.verify(
078                    signedEntitlement, System.currentTimeMillis() / 1000L, auditId);
079        } catch (Throwable t) {
080            // Verification must never propagate. An exception escaping here
081            // would be a denial-of-service on the host's startup path, and
082            // worse, a caller catching broadly could mistake it for "no
083            // entitlement configured" and continue.
084            log.error("GSP_ENTITLEMENT_REFUSED reason=MALFORMED audit=" + auditId
085                    + " (verifier raised " + t.getClass().getName() + ")", t);
086            result = new GspActivation(GspActivationStatus.MALFORMED, auditId, null, 0L);
087        }
088
089        if (result.isValid()) {
090            log.info("GSP_ENTITLEMENT_ACCEPTED audit=" + result.auditId()
091                    + " deployment=" + result.deploymentId()
092                    + " expiresAt=" + result.expiresAtEpochSeconds());
093        } else {
094            log.error("GSP_ENTITLEMENT_REFUSED reason=" + result.reasonCode()
095                    + " audit=" + result.auditId());
096        }
097        current = result;
098        return result;
099    }
100
101    /**
102     * Whether this runtime currently holds a valid entitlement.
103     *
104     * <p>Re-checks expiry on every call, so a long-running process does not
105     * stay activated past {@code exp} just because it started before it.</p>
106     */
107    public static boolean isActivated() {
108        return activationStatus().isValid();
109    }
110
111    /**
112     * The current activation, re-evaluated for expiry.
113     *
114     * @return never null; {@link GspActivationStatus#MISSING} before any
115     *         {@link #activate} call
116     */
117    public static GspActivation activationStatus() {
118        GspActivation snapshot = current;
119        if (snapshot.isValid()
120                && snapshot.expiresAtEpochSeconds() > 0L
121                && System.currentTimeMillis() / 1000L > snapshot.expiresAtEpochSeconds()) {
122            GspActivation expired = new GspActivation(
123                    GspActivationStatus.EXPIRED, snapshot.auditId(),
124                    snapshot.deploymentId(), snapshot.expiresAtEpochSeconds());
125            log.error("GSP_ENTITLEMENT_REFUSED reason=EXPIRED audit=" + expired.auditId()
126                    + " (entitlement expired while the process was running)");
127            current = expired;
128            return expired;
129        }
130        return snapshot;
131    }
132
133    /**
134     * Marker resource that makes this build refuse to work without an
135     * entitlement. Added by the SQLFlow build profile; absent from the ordinary
136     * commercial and trial jars, which must keep working for every existing
137     * customer exactly as before.
138     */
139    private static final String ENFORCEMENT_MARKER = "META-INF/gsp-entitlement-required";
140
141    /** Resolved once. {@code Boolean} rather than boolean so tests can override. */
142    private static volatile Boolean enforcementOverride = null;
143
144    private static final boolean MARKER_PRESENT =
145            GspRuntime.class.getClassLoader().getResource(ENFORCEMENT_MARKER) != null;
146
147    /**
148     * Whether this build refuses to produce results without an entitlement.
149     *
150     * <p>False for the ordinary jars, so this whole mechanism is invisible to
151     * existing users. True only for a build that shipped the marker.</p>
152     *
153     * <p>Honest about its limits: a marker on the classpath raises the cost of
154     * removing the check, it is not a security boundary. An attacker who
155     * controls the JVM can strip it, just as they could patch the check out of
156     * the bytecode. Obfuscation and this marker buy cost, not impossibility.</p>
157     */
158    public static boolean isEnforcementEnabled() {
159        Boolean override = enforcementOverride;
160        return override != null ? override.booleanValue() : MARKER_PRESENT;
161    }
162
163    /**
164     * Refuses to continue when an entitlement-gated build is not activated.
165     *
166     * <p>Called from the result-producing entry points. On a build without the
167     * marker this is a cheap no-op.</p>
168     *
169     * @throws GspNotActivatedException explicitly, rather than degrading the
170     *         result — see that class for why covert failure was rejected
171     */
172    public static void requireActivated() {
173        if (!isEnforcementEnabled()) {
174            return;
175        }
176        GspActivation state = activationStatus();
177        if (!state.isValid()) {
178            if (state.auditId() == null) {
179                // The never-activated state is a constant and carries no audit
180                // id, so a refusal here would log "audit=null" -- useless to
181                // the operator who has to explain a failed startup. The agreed
182                // design makes the audit id a CONDITION of refusing at all, so
183                // mint one at the refusal point rather than log a null.
184                state = new GspActivation(state.getStatus(), newAuditId(),
185                        state.deploymentId(), state.expiresAtEpochSeconds());
186            }
187            log.error("GSP_ENTITLEMENT_REFUSED reason=" + state.reasonCode()
188                    + " audit=" + state.auditId()
189                    + " (refusing to produce a result on an entitlement-gated build)");
190            throw new GspNotActivatedException(state);
191        }
192    }
193
194    /** Test seam: force enforcement on or off; null restores the build default. */
195    static void setEnforcementForTesting(Boolean value) {
196        enforcementOverride = value;
197    }
198
199    /** Test seam: restore the pre-activation state. */
200    static void resetForTesting() {
201        current = new GspActivation(GspActivationStatus.MISSING, null, null, 0L);
202    }
203
204    private static String newAuditId() {
205        byte[] raw = new byte[8];
206        AUDIT_IDS.nextBytes(raw);
207        StringBuilder sb = new StringBuilder(16);
208        for (byte b : raw) {
209            sb.append(Character.forDigit((b >> 4) & 0xF, 16));
210            sb.append(Character.forDigit(b & 0xF, 16));
211        }
212        return sb.toString();
213    }
214}