/* * 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.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; import java.util.List; import java.util.Map; import java.util.Optional; import keywhiz.api.model.Group; import keywhiz.api.model.SanitizedSecret; import keywhiz.api.model.SanitizedSecretWithGroups; import keywhiz.api.model.Secret; import keywhiz.api.model.SecretSeriesAndContent; import keywhiz.service.crypto.ContentCryptographer; import keywhiz.service.crypto.ContentEncodingException; import keywhiz.service.crypto.SecretTransformer; import javax.annotation.Nullable; import static com.google.common.base.Preconditions.checkArgument; import static com.google.common.base.Preconditions.checkNotNull; import static java.nio.charset.StandardCharsets.UTF_8; import static java.util.stream.Collectors.toList; import static java.util.stream.Collectors.toMap; import static keywhiz.api.model.SanitizedSecretWithGroups.fromSecretSeriesAndContentAndGroups; public class SecretController { private final SecretTransformer transformer; private final ContentCryptographer cryptographer; private final SecretDAO secretDAO; private final AclDAO aclDAO; public SecretController(SecretTransformer transformer, ContentCryptographer cryptographer, SecretDAO secretDAO, AclDAO aclDAO) { this.transformer = transformer; this.cryptographer = cryptographer; this.secretDAO = secretDAO; this.aclDAO = aclDAO; } /** * @param secretId external secret series id to look up secrets by. * @return Secret matching input parameters or Optional.absent(). */ public Optional<Secret> getSecretById(long secretId) { return secretDAO.getSecretById(secretId).map(transformer::transform); } /** * @param name of secret series to look up secrets by. * @return Secret matching input parameters or Optional.absent(). */ public Optional<Secret> getSecretByName(String name) { return secretDAO.getSecretByName(name).map(transformer::transform); } /** * @param expireMaxTime timestamp for farthest expiry to include * @param group limit results to secrets assigned to this group, if provided. * @return all existing sanitized secrets matching criteria. * */ public List<SanitizedSecret> getSanitizedSecrets(@Nullable Long expireMaxTime, Group group) { return secretDAO.getSecrets(expireMaxTime, group).stream() .map(SanitizedSecret::fromSecretSeriesAndContent) .collect(toList()); } /** * @param expireMaxTime timestamp for farthest expiry to include * @return all existing sanitized secrets and their groups matching criteria. * */ public List<SanitizedSecretWithGroups> getExpiringSanitizedSecrets(@Nullable Long expireMaxTime) { ImmutableList<SecretSeriesAndContent> secrets = secretDAO.getSecrets(expireMaxTime, null); Map<Long, SecretSeriesAndContent> secretIds = secrets.stream() .collect(toMap(s -> s.series().id(), s -> s)); Map<Long, List<Group>> groupsForSecrets = aclDAO.getGroupsForSecrets(secretIds.keySet()); return secrets.stream().map(s -> { List<Group> groups = groupsForSecrets.get(s.series().id()); if (groups == null) { groups = ImmutableList.of(); } return fromSecretSeriesAndContentAndGroups(s, groups); }).collect(toList()); } /** @return names of all existing sanitized secrets. */ public List<SanitizedSecret> getSecretsNameOnly() { return secretDAO.getSecretsNameOnly() .stream() .map(s -> SanitizedSecret.of(s.getKey(), s.getValue())) .collect(toList()); } /** * @param idx the first index to select in a list of secrets sorted by creation time * @param num the number of secrets after idx to select in the list of secrets * @param newestFirst if true, order the secrets from newest creation time to oldest * @return A list of secret names */ public List<SanitizedSecret> getSecretsBatched(int idx, int num, boolean newestFirst) { checkArgument(idx >= 0, "Index must be positive when getting batched secret names!"); checkArgument(num >= 0, "Num must be positive when getting batched secret names!"); return secretDAO.getSecretsBatched(idx, num, newestFirst).stream() .map(SanitizedSecret::fromSecretSeriesAndContent) .collect(toList()); } public SecretBuilder builder(String name, String secret, String creator, long expiry) { checkArgument(!name.isEmpty()); checkArgument(!secret.isEmpty()); checkArgument(!creator.isEmpty()); String hmac = cryptographer.computeHmac(secret.getBytes(UTF_8)); // Compute HMAC on base64 encoded data if (hmac == null) { throw new ContentEncodingException("Error encoding content for SecretBuilder!"); } String encryptedSecret = cryptographer.encryptionKeyDerivedFrom(name).encrypt(secret); return new SecretBuilder(transformer, secretDAO, name, encryptedSecret, hmac, creator, expiry); } /** Builder to generate new secret series or versions with. */ public static class SecretBuilder { private final SecretTransformer transformer; private final SecretDAO secretDAO; private final String name; private final String encryptedSecret; private final String hmac; private final String creator; private String description = ""; private Map<String, String> metadata = ImmutableMap.of(); private long expiry = 0; private String type; private Map<String, String> generationOptions = ImmutableMap.of(); /** * @param transformer * @param secretDAO * @param name of secret series. * @param encryptedSecret encrypted content of secret version * @param creator username responsible for creating this secret version. */ private SecretBuilder(SecretTransformer transformer, SecretDAO secretDAO, String name, String encryptedSecret, String hmac, String creator, long expiry) { this.transformer = transformer; this.secretDAO = secretDAO; this.name = name; this.encryptedSecret = encryptedSecret; this.hmac = hmac; this.creator = creator; this.expiry = expiry; } /** * Supply an optional description of the secret. * @param description description of secret * @return the builder */ public SecretBuilder withDescription(String description) { this.description = checkNotNull(description); return this; } /** * Supply optional map of metadata properties for the secret. * @param metadata metadata of secret * @return the builder */ public SecretBuilder withMetadata(Map<String, String> metadata) { this.metadata = checkNotNull(metadata); return this; } /** * Supply a secret type, otherwise the default '' is used. * @param type type of secret * @return the builder */ public SecretBuilder withType(String type) { this.type = checkNotNull(type); return this; } /** * Finalizes creation of a new secret. * * @return an instance of the newly created secret. */ public Secret create() { secretDAO.createSecret(name, encryptedSecret, hmac, creator, metadata, expiry, description, type, generationOptions); return transformer.transform(secretDAO.getSecretByName(name).get()); } public Secret createOrUpdate() { secretDAO.createOrUpdateSecret(name, encryptedSecret, hmac, creator, metadata, expiry, description, type, generationOptions); return transformer.transform(secretDAO.getSecretByName(name).get()); } } }