/* * Copyright (C) 2015 Square, Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package keywhiz.service.daos; import com.fasterxml.jackson.databind.ObjectMapper; import com.google.common.collect.ImmutableMap; import java.util.AbstractMap.SimpleEntry; import java.util.Optional; import javax.inject.Inject; import javax.ws.rs.NotFoundException; import keywhiz.KeywhizTestRunner; import keywhiz.api.ApiDate; import keywhiz.api.automation.v2.PartialUpdateSecretRequestV2; import keywhiz.api.model.SecretContent; import keywhiz.api.model.SecretSeries; import keywhiz.api.model.SecretSeriesAndContent; import keywhiz.service.crypto.ContentCryptographer; import keywhiz.service.crypto.CryptoFixtures; import keywhiz.service.daos.SecretDAO.SecretDAOFactory; import org.jooq.DSLContext; import org.jooq.Table; import org.jooq.exception.DataAccessException; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; import static java.nio.charset.StandardCharsets.UTF_8; import static keywhiz.jooq.tables.Secrets.SECRETS; import static keywhiz.jooq.tables.SecretsContent.SECRETS_CONTENT; import static org.assertj.core.api.Assertions.assertThat; @RunWith(KeywhizTestRunner.class) public class SecretDAOTest { @Inject private DSLContext jooqContext; @Inject private ObjectMapper objectMapper; @Inject private SecretDAOFactory secretDAOFactory; private final static ContentCryptographer cryptographer = CryptoFixtures.contentCryptographer(); private final static ApiDate date = ApiDate.now(); private ImmutableMap<String, String> emptyMetadata = ImmutableMap.of(); private SecretSeries series1 = SecretSeries.of(1, "secret1", "desc1", date, "creator", date, "updater", null, null, 101L); private String content = "c2VjcmV0MQ=="; private String encryptedContent = cryptographer.encryptionKeyDerivedFrom(series1.name()).encrypt(content); private SecretContent content1 = SecretContent.of(101, 1, encryptedContent, "checksum", date, "creator", date, "updater", emptyMetadata, 0); private SecretSeriesAndContent secret1 = SecretSeriesAndContent.of(series1, content1); private SecretSeries series2 = SecretSeries.of(2, "secret2", "desc2", date, "creator", date, "updater", null, null, 103L); private SecretContent content2a = SecretContent.of(102, 2, encryptedContent, "checksum", date, "creator", date, "updater", emptyMetadata, 0); private SecretSeriesAndContent secret2a = SecretSeriesAndContent.of(series2, content2a); private SecretContent content2b = SecretContent.of(103, 2, "some other content", "checksum", date, "creator", date, "updater", emptyMetadata, 0); private SecretSeriesAndContent secret2b = SecretSeriesAndContent.of(series2, content2b); private SecretSeries series3 = SecretSeries.of(3, "secret3", "desc3", date, "creator", date, "updater", null, null, null); private SecretContent content3 = SecretContent.of(104, 3, encryptedContent, "checksum", date, "creator", date, "updater", emptyMetadata, 0); private SecretSeriesAndContent secret3 = SecretSeriesAndContent.of(series3, content3); private SecretDAO secretDAO; @Before public void setUp() throws Exception { jooqContext.insertInto(SECRETS) .set(SECRETS.ID, series1.id()) .set(SECRETS.NAME, series1.name()) .set(SECRETS.DESCRIPTION, series1.description()) .set(SECRETS.CREATEDBY, series1.createdBy()) .set(SECRETS.CREATEDAT, series1.createdAt().toEpochSecond()) .set(SECRETS.UPDATEDBY, series1.updatedBy()) .set(SECRETS.UPDATEDAT, series1.updatedAt().toEpochSecond()) .set(SECRETS.CURRENT, series1.currentVersion().orElse(null)) .execute(); jooqContext.insertInto(SECRETS_CONTENT) .set(SECRETS_CONTENT.ID, secret1.content().id()) .set(SECRETS_CONTENT.SECRETID, secret1.series().id()) .set(SECRETS_CONTENT.ENCRYPTED_CONTENT, secret1.content().encryptedContent()) .set(SECRETS_CONTENT.CONTENT_HMAC, "checksum") .set(SECRETS_CONTENT.CREATEDBY, secret1.content().createdBy()) .set(SECRETS_CONTENT.CREATEDAT, secret1.content().createdAt().toEpochSecond()) .set(SECRETS_CONTENT.UPDATEDBY, secret1.content().updatedBy()) .set(SECRETS_CONTENT.UPDATEDAT, secret1.content().updatedAt().toEpochSecond()) .set(SECRETS_CONTENT.METADATA, objectMapper.writeValueAsString(secret1.content().metadata())) .execute(); jooqContext.insertInto(SECRETS) .set(SECRETS.ID, series2.id()) .set(SECRETS.NAME, series2.name()) .set(SECRETS.DESCRIPTION, series2.description()) .set(SECRETS.CREATEDBY, series2.createdBy()) .set(SECRETS.CREATEDAT, series2.createdAt().toEpochSecond()) .set(SECRETS.UPDATEDBY, series2.updatedBy()) .set(SECRETS.UPDATEDAT, series2.updatedAt().toEpochSecond()) .set(SECRETS.CURRENT, series2.currentVersion().orElse(null)) .execute(); jooqContext.insertInto(SECRETS_CONTENT) .set(SECRETS_CONTENT.ID, secret2a.content().id()) .set(SECRETS_CONTENT.SECRETID, secret2a.series().id()) .set(SECRETS_CONTENT.ENCRYPTED_CONTENT, secret2a.content().encryptedContent()) .set(SECRETS_CONTENT.CONTENT_HMAC, "checksum") .set(SECRETS_CONTENT.CREATEDBY, secret2a.content().createdBy()) .set(SECRETS_CONTENT.CREATEDAT, secret2a.content().createdAt().toEpochSecond()) .set(SECRETS_CONTENT.UPDATEDBY, secret2a.content().updatedBy()) .set(SECRETS_CONTENT.UPDATEDAT, secret2a.content().updatedAt().toEpochSecond()) .set(SECRETS_CONTENT.METADATA, objectMapper.writeValueAsString(secret2a.content().metadata())) .execute(); jooqContext.insertInto(SECRETS_CONTENT) .set(SECRETS_CONTENT.ID, secret2b.content().id()) .set(SECRETS_CONTENT.SECRETID, secret2b.series().id()) .set(SECRETS_CONTENT.ENCRYPTED_CONTENT, secret2b.content().encryptedContent()) .set(SECRETS_CONTENT.CONTENT_HMAC, "checksum") .set(SECRETS_CONTENT.CREATEDBY, secret2b.content().createdBy()) .set(SECRETS_CONTENT.CREATEDAT, secret2b.content().createdAt().toEpochSecond()) .set(SECRETS_CONTENT.UPDATEDBY, secret2b.content().updatedBy()) .set(SECRETS_CONTENT.UPDATEDAT, secret2b.content().updatedAt().toEpochSecond()) .set(SECRETS_CONTENT.METADATA, objectMapper.writeValueAsString(secret2b.content().metadata())) .execute(); jooqContext.insertInto(SECRETS) .set(SECRETS.ID, series3.id()) .set(SECRETS.NAME, series3.name()) .set(SECRETS.DESCRIPTION, series3.description()) .set(SECRETS.CREATEDBY, series3.createdBy()) .set(SECRETS.CREATEDAT, series3.createdAt().toEpochSecond()) .set(SECRETS.UPDATEDBY, series3.updatedBy()) .set(SECRETS.UPDATEDAT, series3.updatedAt().toEpochSecond()) .set(SECRETS.CURRENT, series3.currentVersion().orElse(null)) .execute(); jooqContext.insertInto(SECRETS_CONTENT) .set(SECRETS_CONTENT.ID, secret3.content().id()) .set(SECRETS_CONTENT.SECRETID, secret3.series().id()) .set(SECRETS_CONTENT.ENCRYPTED_CONTENT, secret3.content().encryptedContent()) .set(SECRETS_CONTENT.CONTENT_HMAC, "checksum") .set(SECRETS_CONTENT.CREATEDBY, secret3.content().createdBy()) .set(SECRETS_CONTENT.CREATEDAT, secret3.content().createdAt().toEpochSecond()) .set(SECRETS_CONTENT.UPDATEDBY, secret3.content().updatedBy()) .set(SECRETS_CONTENT.UPDATEDAT, secret3.content().updatedAt().toEpochSecond()) .set(SECRETS_CONTENT.METADATA, objectMapper.writeValueAsString(secret3.content().metadata())) .execute(); secretDAO = secretDAOFactory.readwrite(); } //--------------------------------------------------------------------------------------- // createSecret //--------------------------------------------------------------------------------------- @Test public void createSecret() { int secretsBefore = tableSize(SECRETS); int secretContentsBefore = tableSize(SECRETS_CONTENT); String name = "newSecret"; String content = "c2VjcmV0MQ=="; String hmac = cryptographer.computeHmac(content.getBytes(UTF_8)); String encryptedContent = cryptographer.encryptionKeyDerivedFrom(name).encrypt(content); long newId = secretDAO.createSecret(name, encryptedContent, hmac, "creator", ImmutableMap.of(), 0, "", null, ImmutableMap.of()); SecretSeriesAndContent newSecret = secretDAO.getSecretById(newId).get(); assertThat(tableSize(SECRETS)).isEqualTo(secretsBefore + 1); assertThat(tableSize(SECRETS_CONTENT)).isEqualTo(secretContentsBefore + 1); newSecret = secretDAO.getSecretByName(newSecret.series().name()).get(); assertThat(secretDAO.getSecrets(null, null)).containsOnly(secret1, secret2b, newSecret); } @Test(expected = DataAccessException.class) public void createSecretFailsIfSecretExists() { String name = "newSecret"; secretDAO.createSecret(name, "some secret", "checksum", "creator", ImmutableMap.of(), 0, "", null, ImmutableMap.of()); secretDAO.createSecret(name, "some secret", "checksum", "creator", ImmutableMap.of(), 0, "", null, ImmutableMap.of()); } @Test public void createSecretSucceedsIfCurrentVersionIsNull() { String name = "newSecret"; long firstId = secretDAO.createSecret(name, "content1", cryptographer.computeHmac("content1".getBytes(UTF_8)), "creator1", ImmutableMap.of("foo", "bar"), 1000, "description1", "type1", ImmutableMap.of()); jooqContext.update(SECRETS) .set(SECRETS.CURRENT, (Long)null) .where(SECRETS.ID.eq(firstId)) .execute(); long secondId = secretDAO.createSecret(name, "content2", cryptographer.computeHmac("content2".getBytes(UTF_8)), "creator2", ImmutableMap.of("foo2", "bar2"), 2000, "description2", "type2", ImmutableMap.of()); assertThat(secondId).isGreaterThan(firstId); SecretSeriesAndContent newSecret = secretDAO.getSecretById(secondId).get(); assertThat(newSecret.series().createdBy()).isEqualTo("creator2"); assertThat(newSecret.series().updatedBy()).isEqualTo("creator2"); assertThat(newSecret.series().description()).isEqualTo("description2"); assertThat(newSecret.series().type().get()).isEqualTo("type2"); assertThat(newSecret.content().createdBy()).isEqualTo("creator2"); assertThat(newSecret.content().encryptedContent()).isEqualTo("content2"); assertThat(newSecret.content().metadata()).isEqualTo(ImmutableMap.of("foo2", "bar2")); } //--------------------------------------------------------------------------------------- // createOrUpdateSecret //--------------------------------------------------------------------------------------- @Test public void createOrUpdateSecretWhenSecretDoesNotExist() { int secretsBefore = tableSize(SECRETS); int secretContentsBefore = tableSize(SECRETS_CONTENT); String name = "newSecret"; String content = "c2VjcmV0MQ=="; String hmac = cryptographer.computeHmac(content.getBytes(UTF_8)); String encryptedContent = cryptographer.encryptionKeyDerivedFrom(name).encrypt(content); long newId = secretDAO.createOrUpdateSecret(name, encryptedContent, hmac, "creator", ImmutableMap.of(), 0, "", null, ImmutableMap.of()); SecretSeriesAndContent newSecret = secretDAO.getSecretById(newId).get(); assertThat(tableSize(SECRETS)).isEqualTo(secretsBefore + 1); assertThat(tableSize(SECRETS_CONTENT)).isEqualTo(secretContentsBefore + 1); newSecret = secretDAO.getSecretByName(newSecret.series().name()).get(); assertThat(secretDAO.getSecrets(null, null)).containsOnly(secret1, secret2b, newSecret); } @Test public void createOrUpdateSecretWhenSecretExists() { String name = "newSecret"; long firstId = secretDAO.createSecret(name, "content1", cryptographer.computeHmac("content1".getBytes(UTF_8)), "creator1", ImmutableMap.of("foo", "bar"), 1000, "description1", "type1", ImmutableMap.of()); long secondId = secretDAO.createOrUpdateSecret(name, "content2", cryptographer.computeHmac("content2".getBytes(UTF_8)), "creator2", ImmutableMap.of("foo2", "bar2"), 2000, "description2", "type2", ImmutableMap.of()); assertThat(secondId).isEqualTo(firstId); SecretSeriesAndContent newSecret = secretDAO.getSecretById(firstId).get(); assertThat(newSecret.series().createdBy()).isEqualTo("creator1"); assertThat(newSecret.series().updatedBy()).isEqualTo("creator2"); assertThat(newSecret.series().description()).isEqualTo("description2"); assertThat(newSecret.series().type().get()).isEqualTo("type2"); assertThat(newSecret.content().createdBy()).isEqualTo("creator2"); assertThat(newSecret.content().encryptedContent()).isEqualTo("content2"); assertThat(newSecret.content().metadata()).isEqualTo(ImmutableMap.of("foo2", "bar2")); } //--------------------------------------------------------------------------------------- // updateSecret //--------------------------------------------------------------------------------------- @Test(expected = NotFoundException.class) public void partialUpdateSecretWhenSecretSeriesDoesNotExist() { String name = "newSecret"; String content = "c2VjcmV0MQ=="; PartialUpdateSecretRequestV2 request = PartialUpdateSecretRequestV2.builder().contentPresent(true).content(content).build(); secretDAO.partialUpdateSecret(name, "test", request); } @Test(expected = NotFoundException.class) public void partialUpdateSecretWhenSecretContentDoesNotExist() { String name = "newSecret"; String content = "c2VjcmV0MQ=="; PartialUpdateSecretRequestV2 request = PartialUpdateSecretRequestV2.builder().contentPresent(true).content(content).build(); jooqContext.insertInto(SECRETS) .set(SECRETS.ID, 12L) .set(SECRETS.NAME, name) .set(SECRETS.DESCRIPTION, series1.description()) .set(SECRETS.CREATEDBY, series1.createdBy()) .set(SECRETS.CREATEDAT, series1.createdAt().toEpochSecond()) .set(SECRETS.UPDATEDBY, series1.updatedBy()) .set(SECRETS.UPDATEDAT, series1.updatedAt().toEpochSecond()) .set(SECRETS.CURRENT, 12L) .execute(); secretDAO.partialUpdateSecret(name, "test", request); } @Test(expected = NotFoundException.class) public void partialUpdateSecretWhenSecretCurrentIsNotSet() { String content = "c2VjcmV0MQ=="; PartialUpdateSecretRequestV2 request = PartialUpdateSecretRequestV2.builder().contentPresent(true).content(content).build(); secretDAO.partialUpdateSecret(series3.name(), "test", request); } @Test public void partialUpdateSecretWhenSecretExists() { // Update the content and set the type for series1 long id = secretDAO.partialUpdateSecret(series1.name(), "creator1", PartialUpdateSecretRequestV2.builder() .contentPresent(true) .content("content1") .typePresent(true) .type("type1") .build()); SecretSeriesAndContent newSecret = secretDAO.getSecretById(id).orElseThrow(IllegalStateException::new); assertThat(newSecret.series().createdBy()).isEqualTo("creator"); assertThat(newSecret.series().updatedBy()).isEqualTo("creator1"); assertThat(newSecret.series().description()).isEqualTo(series1.description()); assertThat(newSecret.series().type().get()).isEqualTo("type1"); assertThat(newSecret.content().createdBy()).isEqualTo("creator1"); assertThat(newSecret.content().hmac()).isEqualTo( cryptographer.computeHmac("content1".getBytes(UTF_8))); assertThat(newSecret.content().metadata()).isEqualTo(secret1.content().metadata()); assertThat(newSecret.content().expiry()).isEqualTo(secret1.content().expiry()); // Update the expiry and metadata for series2 id = secretDAO.partialUpdateSecret(series2.name(), "creator2", PartialUpdateSecretRequestV2.builder() .expiryPresent(true) .expiry(12345L) .metadataPresent(true) .metadata(ImmutableMap.of("owner", "keywhiz-test")) .build()); newSecret = secretDAO.getSecretById(id).orElseThrow(IllegalStateException::new); assertThat(newSecret.series().createdBy()).isEqualTo("creator"); assertThat(newSecret.series().updatedBy()).isEqualTo("creator2"); assertThat(newSecret.series().description()).isEqualTo(series2.description()); assertThat(newSecret.content().createdBy()).isEqualTo("creator2"); assertThat(newSecret.content().hmac()).isEqualTo("checksum"); assertThat(newSecret.content().metadata()).isEqualTo(ImmutableMap.of("owner", "keywhiz-test")); assertThat(newSecret.content().expiry()).isEqualTo(12345L); } //--------------------------------------------------------------------------------------- @Test public void getSecretByName() { String name = secret2b.series().name(); assertThat(secretDAO.getSecretByName(name)).contains(secret2b); } @Test public void getSecretByNameOneReturnsEmptyWhenCurrentVersionIsNull() { String name = secret1.series().name(); jooqContext.update(SECRETS) .set(SECRETS.CURRENT, (Long)null) .where(SECRETS.ID.eq(series1.id())) .execute(); assertThat(secretDAO.getSecretByName(name)).isEmpty(); } @Test public void getSecretByNameOneReturnsEmptyWhenRowIsMissing() { String name = "nonExistantSecret"; assertThat(secretDAO.getSecretByName(name).isPresent()).isFalse(); long newId = secretDAO.createSecret(name, "content", cryptographer.computeHmac("content".getBytes(UTF_8)), "creator", ImmutableMap.of(), 0, "", null, ImmutableMap.of()); SecretSeriesAndContent newSecret = secretDAO.getSecretById(newId).get(); assertThat(secretDAO.getSecretByName(name).isPresent()).isTrue(); jooqContext.deleteFrom(SECRETS_CONTENT) .where(SECRETS_CONTENT.ID.eq(newSecret.content().id())) .execute(); assertThat(secretDAO.getSecretByName(name).isPresent()).isFalse(); } @Test public void getSecretById() { assertThat(secretDAO.getSecretById(series2.id())).isEqualTo(Optional.of(secret2b)); } @Test public void getSecretByIdOneReturnsEmptyWhenCurrentVersionIsNull() { jooqContext.update(SECRETS) .set(SECRETS.CURRENT, (Long)null) .where(SECRETS.ID.eq(series2.id())) .execute(); assertThat(secretDAO.getSecretById(series2.id())).isEmpty(); } @Test(expected = IllegalStateException.class) public void getSecretByIdOneThrowsExceptionIfCurrentVersionIsInvalid() { jooqContext.update(SECRETS) .set(SECRETS.CURRENT, -1234L) .where(SECRETS.ID.eq(series2.id())) .execute(); secretDAO.getSecretById(series2.id()); } @Test public void getNonExistentSecret() { assertThat(secretDAO.getSecretByName("non-existent")).isEmpty(); assertThat(secretDAO.getSecretById(-1231)).isEmpty(); } @Test public void getSecrets() { assertThat(secretDAO.getSecrets(null, null)).containsOnly(secret1, secret2b); } @Test public void getSecretsByNameOnly() { assertThat(secretDAO.getSecretsNameOnly()).containsOnly( new SimpleEntry<>(series1.id(), series1.name()), new SimpleEntry<>(series2.id(), series2.name())); } @Test public void deleteSecretsByName() { secretDAO.createSecret("toBeDeleted_deleteSecretsByName", "encryptedShhh", cryptographer.computeHmac("encryptedShhh".getBytes(UTF_8)), "creator", ImmutableMap.of(), 0, "", null, null); secretDAO.deleteSecretsByName("toBeDeleted_deleteSecretsByName"); Optional<SecretSeriesAndContent> secret = secretDAO.getSecretByName("toBeDeleted_deleteSecretsByName"); assertThat(secret.isPresent()).isFalse(); } private int tableSize(Table table) { return jooqContext.fetchCount(table); } }