/* * Copyright (c) 2016 Schibsted Products & Technology AS. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. */ package com.schibsted.security.strongbox.sdk.impl; import com.amazonaws.auth.AWSCredentialsProvider; import com.amazonaws.auth.DefaultAWSCredentialsProviderChain; import com.amazonaws.services.dynamodbv2.model.ResourceNotFoundException; import com.schibsted.security.strongbox.sdk.internal.access.IAMPolicyManager; import com.schibsted.security.strongbox.sdk.internal.encryption.Encryptor; import com.schibsted.security.strongbox.sdk.internal.encryption.FileEncryptionContext; import com.schibsted.security.strongbox.sdk.exceptions.AlreadyExistsException; import com.schibsted.security.strongbox.sdk.exceptions.DoesNotExistException; import com.schibsted.security.strongbox.sdk.exceptions.FailedToDeleteResourceException; import com.schibsted.security.strongbox.sdk.exceptions.SecretsGroupException; import com.schibsted.security.strongbox.sdk.exceptions.UnexpectedStateException; import com.schibsted.security.strongbox.sdk.exceptions.UnsupportedTypeException; import com.schibsted.security.strongbox.sdk.internal.srn.SecretsGroupSRN; import com.schibsted.security.strongbox.sdk.internal.impl.DefaultSecretsGroup; import com.schibsted.security.strongbox.sdk.internal.kv4j.generated.DynamoDB; import com.schibsted.security.strongbox.sdk.internal.kv4j.generated.File; import com.schibsted.security.strongbox.sdk.internal.kv4j.generated.Store; import com.schibsted.security.strongbox.sdk.types.*; import com.schibsted.security.strongbox.sdk.internal.types.config.UserConfig; import com.schibsted.security.strongbox.sdk.internal.encryption.KMSEncryptor; import com.schibsted.security.strongbox.sdk.SecretsGroup; import com.schibsted.security.strongbox.sdk.SecretsGroupManager; import com.schibsted.security.strongbox.sdk.internal.types.store.StorageType; import com.schibsted.security.strongbox.sdk.internal.types.store.DynamoDBReference; import com.schibsted.security.strongbox.sdk.internal.types.store.FileReference; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import com.schibsted.security.strongbox.sdk.internal.types.store.StorageReference; import java.util.ArrayList; import java.util.List; import java.util.Optional; import java.util.Set; import java.util.concurrent.Callable; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ExecutionException; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.Future; import java.util.concurrent.TimeUnit; import java.util.concurrent.locks.ReadWriteLock; import java.util.concurrent.locks.ReentrantReadWriteLock; /** * @author stiankri * @author kvlees * @author torarvid * @author hawkaa */ public class DefaultSecretsGroupManager implements SecretsGroupManager { private static final Logger log = LoggerFactory.getLogger(DefaultSecretsGroupManager.class); private final AWSCredentialsProvider awsCredentials; private final IAMPolicyManager policyManager; private final UserConfig userConfig; private final EncryptionStrength encryptionStrength; private final ClientConfiguration clientConfiguration; private final ConcurrentHashMap<SecretsGroupIdentifier, ReadWriteLock> readWriteLocks = new ConcurrentHashMap<>(); private final ConcurrentHashMap<SecretsGroupIdentifier, KMSEncryptor> encryptors = new ConcurrentHashMap<>(); public DefaultSecretsGroupManager() { this(new DefaultAWSCredentialsProviderChain()); } public DefaultSecretsGroupManager(AWSCredentialsProvider awsCredentials) { this(awsCredentials, new UserConfig(), EncryptionStrength.AES_128, getDefaultClientConfiguration()); } public DefaultSecretsGroupManager(AWSCredentialsProvider awsCredentials, UserConfig userConfig) { this(awsCredentials, userConfig, EncryptionStrength.AES_128, getDefaultClientConfiguration()); } public DefaultSecretsGroupManager(AWSCredentialsProvider awsCredentials, UserConfig userConfig, EncryptionStrength encryptionStrength) { this(awsCredentials, userConfig, encryptionStrength, getDefaultClientConfiguration()); } public DefaultSecretsGroupManager( AWSCredentialsProvider awsCredentials, UserConfig userConfig, EncryptionStrength encryptionStrength, ClientConfiguration clientConfiguration) { this.awsCredentials = awsCredentials; this.clientConfiguration = clientConfiguration; policyManager = IAMPolicyManager.fromCredentials(awsCredentials, this.clientConfiguration); this.userConfig = userConfig; this.encryptionStrength = encryptionStrength; } private static ClientConfiguration getDefaultClientConfiguration() { return new ClientConfiguration(); } public SRN srn(SecretsGroupIdentifier group) { return new SecretsGroupSRN(policyManager.getAccount(), group); } @Override public SecretsGroupInfo create(SecretsGroupIdentifier group) { return create(group, new DynamoDBReference()); } public SecretsGroupInfo create(SecretsGroupIdentifier group, StorageReference storageReference) { return create(group, storageReference, false); } public SecretsGroupInfo create(SecretsGroupIdentifier group, StorageReference storageReference, boolean allowExistingPendingDeletedOrDisabledKey) { synchronized (readWriteLocks) { ReadWriteLock readWriteLock = getReadWriteLock(group); readWriteLock.writeLock().lock(); try { final KMSEncryptor kmsEncryptor = getEncryptor(group); // This method is checking and creating resources in parallel, and since we have already taken the lock globally, // it is ok that the threads spawned from this method are not sharing the global lock. // TODO: consider rewriting the code so that the lock is used from this thread, making the local lock unnecessary. final ReadWriteLock localReadWriteLock = new ReentrantReadWriteLock(); verifyThatNonOfTheResourcesExistsOrThrow(group, kmsEncryptor, localReadWriteLock, allowExistingPendingDeletedOrDisabledKey); ExecutorService executor = Executors.newFixedThreadPool(6); try { Future<Store> storeFuture = executor.submit(() -> { Store store = createStore(group, storageReference, localReadWriteLock); setLocalState(group, storageReference); return store; }); Future<Void> encryptorFuture = executor.submit((Callable<Void>) () -> { kmsEncryptor.create(allowExistingPendingDeletedOrDisabledKey); return null; }); encryptorFuture.get(); final Store store = storeFuture.get(); Future<String> adminArnFuture = executor.submit(() -> policyManager.createAdminPolicy(group, kmsEncryptor, store)); Future<String> readOnlyArnFuture = executor.submit(() -> policyManager.createReadOnlyPolicy(group, kmsEncryptor, store)); String adminArn = adminArnFuture.get(); String readOnlyArn = readOnlyArnFuture.get(); SecretsGroupSRN secretsGroupSRN = new SecretsGroupSRN(policyManager.getAccount(), group); return new SecretsGroupInfo(secretsGroupSRN, Optional.of(kmsEncryptor.getArn()), Optional.of(store.getArn()), Optional.of(adminArn), Optional.of(readOnlyArn), new ArrayList<>(), new ArrayList<>()); } catch (InterruptedException | ExecutionException e) { throw new SecretsGroupException(group, "Failed to create group: this might have left a partially constructed group, which can be deleted.", e); } finally { executor.shutdownNow(); } } finally { readWriteLock.writeLock().unlock(); } } } private ReadWriteLock getReadWriteLock(SecretsGroupIdentifier group) { ReadWriteLock newLock = new ReentrantReadWriteLock(); ReadWriteLock existingLock = readWriteLocks.putIfAbsent(group, new ReentrantReadWriteLock()); return existingLock != null ? existingLock : newLock; } private void verifyThatNonOfTheResourcesExistsOrThrow(final SecretsGroupIdentifier group, final KMSEncryptor encryptor, final ReadWriteLock readWriteLock, boolean allowExistingPendingDeletedOrDisabledKey) { ExecutorService executor = Executors.newFixedThreadPool(4); try { Future<Optional<StorageType>> storageExistsFuture = executor.submit(() -> storageExists(group, readWriteLock)); Future<Boolean> encryptorExistsFuture = executor.submit(() -> encryptor.exists(allowExistingPendingDeletedOrDisabledKey)); Future<Boolean> adminPolicyExistsFuture = executor.submit(() -> policyManager.adminPolicyExists(group)); Future<Boolean> readOnlyPolicyExistsFuture = executor.submit(() -> policyManager.readOnlyPolicyExists(group)); Optional<StorageType> storageType = storageExistsFuture.get(); if (storageType.isPresent()) { throw new AlreadyExistsException(String.format("There already exists a storage backend for the group '%s' of type '%s'", group, storageType.get())); } if (adminPolicyExistsFuture.get()) { throw new AlreadyExistsException(String.format("There already exists an admin policy for the group '%s'", group)); } if (readOnlyPolicyExistsFuture.get()) { throw new AlreadyExistsException(String.format("There already exists a read only policy for the group '%s'", group)); } if (encryptorExistsFuture.get()) { throw new AlreadyExistsException(String.format("There already exists an encryptor backend for the group '%s'. Please note that it takes %d days for a key to be deleted. If you intend to reuse the key, use the '--allow-key-reuse' flag.", group, encryptor.pendingDeletionWindowInDays())); } } catch (AlreadyExistsException e) { throw new SecretsGroupException(group, "The group already exists", e); } catch (InterruptedException | ExecutionException e) { throw new SecretsGroupException(group, "Failed to verify if the group already exists", e); } finally { executor.shutdownNow(); } } // TODO: infer storage type: if not in overwrite, try DynamoDB then s3? pinning? private Store getCurrentStore(SecretsGroupIdentifier group, ReadWriteLock readWriteLock) { if (userConfig.getLocalFilePath(group).isPresent()) { // TODO: load encryptor once final KMSEncryptor kmsEncryptor = getEncryptor(group); return new File(userConfig.getLocalFilePath(group).get(), kmsEncryptor, new FileEncryptionContext(group), readWriteLock); } try { DynamoDB dynamoDB = DynamoDB.fromCredentials(awsCredentials, clientConfiguration, group, readWriteLock); return dynamoDB; } catch (ResourceNotFoundException e) { throw new DoesNotExistException("No storage backend found!", e); } } private StorageReference getCurrentStorageReference(SecretsGroupIdentifier group) { if (userConfig.getLocalFilePath(group).isPresent()) { return new FileReference(userConfig.getLocalFilePath(group).get()); } else { return new DynamoDBReference(); } } private Optional<StorageType> storageExists(SecretsGroupIdentifier group, ReadWriteLock readWriteLock) { if (userConfig.getLocalFilePath(group).isPresent()) { final KMSEncryptor kmsEncryptor = getEncryptor(group); File file = new File(userConfig.getLocalFilePath(group).get(), kmsEncryptor, new FileEncryptionContext(group), readWriteLock); return file.exists() ? Optional.of(StorageType.FILE) : Optional.empty(); } else { DynamoDB dynamoDB = DynamoDB.fromCredentials(awsCredentials, clientConfiguration, group, readWriteLock); return dynamoDB.exists() ? Optional.of(StorageType.DYNAMODB) : Optional.empty(); } } private Store createStore(SecretsGroupIdentifier group, StorageReference storageReference, ReadWriteLock readWriteLock) { if (storageReference instanceof DynamoDBReference) { DynamoDB store = DynamoDB.fromCredentials(awsCredentials, clientConfiguration, group, readWriteLock); store.create(); return store; } else if (storageReference instanceof FileReference) { FileReference fileReference = (FileReference) storageReference; final KMSEncryptor kmsEncryptor = getEncryptor(group); File store = new File(fileReference.path, kmsEncryptor, new FileEncryptionContext(group), readWriteLock); store.create(); return store; } else { throw new UnsupportedTypeException(storageReference.getClass().getName()); } } private KMSEncryptor getEncryptor(SecretsGroupIdentifier group) { return encryptors.computeIfAbsent(group, k -> KMSEncryptor.fromCredentials(awsCredentials, clientConfiguration, group, this.encryptionStrength)); } public Encryptor encryptor(SecretsGroupIdentifier group) { return getEncryptor(group); } private void setLocalState(SecretsGroupIdentifier group, StorageReference storageReference) { if (storageReference instanceof FileReference) { FileReference fileReference = (FileReference) storageReference; java.io.File newPath = fileReference.path; userConfig.addLocalFilePath(group, newPath); } } private void removeLocalState(SecretsGroupIdentifier group, Store store) { if (store instanceof File) { userConfig.removeLocalFilePath(group); } } @Override public SecretsGroup get(SecretsGroupIdentifier group) { ReadWriteLock readWriteLock = getReadWriteLock(group); Store store = getCurrentStore(group, readWriteLock); KMSEncryptor encryptor = getEncryptor(group); return new DefaultSecretsGroup(getAccount(), group, store, encryptor, readWriteLock); } @Override public Set<SecretsGroupIdentifier> identifiers() { synchronized (readWriteLocks) { IAMPolicyManager policyManager = IAMPolicyManager.fromCredentials(awsCredentials, clientConfiguration); return policyManager.getSecretsGroupIdentifiers(); } } @Override public SecretsGroupInfo info(SecretsGroupIdentifier group) { ReadWriteLock readWriteLock = getReadWriteLock(group); readWriteLock.readLock().lock(); try { ExecutorService executor = Executors.newFixedThreadPool(6); KMSEncryptor kmsEncryptor = getEncryptor(group); Future<Optional<String>> kmsArnFuture = executor.submit(() -> { if (kmsEncryptor.exists()) { return Optional.of(kmsEncryptor.getArn()); } else { return Optional.empty(); } }); Future<Optional<String>> storeArnFuture = executor.submit(() -> { try { Store store = getCurrentStore(group, readWriteLock); if (store.exists()) { return Optional.of(store.getArn()); } else { return Optional.empty(); } } catch (DoesNotExistException e) { return Optional.empty(); } }); Future<Optional<String>> adminPolicyArnFuture = executor.submit(() -> { if (policyManager.adminPolicyExists(group)) { return Optional.of(policyManager.getAdminPolicyArn(group)); } else { return Optional.empty(); } }); Future<Optional<String>> readOnlyPolicyArnFuture = executor.submit(() -> { if (policyManager.readOnlyPolicyExists(group)) { return Optional.of(policyManager.getReadOnlyArn(group)); } else { return Optional.empty(); } }); Future<List<Principal>> adminFuture = executor.submit(() -> { try { return policyManager.listAttachedAdmin(group); } catch (DoesNotExistException e) { return new ArrayList<>(); } }); Future<List<Principal>> readOnlyFuture = executor.submit(() -> { try { return policyManager.listAttachedReadOnly(group); } catch (DoesNotExistException e) { return new ArrayList<>(); } }); executor.shutdown(); try { Optional<String> kmsArn = kmsArnFuture.get(); Optional<String> storeArn = storeArnFuture.get(); Optional<String> adminPolicyArn = adminPolicyArnFuture.get(); Optional<String> readOnlyPolicyArn = readOnlyPolicyArnFuture.get(); List<Principal> admin = adminFuture.get(); List<Principal> readOnly = readOnlyFuture.get(); SecretsGroupSRN secretsGroupSRN = new SecretsGroupSRN(policyManager.getAccount(), group); return new SecretsGroupInfo(secretsGroupSRN, kmsArn, storeArn, adminPolicyArn, readOnlyPolicyArn, admin, readOnly); } catch (InterruptedException | ExecutionException e) { throw new SecretsGroupException(group, "Error getting group information", e); } } finally { readWriteLock.readLock().unlock(); } } @Override public void delete(SecretsGroupIdentifier group) { synchronized (readWriteLocks) { ReadWriteLock readWriteLock = getReadWriteLock(group); readWriteLock.readLock().lock(); try { log.info("About to delete Secrets Group: {}", group.name); ExecutorService executor = Executors.newFixedThreadPool(3); executor.submit((Callable<Void>) () -> { try { // We have already taken a global lock, and we want this thread to proceed independently final ReadWriteLock localReadWriteLock = new ReentrantReadWriteLock(); Store store = getCurrentStore(group, localReadWriteLock); store.delete(); removeLocalState(group, store); log.info(" Deleted Store"); } catch (DoesNotExistException e) { // Ignore } return null; }); executor.submit((Callable<Void>) () -> { try { policyManager.detachAllPrincipals(group); log.info(" Detached all Principals from the IAM Policies"); policyManager.deleteAdminPolicy(group); log.info(" Deleted Admin Policy"); policyManager.deleteReadonlyPolicy(group); log.info(" Deleted Readonly Policy"); } catch (DoesNotExistException e) { // Ignore } return null; }); executor.submit((Callable<Void>) () -> { try { KMSEncryptor kmsEncryptor = getEncryptor(group); kmsEncryptor.delete(); log.info(String.format(" Scheduled KMS key for deletion in %s days", kmsEncryptor.pendingDeletionWindowInDays())); } catch (DoesNotExistException | UnexpectedStateException e) { // Ignore } return null; }); executor.shutdown(); // TODO: make this more robust; when it times out or gets interrupted, the threads might still carry on in the background even though we release the lock int timeoutValue = 2; TimeUnit timeoutUnit = TimeUnit.MINUTES; if (!executor.awaitTermination(timeoutValue, timeoutUnit)) { throw new InterruptedException(String.format("Timeout of %d %s was reached when deleting resources for the group '%s'. This might have left the system in a dirty state.", timeoutValue, timeoutUnit.name(), group.name)); } } catch (InterruptedException e) { throw new FailedToDeleteResourceException(String.format("Deletion of group '%s' was interrupted, this might have left the resources in a dirty state.", group.name), e); } finally { readWriteLock.readLock().unlock(); } } } @Override public void attachAdmin(SecretsGroupIdentifier group, Principal principal) { ReadWriteLock readWriteLock = getReadWriteLock(group); readWriteLock.writeLock().lock(); try { policyManager.attachAdmin(group, principal); } finally { readWriteLock.writeLock().unlock(); } } @Override public void detachAdmin(SecretsGroupIdentifier group, Principal principal) { ReadWriteLock readWriteLock = getReadWriteLock(group); readWriteLock.writeLock().lock(); try { policyManager.detachAdmin(group, principal); } finally { readWriteLock.writeLock().unlock(); } } @Override public void detachReadOnly(SecretsGroupIdentifier group, Principal principal) { ReadWriteLock readWriteLock = getReadWriteLock(group); readWriteLock.writeLock().lock(); try { policyManager.detachReadOnly(group, principal); } finally { readWriteLock.writeLock().unlock(); } } @Override public void attachReadOnly(SecretsGroupIdentifier group, Principal principal) { ReadWriteLock readWriteLock = getReadWriteLock(group); readWriteLock.writeLock().lock(); try { policyManager.attachReadOnly(group, principal); } finally { readWriteLock.writeLock().unlock(); } } private String getAccount() { return policyManager.getAccount(); } // TODO: should this be in the interface? public void backup(SecretsGroupIdentifier group, Store backupStore, boolean failIfBackupStoreAlreadyExists) { ReadWriteLock readWriteLock = getReadWriteLock(group); readWriteLock.writeLock().lock(); try { Store currentStore = getCurrentStore(group, readWriteLock); if (backupStore.exists()) { if (failIfBackupStoreAlreadyExists) { throw new AlreadyExistsException("The store to backup to already exists"); } backupStore.delete(); } backupStore.create(); currentStore.stream().forEach(backupStore::create); backupStore.close(); } finally { readWriteLock.writeLock().unlock(); } } public void restore(SecretsGroupIdentifier group, Store backupStore, boolean failIfStoreToRestoreAlreadyExists) { ReadWriteLock readWriteLock = getReadWriteLock(group); readWriteLock.writeLock().lock(); try { Store currentStore = getCurrentStore(group, readWriteLock); if (currentStore.exists()) { if (failIfStoreToRestoreAlreadyExists) { throw new AlreadyExistsException("The store to restore already exists"); } currentStore.delete(); } currentStore.create(); backupStore.stream().forEach(currentStore::create); currentStore.close(); } finally { readWriteLock.writeLock().unlock(); } } public SecretsGroupInfo migrate(SecretsGroupIdentifier group, StorageReference newStorageReference) { ReadWriteLock readWriteLock = getReadWriteLock(group); readWriteLock.writeLock().lock(); try { StorageReference currentStorageReference = getCurrentStorageReference(group); if (currentStorageReference.equals(newStorageReference)) { throw new IllegalStateException("You cannot migrate to the same backend!"); } Store currentStore = getCurrentStore(group, readWriteLock); try (Store newStore = createStore(group, newStorageReference, readWriteLock)) { currentStore.stream().forEach(newStore::create); } if (newStorageReference instanceof FileReference) { setLocalState(group, newStorageReference); } else { removeLocalState(group, currentStore); } currentStore.delete(); return info(group); } finally { readWriteLock.writeLock().unlock(); } } }