|  | @@ -0,0 +1,249 @@
 | 
	
		
			
				|  |  | +/*
 | 
	
		
			
				|  |  | + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
 | 
	
		
			
				|  |  | + * or more contributor license agreements. Licensed under the Elastic License
 | 
	
		
			
				|  |  | + * 2.0 and the Server Side Public License, v 1; you may not use this file except
 | 
	
		
			
				|  |  | + * in compliance with, at your election, the Elastic License 2.0 or the Server
 | 
	
		
			
				|  |  | + * Side Public License, v 1.
 | 
	
		
			
				|  |  | + */
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +package org.elasticsearch.common.settings;
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +import org.elasticsearch.core.TimeValue;
 | 
	
		
			
				|  |  | +import org.elasticsearch.test.ESTestCase;
 | 
	
		
			
				|  |  | +import org.mockito.stubbing.Answer;
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +import java.time.Instant;
 | 
	
		
			
				|  |  | +import java.util.HashSet;
 | 
	
		
			
				|  |  | +import java.util.Set;
 | 
	
		
			
				|  |  | +import java.util.concurrent.CountDownLatch;
 | 
	
		
			
				|  |  | +import java.util.concurrent.atomic.AtomicBoolean;
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +import static org.mockito.Mockito.mock;
 | 
	
		
			
				|  |  | +import static org.mockito.Mockito.when;
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +public class RotatableSecretTests extends ESTestCase {
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +    private final SecureString secret1 = new SecureString(randomAlphaOfLength(10));
 | 
	
		
			
				|  |  | +    private final SecureString secret2 = new SecureString(randomAlphaOfLength(10));
 | 
	
		
			
				|  |  | +    private final SecureString secret3 = new SecureString(randomAlphaOfLength(10));
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +    public void testBasicRotation() throws Exception {
 | 
	
		
			
				|  |  | +        // initial state
 | 
	
		
			
				|  |  | +        RotatableSecret rotatableSecret = new RotatableSecret(secret1);
 | 
	
		
			
				|  |  | +        assertTrue(rotatableSecret.matches(secret1));
 | 
	
		
			
				|  |  | +        assertFalse(rotatableSecret.matches(secret2));
 | 
	
		
			
				|  |  | +        assertFalse(rotatableSecret.matches(new SecureString(randomAlphaOfLength(10))));
 | 
	
		
			
				|  |  | +        assertTrue(rotatableSecret.isSet());
 | 
	
		
			
				|  |  | +        assertEquals(secret1, rotatableSecret.getSecrets().current());
 | 
	
		
			
				|  |  | +        assertNull(rotatableSecret.getSecrets().prior());
 | 
	
		
			
				|  |  | +        assertEquals(Instant.EPOCH, rotatableSecret.getSecrets().priorValidTill());
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +        // normal rotation
 | 
	
		
			
				|  |  | +        TimeValue expiresIn = TimeValue.timeValueDays(1);
 | 
	
		
			
				|  |  | +        rotatableSecret.rotate(secret2, expiresIn);
 | 
	
		
			
				|  |  | +        assertTrue(rotatableSecret.matches(secret1));
 | 
	
		
			
				|  |  | +        assertTrue(rotatableSecret.matches(secret2));
 | 
	
		
			
				|  |  | +        assertFalse(rotatableSecret.matches(new SecureString(randomAlphaOfLength(10))));
 | 
	
		
			
				|  |  | +        assertTrue(rotatableSecret.isSet());
 | 
	
		
			
				|  |  | +        assertEquals(secret2, rotatableSecret.getSecrets().current());
 | 
	
		
			
				|  |  | +        assertEquals(secret1, rotatableSecret.getSecrets().prior());
 | 
	
		
			
				|  |  | +        assertTrue(rotatableSecret.getSecrets().priorValidTill().isAfter(Instant.now()));
 | 
	
		
			
				|  |  | +        assertTrue(
 | 
	
		
			
				|  |  | +            rotatableSecret.getSecrets().priorValidTill().isBefore(Instant.now().plusMillis(TimeValue.timeValueDays(2).getMillis()))
 | 
	
		
			
				|  |  | +        );
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +        // attempt to rotate same value does nothing
 | 
	
		
			
				|  |  | +        rotatableSecret.rotate(secret2, TimeValue.timeValueDays(99)); // ignores the new expiry since you can't rotate the same secret
 | 
	
		
			
				|  |  | +        assertTrue(rotatableSecret.matches(secret1));
 | 
	
		
			
				|  |  | +        assertTrue(rotatableSecret.matches(secret2));
 | 
	
		
			
				|  |  | +        assertFalse(rotatableSecret.matches(new SecureString(randomAlphaOfLength(10))));
 | 
	
		
			
				|  |  | +        assertTrue(rotatableSecret.isSet());
 | 
	
		
			
				|  |  | +        assertEquals(secret2, rotatableSecret.getSecrets().current());
 | 
	
		
			
				|  |  | +        assertEquals(secret1, rotatableSecret.getSecrets().prior());
 | 
	
		
			
				|  |  | +        assertTrue(rotatableSecret.getSecrets().priorValidTill().isAfter(Instant.now()));
 | 
	
		
			
				|  |  | +        assertTrue(
 | 
	
		
			
				|  |  | +            rotatableSecret.getSecrets().priorValidTill().isBefore(Instant.now().plusMillis(TimeValue.timeValueDays(2).getMillis()))
 | 
	
		
			
				|  |  | +        );
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +        // rotate with expiry
 | 
	
		
			
				|  |  | +        rotatableSecret.rotate(secret3, TimeValue.timeValueMillis(1));
 | 
	
		
			
				|  |  | +        Thread.sleep(2); // ensure secret2 is expired
 | 
	
		
			
				|  |  | +        assertTrue(rotatableSecret.matches(secret3));
 | 
	
		
			
				|  |  | +        assertFalse(rotatableSecret.matches(secret1));
 | 
	
		
			
				|  |  | +        assertFalse(rotatableSecret.matches(secret2));
 | 
	
		
			
				|  |  | +        assertFalse(rotatableSecret.matches(new SecureString(randomAlphaOfLength(10))));
 | 
	
		
			
				|  |  | +        assertTrue(rotatableSecret.isSet());
 | 
	
		
			
				|  |  | +        assertEquals(secret3, rotatableSecret.getSecrets().current());
 | 
	
		
			
				|  |  | +        assertNull(rotatableSecret.getSecrets().prior());
 | 
	
		
			
				|  |  | +        assertTrue(rotatableSecret.getSecrets().priorValidTill().isBefore(Instant.now()));
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +        // unset current and prior
 | 
	
		
			
				|  |  | +        rotatableSecret.rotate(null, TimeValue.ZERO);
 | 
	
		
			
				|  |  | +        assertFalse(rotatableSecret.matches(secret3));
 | 
	
		
			
				|  |  | +        assertFalse(rotatableSecret.matches(secret1));
 | 
	
		
			
				|  |  | +        assertFalse(rotatableSecret.matches(secret2));
 | 
	
		
			
				|  |  | +        assertFalse(rotatableSecret.matches(new SecureString(randomAlphaOfLength(10))));
 | 
	
		
			
				|  |  | +        assertFalse(rotatableSecret.isSet());
 | 
	
		
			
				|  |  | +        assertNull(rotatableSecret.getSecrets().current());
 | 
	
		
			
				|  |  | +        assertNull(rotatableSecret.getSecrets().prior());
 | 
	
		
			
				|  |  | +        assertTrue(rotatableSecret.getSecrets().priorValidTill().isBefore(Instant.now()));
 | 
	
		
			
				|  |  | +    }
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +    public void testConcurrentReadWhileLocked() throws Exception {
 | 
	
		
			
				|  |  | +        // initial state
 | 
	
		
			
				|  |  | +        RotatableSecret rotatableSecret = new RotatableSecret(secret1);
 | 
	
		
			
				|  |  | +        assertTrue(rotatableSecret.matches(secret1));
 | 
	
		
			
				|  |  | +        assertFalse(rotatableSecret.matches(secret2));
 | 
	
		
			
				|  |  | +        assertEquals(secret1, rotatableSecret.getSecrets().current());
 | 
	
		
			
				|  |  | +        assertNull(rotatableSecret.getSecrets().prior());
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +        boolean expired = randomBoolean();
 | 
	
		
			
				|  |  | +        CountDownLatch latch = new CountDownLatch(1);
 | 
	
		
			
				|  |  | +        TimeValue mockGracePeriod = mock(TimeValue.class);  // use a mock to force a long rotation to exercise the concurrency
 | 
	
		
			
				|  |  | +        when(mockGracePeriod.getMillis()).then((Answer<Long>) invocation -> {
 | 
	
		
			
				|  |  | +            latch.await();
 | 
	
		
			
				|  |  | +            return expired ? 0L : Long.MAX_VALUE;
 | 
	
		
			
				|  |  | +        });
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +        // start writer thread
 | 
	
		
			
				|  |  | +        Thread t1 = new Thread(() -> rotatableSecret.rotate(secret2, mockGracePeriod));
 | 
	
		
			
				|  |  | +        t1.start();
 | 
	
		
			
				|  |  | +        assertBusy(() -> assertEquals(Thread.State.WAITING, t1.getState())); // waiting on countdown latch, holds write lock
 | 
	
		
			
				|  |  | +        assertTrue(rotatableSecret.isWriteLocked());
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +        // start reader threads
 | 
	
		
			
				|  |  | +        int readers = randomIntBetween(1, 16);
 | 
	
		
			
				|  |  | +        Set<Thread> readerThreads = new HashSet<>(readers);
 | 
	
		
			
				|  |  | +        for (int i = 0; i < readers; i++) {
 | 
	
		
			
				|  |  | +            Thread t = new Thread(() -> {
 | 
	
		
			
				|  |  | +                if (randomBoolean()) { // either matches or isSet can block
 | 
	
		
			
				|  |  | +                    if (expired) {
 | 
	
		
			
				|  |  | +                        assertFalse(rotatableSecret.matches(secret1));
 | 
	
		
			
				|  |  | +                    } else {
 | 
	
		
			
				|  |  | +                        assertTrue(rotatableSecret.matches(secret1));
 | 
	
		
			
				|  |  | +                    }
 | 
	
		
			
				|  |  | +                    assertTrue(rotatableSecret.matches(secret2));
 | 
	
		
			
				|  |  | +                } else {
 | 
	
		
			
				|  |  | +                    assertTrue(rotatableSecret.isSet());
 | 
	
		
			
				|  |  | +                }
 | 
	
		
			
				|  |  | +            });
 | 
	
		
			
				|  |  | +            readerThreads.add(t);
 | 
	
		
			
				|  |  | +            t.start();
 | 
	
		
			
				|  |  | +        }
 | 
	
		
			
				|  |  | +        for (Thread t : readerThreads) {
 | 
	
		
			
				|  |  | +            assertBusy(() -> assertEquals(Thread.State.WAITING, t.getState())); // waiting on write lock from thread 1 to be released
 | 
	
		
			
				|  |  | +        }
 | 
	
		
			
				|  |  | +        assertTrue(rotatableSecret.isWriteLocked());
 | 
	
		
			
				|  |  | +        latch.countDown(); // let thread1 finish, which also unblocks the reader threads
 | 
	
		
			
				|  |  | +        assertBusy(() -> assertEquals(Thread.State.TERMINATED, t1.getState())); // done with work
 | 
	
		
			
				|  |  | +        for (Thread t : readerThreads) {
 | 
	
		
			
				|  |  | +            assertBusy(() -> assertEquals(Thread.State.TERMINATED, t.getState())); // done with work
 | 
	
		
			
				|  |  | +            t.join();
 | 
	
		
			
				|  |  | +        }
 | 
	
		
			
				|  |  | +        t1.join();
 | 
	
		
			
				|  |  | +        assertFalse(rotatableSecret.isWriteLocked());
 | 
	
		
			
				|  |  | +    }
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +    public void testConcurrentRotations() throws Exception {
 | 
	
		
			
				|  |  | +        // initial state
 | 
	
		
			
				|  |  | +        RotatableSecret rotatableSecret = new RotatableSecret(secret1);
 | 
	
		
			
				|  |  | +        assertTrue(rotatableSecret.matches(secret1));
 | 
	
		
			
				|  |  | +        assertFalse(rotatableSecret.matches(secret2));
 | 
	
		
			
				|  |  | +        assertEquals(secret1, rotatableSecret.getSecrets().current());
 | 
	
		
			
				|  |  | +        assertNull(rotatableSecret.getSecrets().prior());
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +        // start first rotation
 | 
	
		
			
				|  |  | +        AtomicBoolean latch1 = new AtomicBoolean(false); // using boolean as latch to differentiate the kinds of waiting
 | 
	
		
			
				|  |  | +        TimeValue mockGracePeriod1 = mock(TimeValue.class);  // use a mock to force a long rotation to exercise the concurrency
 | 
	
		
			
				|  |  | +        when(mockGracePeriod1.getMillis()).then((Answer<Long>) invocation -> {
 | 
	
		
			
				|  |  | +            while (latch1.get() == false) {
 | 
	
		
			
				|  |  | +                Thread.sleep(10); // thread in TIMED_WAITING
 | 
	
		
			
				|  |  | +            }
 | 
	
		
			
				|  |  | +            return Long.MAX_VALUE;
 | 
	
		
			
				|  |  | +        });
 | 
	
		
			
				|  |  | +        Thread t1 = new Thread(() -> rotatableSecret.rotate(secret2, mockGracePeriod1));
 | 
	
		
			
				|  |  | +        t1.start();
 | 
	
		
			
				|  |  | +        assertBusy(() -> assertEquals(Thread.State.TIMED_WAITING, t1.getState())); // waiting on latch, holds write lock
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +        // start second rotation
 | 
	
		
			
				|  |  | +        AtomicBoolean latch2 = new AtomicBoolean(false);
 | 
	
		
			
				|  |  | +        TimeValue mockGracePeriod2 = mock(TimeValue.class);  // use a mock to force a long rotation to exercise the concurrency
 | 
	
		
			
				|  |  | +        when(mockGracePeriod2.getMillis()).then((Answer<Long>) invocation -> {
 | 
	
		
			
				|  |  | +            while (latch2.get() == false) {
 | 
	
		
			
				|  |  | +                Thread.sleep(10); // thread in TIMED_WAITING
 | 
	
		
			
				|  |  | +            }
 | 
	
		
			
				|  |  | +            return Long.MAX_VALUE;
 | 
	
		
			
				|  |  | +        });
 | 
	
		
			
				|  |  | +        Thread t2 = new Thread(() -> rotatableSecret.rotate(secret3, mockGracePeriod2));
 | 
	
		
			
				|  |  | +        t2.start();
 | 
	
		
			
				|  |  | +        assertBusy(() -> assertEquals(Thread.State.WAITING, t2.getState())); // waiting on write lock from thread 1
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +        // start third rotation
 | 
	
		
			
				|  |  | +        AtomicBoolean latch3 = new AtomicBoolean(false);
 | 
	
		
			
				|  |  | +        TimeValue mockGracePeriod3 = mock(TimeValue.class);  // use a mock to force a long rotation to exercise the concurrency
 | 
	
		
			
				|  |  | +        when(mockGracePeriod3.getMillis()).then((Answer<Long>) invocation -> {
 | 
	
		
			
				|  |  | +            while (latch3.get() == false) {
 | 
	
		
			
				|  |  | +                Thread.sleep(10); // thread in TIMED_WAITING
 | 
	
		
			
				|  |  | +            }
 | 
	
		
			
				|  |  | +            return Long.MAX_VALUE;
 | 
	
		
			
				|  |  | +        });
 | 
	
		
			
				|  |  | +        Thread t3 = new Thread(() -> rotatableSecret.rotate(null, mockGracePeriod3));
 | 
	
		
			
				|  |  | +        t3.start();
 | 
	
		
			
				|  |  | +        assertBusy(() -> assertEquals(Thread.State.WAITING, t3.getState())); // waiting on write lock from thread 1
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +        // initial state
 | 
	
		
			
				|  |  | +        assertEquals(rotatableSecret.getSecrets().current(), secret1);
 | 
	
		
			
				|  |  | +        assertNull(rotatableSecret.getSecrets().prior());
 | 
	
		
			
				|  |  | +        assertBusy(() -> assertEquals(Thread.State.TIMED_WAITING, t1.getState())); // waiting on latch
 | 
	
		
			
				|  |  | +        assertBusy(() -> assertEquals(Thread.State.WAITING, t2.getState())); // waiting on lock
 | 
	
		
			
				|  |  | +        assertBusy(() -> assertEquals(Thread.State.WAITING, t3.getState())); // waiting on lock
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +        latch1.set(true); // let first rotation succeed
 | 
	
		
			
				|  |  | +        assertBusy(() -> assertEquals(Thread.State.TERMINATED, t1.getState())); // work done
 | 
	
		
			
				|  |  | +        assertBusy(() -> assertEquals(Thread.State.TIMED_WAITING, t2.getState())); // waiting on latch
 | 
	
		
			
				|  |  | +        assertBusy(() -> assertEquals(Thread.State.WAITING, t3.getState()));  // waiting lock
 | 
	
		
			
				|  |  | +        assertEquals(rotatableSecret.getSecrets().current(), secret2);
 | 
	
		
			
				|  |  | +        assertEquals(rotatableSecret.getSecrets().prior(), secret1);
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +        latch2.set(true); // let second rotation succeed
 | 
	
		
			
				|  |  | +        assertBusy(() -> assertEquals(Thread.State.TERMINATED, t1.getState())); // work done
 | 
	
		
			
				|  |  | +        assertBusy(() -> assertEquals(Thread.State.TERMINATED, t2.getState())); // work done
 | 
	
		
			
				|  |  | +        assertBusy(() -> assertEquals(Thread.State.TIMED_WAITING, t3.getState())); // waiting on latch
 | 
	
		
			
				|  |  | +        assertEquals(rotatableSecret.getSecrets().current(), secret3);
 | 
	
		
			
				|  |  | +        assertEquals(rotatableSecret.getSecrets().prior(), secret2);
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +        latch3.set(true); // let third rotation succeed
 | 
	
		
			
				|  |  | +        assertBusy(() -> assertEquals(Thread.State.TERMINATED, t1.getState())); // work done
 | 
	
		
			
				|  |  | +        assertBusy(() -> assertEquals(Thread.State.TERMINATED, t2.getState())); // work done
 | 
	
		
			
				|  |  | +        assertBusy(() -> assertEquals(Thread.State.TERMINATED, t3.getState())); // work done
 | 
	
		
			
				|  |  | +        assertEquals(rotatableSecret.getSecrets().current(), null);
 | 
	
		
			
				|  |  | +        assertEquals(rotatableSecret.getSecrets().prior(), secret3);
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +        t1.join();
 | 
	
		
			
				|  |  | +        t2.join();
 | 
	
		
			
				|  |  | +        t3.join();
 | 
	
		
			
				|  |  | +    }
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +    public void testUnsetThenRotate() {
 | 
	
		
			
				|  |  | +        // it is not set on startup
 | 
	
		
			
				|  |  | +        RotatableSecret rotatableSecret = new RotatableSecret(null);
 | 
	
		
			
				|  |  | +        assertFalse(rotatableSecret.matches(new SecureString(randomAlphaOfLength(10))));
 | 
	
		
			
				|  |  | +        assertFalse(rotatableSecret.isSet());
 | 
	
		
			
				|  |  | +        assertNull(rotatableSecret.getSecrets().current());
 | 
	
		
			
				|  |  | +        assertNull(rotatableSecret.getSecrets().prior());
 | 
	
		
			
				|  |  | +        assertEquals(Instant.EPOCH, rotatableSecret.getSecrets().priorValidTill());
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +        // normal rotation for when it was not set on startup
 | 
	
		
			
				|  |  | +        TimeValue expiresIn = TimeValue.timeValueDays(1);
 | 
	
		
			
				|  |  | +        rotatableSecret.rotate(secret1, expiresIn);
 | 
	
		
			
				|  |  | +        assertTrue(rotatableSecret.matches(secret1));
 | 
	
		
			
				|  |  | +        assertFalse(rotatableSecret.matches(new SecureString(randomAlphaOfLength(10))));
 | 
	
		
			
				|  |  | +        assertTrue(rotatableSecret.isSet());
 | 
	
		
			
				|  |  | +        assertEquals(secret1, rotatableSecret.getSecrets().current());
 | 
	
		
			
				|  |  | +        assertNull(rotatableSecret.getSecrets().prior());
 | 
	
		
			
				|  |  | +        assertTrue(rotatableSecret.getSecrets().priorValidTill().isAfter(Instant.now()));
 | 
	
		
			
				|  |  | +        assertTrue(
 | 
	
		
			
				|  |  | +            rotatableSecret.getSecrets().priorValidTill().isBefore(Instant.now().plusMillis(TimeValue.timeValueDays(2).getMillis()))
 | 
	
		
			
				|  |  | +        );
 | 
	
		
			
				|  |  | +    }
 | 
	
		
			
				|  |  | +}
 |