001/*
002 * Licensed to the Apache Software Foundation (ASF) under one or more
003 * contributor license agreements.  See the NOTICE file distributed with
004 * this work for additional information regarding copyright ownership.
005 * The ASF licenses this file to You under the Apache License, Version 2.0
006 * (the "License"); you may not use this file except in compliance with
007 * the License.  You may obtain a copy of the License at
008 *
009 *      http://www.apache.org/licenses/LICENSE-2.0
010 *
011 * Unless required by applicable law or agreed to in writing, software
012 * distributed under the License is distributed on an "AS IS" BASIS,
013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
014 * See the License for the specific language governing permissions and
015 * limitations under the License.
016 */
017package org.apache.juneau.console.views;
018
019import static org.apache.juneau.commons.utils.Shorts.*;
020
021import java.nio.charset.*;
022import java.security.*;
023import java.util.*;
024
025/**
026 * A server-minted idempotency key for a row-action submit, bound at mint time to the {@code (action, targetId)} pair
027 * it was issued for.
028 *
029 * <h5 class='section'>Why the key is server-minted and bound, not a client UUID</h5>
030 * <p>
031 * The key is minted <b>when the modal opens</b> &mdash; the server is already in the loop then, since the modal-open
032 * confirmation is server-rendered against the current record &mdash; so a double-click, an impatient re-submit and a
033 * browser retry all carry the <b>same</b> key and collapse to one effect.  The modal owns the key's <i>carry</i>
034 * lifecycle; it does <b>not</b> mint it.
035 * <p>
036 * A client-generated key is not the security property.  A globally-unique key looked up <i>only by key</i> would let
037 * a recorded terminal outcome for incident&nbsp;A be replayed under a submit for incident&nbsp;B &mdash; returning
038 * A's "success" for a write B never performed, and resurrecting the "resolve an incident that was re-triggered in the
039 * interim" case.  This key is therefore bound to {@code (action, targetId)} at mint time: a submit whose
040 * {@code (action, targetId)} does not {@link #matches(String, String) match} the binding is a <b>named refusal</b>,
041 * never a replayed success.
042 *
043 * <h5 class='section'>Unforgeability</h5>
044 * <p>
045 * The key value is {@value #KEY_BYTES} random bytes ({@value #KEY_BITS}&nbsp;bits) from {@link SecureRandom},
046 * hex-encoded &mdash; well beyond the ≥128-bit bar, matching {@link org.apache.juneau.rest.server.filter.SynchronizerToken}'s
047 * 256-bit reference.  The binding is what makes it safe; the width is what makes it unguessable.
048 *
049 * <h5 class='section'>Example &mdash; the artifact-bound pattern (the default):</h5>
050 * <p class='bjava'>
051 *    <jc>// At modal-open: mint a key bound to this action + this row.</jc>
052 *    IdempotencyKey <jv>key</jv> = IdempotencyKey.<jsm>mint</jsm>(<js>"ack"</js>, <jv>incidentId</jv>);
053 *    <jv>modal</jv>.idempotencyKey(<jv>key</jv>.value());
054 *
055 *    <jc>// At submit: rehydrate the recorded binding and check it against the submitted (action, targetId).</jc>
056 *    <jk>if</jk> (! <jv>recorded</jv>.matches(<jv>submittedAction</jv>, <jv>submittedTargetId</jv>))
057 *       <jk>return</jk> ActionResult.<jsm>refusal</jsm>(<js>"write-guard:idempotency-binding-mismatch"</js>);
058 * </p>
059 *
060 * <h5 class='section'>The row-less, self-targeted pattern (the opt-in)</h5>
061 * <p>
062 * A dialog opened from a view's <b>ribbon</b> has no row, so at mint time there is no artifact id to bind to.
063 * {@link #mintSelfTargeted(String)} mints a key whose {@link #targetId()} <b>is its own {@link #value()}</b>, and
064 * the client sends that value back as the submitted target &mdash; so {@link #matches(String, String)} still has a
065 * real, agreed binding to check rather than a null target.
066 *
067 * <p>
068 * <b>Two call sites have to honour one pairing contract, and it is not enforceable by either alone:</b>
069 * <ul class='spaced-list'>
070 *    <li>A {@link #mintSelfTargeted(String)} key MUST be carried by a modal that also sets its
071 *       {@code selfTargeted} flag &mdash; that flag is what tells the client to send the key's own value as the
072 *       target.  Without it the client sends the row's id (or nothing at all, row-less), and the key's binding can
073 *       never match.
074 *    <li>A {@link #mint(String, String)} key must <b>NEVER</b> be paired with {@code selfTargeted} &mdash; that
075 *       pairing makes the client stop sending the real target the key was bound to, silently discarding the artifact
076 *       identity and making {@link #matches(String, String)} vacuous.  This is exactly why the flag is an explicit
077 *       per-dialog opt-in rather than a blanket client-side precedence rule.
078 * </ul>
079 *
080 * <h5 class='section'>Example &mdash; the self-targeted pattern:</h5>
081 * <p class='bjava'>
082 *    <jc>// At modal-open on a ribbon-hosted dialog: there is no row, so the key targets itself.</jc>
083 *    IdempotencyKey <jv>key</jv> = IdempotencyKey.<jsm>mintSelfTargeted</jsm>(<js>"create"</js>);
084 *    <jv>modal</jv>.idempotencyKey(<jv>key</jv>.value()).selfTargeted(<jk>true</jk>);
085 *
086 *    <jc>// At submit: the submitted target IS the key value the client echoed back.</jc>
087 *    <jk>if</jk> (! <jv>recorded</jv>.matches(<jv>submittedAction</jv>, <jv>submittedTargetId</jv>))
088 *       <jk>return</jk> ActionResult.<jsm>refusal</jsm>(<js>"write-guard:idempotency-binding-mismatch"</js>);
089 * </p>
090 *
091 * <h5 class='section'>What a self-targeted key does and does not guarantee</h5>
092 * <p>
093 * A self-targeted key binds to {@code (action, one-opened-dialog)} &mdash; <b>not</b> {@code (action, artifact)}.
094 * It is minted per modal-open, and it is <b>single-execution, replay-on-duplicate</b>: it stops one opened dialog's
095 * submission from being <i>re-executed</i>, and a duplicate submit <b>replays</b> the recorded outcome rather than
096 * failing.  It is <b>not</b> "single-use," and it does not answer <i>"is this submission for the artifact the key
097 * was minted for?"</i> &mdash; at mint time there is no artifact.
098 *
099 * <p>
100 * A self-targeted key must therefore <b>never</b> be described as equivalent to a row-scoped one.  A key minted via
101 * the unchanged {@link #mint(String, String)} binds to a real artifact identity, and that guarantee is strictly
102 * stronger; it stays available to any consumer that has an artifact to bind to.
103 *
104 * <h5 class='section'>See Also:</h5>
105 * <ul>
106 *    <li class='jc'>{@link org.apache.juneau.console.widgets.ModalDef}
107 *    <li class='jc'>{@link ActionResult}
108 *    <li class='jc'>{@link org.apache.juneau.rest.server.filter.SynchronizerToken}
109 *    <li class='jc'>{@link JobKeyLedger}
110 * </ul>
111 *
112 * @since 10.0.0
113 */
114@SuppressWarnings({
115   "java:S1192" // Duplicated "action" is a protocol key; a constant would obscure the wire contract.
116})
117public final class IdempotencyKey {
118
119   /** Width of a minted key value, in bits &mdash; well beyond the ≥128-bit bar. */
120   private static final int KEY_BITS = 256;
121
122   /** Number of random bytes behind a minted key value, derived from {@link #KEY_BITS}. */
123   private static final int KEY_BYTES = KEY_BITS / 8;
124
125   /** Shared secure random source for minting key values; {@link SecureRandom} is safe for concurrent use. */
126   private static final SecureRandom RANDOM = new SecureRandom();
127
128   private final String value;
129   private final String action;
130   private final String targetId;
131
132   private IdempotencyKey(String value, String action, String targetId) {
133      this.value = value;
134      this.action = action;
135      this.targetId = targetId;
136   }
137
138   /**
139    * Mints a fresh key bound to the specified {@code (action, targetId)} pair.
140    *
141    * @param action The stable action id this key is issued for.  Must not be <jk>null</jk> or blank.
142    * @param targetId The id of the row/target this key is issued for.  Must not be <jk>null</jk> or blank.
143    * @return A new key holding a fresh {@value #KEY_BITS}-bit secret, hex-encoded, bound to {@code (action, targetId)}.
144    * @throws IllegalArgumentException If {@code action} or {@code targetId} is <jk>null</jk> or blank.
145    */
146   public static IdempotencyKey mint(String action, String targetId) {
147      requireNonBlank("action", action);
148      requireNonBlank("targetId", targetId);
149      var bytes = new byte[KEY_BYTES];
150      RANDOM.nextBytes(bytes);
151      return new IdempotencyKey(HexFormat.of().formatHex(bytes), action, targetId);
152   }
153
154   /**
155    * Mints a fresh key that is bound to <b>itself</b>: its {@link #targetId()} is its own {@link #value()}.
156    *
157    * <p>
158    * For a dialog opened where there is no artifact to bind to &mdash; a ribbon-hosted, row-less dialog whose
159    * submit creates the thing it is about.  {@link #mint(String, String)} cannot express this, since it requires a
160    * non-blank target that does not exist yet, and there is nothing gained by minting a <i>second</i> random string
161    * to serve as the target: one opaque, unguessable value plays both roles, and the client already has it as the
162    * modal's carried key.
163    *
164    * <p>
165    * <b>The pairing contract is not optional</b> &mdash; a key from this factory must be carried by a modal that
166    * also sets its {@code selfTargeted} flag, or the client will not send the key's value back as the submitted
167    * target and the binding can never match.  Conversely, {@code selfTargeted} must never be set on a modal
168    * carrying a {@link #mint(String, String)} key.  See the class Javadoc, which also states what this key does
169    * and does not guarantee (single-execution replay protection for one opened dialog &mdash; not artifact
170    * identity).
171    *
172    * <p>
173    * {@link #mint(String, String)}, {@link #of(String, String, String)} and {@link #matches(String, String)} are
174    * unchanged by this capability: it arrives entirely as this additional factory.
175    *
176    * @param action The stable action id this key is issued for.  Must not be <jk>null</jk> or blank.
177    * @return A new key holding a fresh {@value #KEY_BITS}-bit secret, hex-encoded, bound to that same value as its target.
178    * @throws IllegalArgumentException If {@code action} is <jk>null</jk> or blank.
179    */
180   public static IdempotencyKey mintSelfTargeted(String action) {
181      requireNonBlank("action", action);
182      var bytes = new byte[KEY_BYTES];
183      RANDOM.nextBytes(bytes);
184      var value = HexFormat.of().formatHex(bytes);
185      return new IdempotencyKey(value, action, value);
186   }
187
188   /**
189    * Rehydrates a previously-minted key from its recorded value and binding.
190    *
191    * <p>
192    * Intended for the submit path: the consumer looks up the recorded {@code (value, action, targetId)} it minted at
193    * modal-open and reconstructs this to {@link #matches(String, String) check} it against the submitted pair.
194    *
195    * @param value The recorded key value.  Must not be <jk>null</jk> or blank.
196    * @param action The action id the key was bound to at mint time.  Must not be <jk>null</jk> or blank.
197    * @param targetId The target id the key was bound to at mint time.  Must not be <jk>null</jk> or blank.
198    * @return A key holding the recorded value and binding.
199    * @throws IllegalArgumentException If any argument is <jk>null</jk> or blank.
200    */
201   public static IdempotencyKey of(String value, String action, String targetId) {
202      requireNonBlank("value", value);
203      requireNonBlank("action", action);
204      requireNonBlank("targetId", targetId);
205      return new IdempotencyKey(value, action, targetId);
206   }
207
208   /**
209    * The opaque key value, for carrying in the modal and echoing back on the submit.
210    *
211    * @return The key value.  Never <jk>null</jk> or blank.
212    */
213   public String value() { return value; }
214
215   /**
216    * The action id this key is bound to.
217    *
218    * @return The bound action id.
219    */
220   public String action() { return action; }
221
222   /**
223    * The target id this key is bound to.
224    *
225    * @return The bound target id.
226    */
227   public String targetId() { return targetId; }
228
229   /**
230    * Whether a submitted {@code (action, targetId)} matches this key's binding.
231    *
232    * <p>
233    * A mismatch must be answered with a <b>named refusal</b>, never a replayed success: it is a key being submitted
234    * against a different action or a different target than it was minted for.  Compares the target id in constant
235    * time so a mismatch cannot be probed character-by-character.
236    *
237    * @param submittedAction The action id presented by the submit.  Can be <jk>null</jk>.
238    * @param submittedTargetId The target id presented by the submit.  Can be <jk>null</jk>.
239    * @return <jk>true</jk> only if both the action and the target id match this key's binding exactly.
240    */
241   public boolean matches(String submittedAction, String submittedTargetId) {
242      if (submittedAction == null || submittedTargetId == null)
243         return false;
244      return constantTimeEquals(action, submittedAction) && constantTimeEquals(targetId, submittedTargetId);
245   }
246
247   private static boolean constantTimeEquals(String a, String b) {
248      return MessageDigest.isEqual(a.getBytes(StandardCharsets.UTF_8), b.getBytes(StandardCharsets.UTF_8));
249   }
250
251   private static void requireNonBlank(String name, String value) {
252      reqnn(name, value);
253      if (value.isBlank())
254         throw iaex("Argument ''%s'' must not be blank.", name);
255   }
256
257   /**
258    * Returns a description that does <b>not</b> include the key value.
259    *
260    * <p>
261    * The value is a secret; a bean-dumping logger or a debug view that stringifies it would otherwise write it
262    * somewhere it can be read back and replayed.  The binding is safe to show.
263    *
264    * @return A value-free description.
265    */
266   @Override /* Object */
267   public String toString() {
268      return "IdempotencyKey(value=<redacted>,action=" + action + ",targetId=" + targetId + ")";
269   }
270}