diff --git a/build.gradle b/build.gradle index 2bf5455d8..228613039 100644 --- a/build.gradle +++ b/build.gradle @@ -111,6 +111,7 @@ dependencies { implementation 'software.amazon.awssdk:acmpca' implementation 'software.amazon.awssdk:dynamodb' implementation 'software.amazon.awssdk:neptune' + implementation 'software.amazon.awssdk:secretsmanager' implementation 'org.json:json:20180813' gyroDoclet "gyro:gyro-doclet:0.99.0-SNAPSHOT" diff --git a/examples/secretsmanager/secret.gyro b/examples/secretsmanager/secret.gyro new file mode 100644 index 000000000..b648e48b2 --- /dev/null +++ b/examples/secretsmanager/secret.gyro @@ -0,0 +1,15 @@ +aws::secret secret + name: "secret-example" + description: "secret-example-description-updated" + force-delete-without-recovery: true + kms-key: $(aws::kms-key secret-kms-key-example) + secret-string: "secret-string-example" + tags: { + "secret-example-tag" : "secret-example-tag-value" + } +end + +aws::kms-key secret-kms-key-example + aliases: ["alias/secret1", "alias/secret2"] + description: "secret kms key example description" +end diff --git a/src/main/java/gyro/aws/secretsmanager/SecretFinder.java b/src/main/java/gyro/aws/secretsmanager/SecretFinder.java new file mode 100644 index 000000000..88b44e8f2 --- /dev/null +++ b/src/main/java/gyro/aws/secretsmanager/SecretFinder.java @@ -0,0 +1,94 @@ +/* + * Copyright 2020, Brightspot. + * + * 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 gyro.aws.secretsmanager; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +import gyro.aws.AwsFinder; +import gyro.core.Type; +import software.amazon.awssdk.services.secretsmanager.SecretsManagerClient; +import software.amazon.awssdk.services.secretsmanager.model.DescribeSecretResponse; +import software.amazon.awssdk.services.secretsmanager.model.ResourceNotFoundException; +import software.amazon.awssdk.services.secretsmanager.model.SecretListEntry; + +/** + * Query secrets manager. + * + * Example + * ------- + * + * .. code-block:: gyro + * + * secrets: $(external-query aws::secret { arn: 'secret-arn-example'}) + */ +@Type("secret") +public class SecretFinder extends AwsFinder { + + private String arn; + + /** + * The Amazon Resource Name (ARN) of the secret. + */ + public String getArn() { + return arn; + } + + public void setArn(String arn) { + this.arn = arn; + } + + @Override + protected List findAllAws(SecretsManagerClient client) { + return client.listSecretsPaginator().stream().flatMap(list -> + list.secretList().stream().map(this::convertEntry)).collect(Collectors.toList()); + } + + @Override + protected List findAws( + SecretsManagerClient client, Map filters) { + List secretList = new ArrayList<>(); + try { + secretList.add(client.describeSecret(r -> r.secretId(filters.get("arn")))); + } catch (ResourceNotFoundException ex) { + // No resource found + } + + return secretList; + } + + private DescribeSecretResponse convertEntry(SecretListEntry entry) { + return DescribeSecretResponse.builder() + .arn(entry.arn()) + .deletedDate(entry.deletedDate()) + .description(entry.description()) + .kmsKeyId(entry.kmsKeyId()) + .lastAccessedDate(entry.lastAccessedDate()) + .lastChangedDate(entry.lastChangedDate()) + .lastRotatedDate(entry.lastRotatedDate()) + .name(entry.name()) + .owningService(entry.owningService()) + .rotationEnabled(entry.rotationEnabled()) + .rotationLambdaARN(entry.rotationLambdaARN()) + .rotationRules(entry.rotationRules()) + .tags(entry.tags()) + .versionIdsToStages(entry.secretVersionsToStages()) + .build(); + } +} diff --git a/src/main/java/gyro/aws/secretsmanager/SecretResource.java b/src/main/java/gyro/aws/secretsmanager/SecretResource.java new file mode 100644 index 000000000..380befa90 --- /dev/null +++ b/src/main/java/gyro/aws/secretsmanager/SecretResource.java @@ -0,0 +1,456 @@ +/* + * Copyright 2020, Brightspot. + * + * 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 gyro.aws.secretsmanager; + +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.stream.Collectors; + +import com.google.common.collect.MapDifference; +import com.google.common.collect.Maps; +import gyro.aws.AwsResource; +import gyro.aws.Copyable; +import gyro.aws.kms.KmsKeyResource; +import gyro.core.GyroUI; +import gyro.core.Type; +import gyro.core.resource.Id; +import gyro.core.resource.Output; +import gyro.core.resource.Resource; +import gyro.core.resource.Updatable; +import gyro.core.scope.State; +import gyro.core.validation.ConflictsWith; +import gyro.core.validation.Required; +import software.amazon.awssdk.core.SdkBytes; +import software.amazon.awssdk.services.secretsmanager.SecretsManagerClient; +import software.amazon.awssdk.services.secretsmanager.model.CreateSecretResponse; +import software.amazon.awssdk.services.secretsmanager.model.DescribeSecretResponse; +import software.amazon.awssdk.services.secretsmanager.model.ResourceNotFoundException; +import software.amazon.awssdk.services.secretsmanager.model.Tag; +import software.amazon.awssdk.services.secretsmanager.model.TagResourceRequest; +import software.amazon.awssdk.services.secretsmanager.model.UntagResourceRequest; + +/** + * Creates a Secret with the specified Name, Description, Kms Key, Secret String and Tags. + * + * Example + * ------- + * + * .. code-block:: gyro + * + * aws::secret secret + * name: "secret-example" + * description: "secret-example-description-updated" + * force-delete-without-recovery: true + * kms-key: $(aws::kms-key secret-kms-key-example) + * secret-string: "secret-string-example" + * tags: { + * "secret-example-tag" : "secret-example-tag-value" + * } + * end + */ +@Type("secret") +public class SecretResource extends AwsResource implements Copyable { + + private String description; + private Boolean forceDeleteWithoutRecovery; + private KmsKeyResource kmsKey; + private Long recoveryWindowInDays; + private String secretBinary; + private String secretString; + private Map tags; + + // Read-only + private String arn; + private String deletedDate; + private String lastAccessedDate; + private String lastChangedDate; + private String lastRotatedDate; + private String name; + private String owningService; + private Boolean rotationEnabled; + private String rotationLambdaARN; + private SecretRotationRulesType secretRotationRulesType; + private String versionId; + private Map> versionIdsToStages; + + /** + * The description of the secret. See `Description Info `_. + */ + @Updatable + public String getDescription() { + return description; + } + + public void setDescription(String description) { + this.description = description; + } + + /** + * The KMS master key to be used to encrypt the protected text in new versions of the secret. + */ + @Updatable + public KmsKeyResource getKmsKey() { + return kmsKey; + } + + public void setKmsKey(KmsKeyResource kmsKey) { + this.kmsKey = kmsKey; + } + + /** + * The data that you want to encrypt and store in the new version of the secret. + */ + @Updatable + public String getSecretBinary() { + return secretBinary; + } + + public void setSecretBinary(String secretBinary) { + this.secretBinary = secretBinary; + } + + /** + * The updated text data that you want to encrypt and store in this new version of the secret. + */ + @Updatable + public String getSecretString() { + return secretString; + } + + public void setSecretString(String secretString) { + this.secretString = secretString; + } + + /** + * Specifies a list of tags that are attached to the secret. + */ + @Updatable + public Map getTags() { + return tags; + } + + public void setTags(Map tags) { + this.tags = tags; + } + + /** + * The Amazon Resource Name (ARN) of the secret. + */ + @Id + @Output + public String getArn() { + return arn; + } + + public void setArn(String arn) { + this.arn = arn; + } + + /** + * The deletion date of the secret. + */ + @Output + public String getDeletedDate() { + return deletedDate; + } + + public void setDeletedDate(String deletedDate) { + this.deletedDate = deletedDate; + } + + /** + * When enabled, lets the secret be deleted without any recovery window. + */ + @ConflictsWith("recovery-window-in-days") + public Boolean getForceDeleteWithoutRecovery() { + return forceDeleteWithoutRecovery; + } + + public void setForceDeleteWithoutRecovery(Boolean forceDeleteWithoutRecovery) { + this.forceDeleteWithoutRecovery = forceDeleteWithoutRecovery; + } + + /** + * The most recent date and time that this secret was accessed. + */ + @Output + public String getLastAccessedDate() { + return lastAccessedDate; + } + + public void setLastAccessedDate(String lastAccessedDate) { + this.lastAccessedDate = lastAccessedDate; + } + + /** + * The most recent date and time that this secret was modified in any way. + */ + @Output + public String getLastChangedDate() { + return lastChangedDate; + } + + public void setLastChangedDate(String lastChangedDate) { + this.lastChangedDate = lastChangedDate; + } + + /** + * The most recent date and time that the Secrets Manager rotation process was successfully completed. + */ + @Output + public String getLastRotatedDate() { + return lastRotatedDate; + } + + public void setLastRotatedDate(String lastRotatedDate) { + this.lastRotatedDate = lastRotatedDate; + } + + /** + * The name of the secret. (Required) + */ + @Required + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + /** + * The name of the owning service. + */ + @Output + public String getOwningService() { + return owningService; + } + + public void setOwningService(String owningService) { + this.owningService = owningService; + } + + /** + * The number of days that Secrets Manager waits before it can delete the secret. + */ + @ConflictsWith("force-delete-without-recovery") + public Long getRecoveryWindowInDays() { + return recoveryWindowInDays; + } + + public void setRecoveryWindowInDays(Long recoveryWindowInDays) { + this.recoveryWindowInDays = recoveryWindowInDays; + } + + /** + * Enable automatic rotation for the secret. + */ + @Output + public Boolean getRotationEnabled() { + return rotationEnabled; + } + + public void setRotationEnabled(Boolean rotationEnabled) { + this.rotationEnabled = rotationEnabled; + } + + /** + * The unique ARN of a Lambda function that's invoked by Secrets Manager to rotate the secret either automatically + * per the schedule or manually by a call to RotateSecret. + */ + @Output + public String getRotationLambdaARN() { + return rotationLambdaARN; + } + + public void setRotationLambdaARN(String rotationLambdaARN) { + this.rotationLambdaARN = rotationLambdaARN; + } + + /** + * The structure that contains the rotation configuration for the secret. + * + * @subresource gyro.aws.secretsmanager.SecretRotationRulesType + */ + @Output + public SecretRotationRulesType getSecretRotationRulesType() { + return secretRotationRulesType; + } + + public void setSecretRotationRulesType(SecretRotationRulesType secretRotationRulesType) { + this.secretRotationRulesType = secretRotationRulesType; + } + + /** + * The unique identifier associated with the version of the secret. + */ + @Output + public String getVersionId() { + return versionId; + } + + public void setVersionId(String versionId) { + this.versionId = versionId; + } + + /** + * The list of all of the currently assigned VersionStage staging labels and the VersionId that each is attached + * to. + */ + @Output + public Map> getVersionIdsToStages() { + return versionIdsToStages; + } + + public void setVersionIdsToStages(Map> versionIdsToStages) { + this.versionIdsToStages = versionIdsToStages; + } + + @Override + public boolean refresh() { + SecretsManagerClient client = createClient(SecretsManagerClient.class); + DescribeSecretResponse response = null; + + try { + response = client.describeSecret(r -> r.secretId(getArn())); + } catch (ResourceNotFoundException ex) { + // No Resource found + } + + if (response == null) { + return false; + } + + copyFrom(response); + return true; + } + + @Override + public void create(GyroUI ui, State state) throws Exception { + SecretsManagerClient client = createClient(SecretsManagerClient.class); + + CreateSecretResponse response = client.createSecret(r -> r + .description(getDescription()) + .kmsKeyId(getKmsKey() != null ? getKmsKey().getArn() : null) + .name(getName()) + .secretBinary(getSecretBinary() != null ? SdkBytes.fromUtf8String(getSecretBinary()) : null) + .secretString(getSecretString()) + .tags(convertTags(getTags())) + ); + + setArn(response.arn()); + refresh(); + } + + @Override + public void update( + GyroUI ui, State state, Resource current, Set changedFieldNames) throws Exception { + SecretsManagerClient client = createClient(SecretsManagerClient.class); + + if (changedFieldNames.size() > 1 || !changedFieldNames.contains("tags")) { + client.updateSecret(r -> r.secretId(getArn()) + .description(getDescription()) + .kmsKeyId(getKmsKey() != null ? getKmsKey().getArn() : null) + .secretBinary(getSecretBinary() != null ? SdkBytes.fromUtf8String(getSecretBinary()) : null) + .secretString(getSecretString()) + .build()); + } + + if (changedFieldNames.contains("tags")) { + SecretResource oldResource = (SecretResource) current; + Map oldTags = oldResource.getTags(); + MapDifference diff = Maps.difference(oldTags, getTags()); + + TagResourceRequest tagRequest = null; + UntagResourceRequest untagRequest = null; + + if (getTags().isEmpty()) { + untagRequest = UntagResourceRequest.builder() + .secretId(getArn()) + .tagKeys(diff.entriesOnlyOnLeft().keySet()) + .build(); + } else if (diff.entriesOnlyOnLeft().isEmpty()) { + tagRequest = TagResourceRequest.builder() + .secretId(getArn()) + .tags(convertTags(getTags())) + .build(); + } else { + tagRequest = TagResourceRequest.builder() + .secretId(getArn()) + .tags(convertTags(getTags())) + .build(); + + untagRequest = UntagResourceRequest.builder() + .secretId(getArn()) + .tagKeys(diff.entriesOnlyOnLeft().keySet()) + .build(); + } + + if (tagRequest != null) { + client.tagResource(tagRequest); + } + + if (untagRequest != null) { + client.untagResource(untagRequest); + } + } + } + + @Override + public void delete(GyroUI ui, State state) throws Exception { + SecretsManagerClient client = createClient(SecretsManagerClient.class); + + client.deleteSecret(r -> r.secretId(getArn()) + .forceDeleteWithoutRecovery(getForceDeleteWithoutRecovery()) + .recoveryWindowInDays(getRecoveryWindowInDays())); + } + + @Override + public void copyFrom(DescribeSecretResponse model) { + setArn(model.arn()); + setDeletedDate(model.deletedDate() != null ? model.deletedDate().toString() : null); + setDescription(model.description()); + setKmsKey(findById(KmsKeyResource.class, model.kmsKeyId()) != null + ? findById(KmsKeyResource.class, model.kmsKeyId()) + : null); + setLastAccessedDate(model.lastAccessedDate() != null ? model.lastAccessedDate().toString() : null); + setLastChangedDate(model.lastAccessedDate() != null ? model.lastChangedDate().toString() : null); + setLastRotatedDate(model.lastRotatedDate() != null ? model.lastRotatedDate().toString() : null); + setName(model.name()); + setOwningService(model.owningService()); + setRotationEnabled(model.rotationEnabled()); + setRotationLambdaARN(model.rotationLambdaARN()); + + if (model.rotationRules() != null) { + SecretRotationRulesType secretRotationRulesType = newSubresource(SecretRotationRulesType.class); + secretRotationRulesType.copyFrom(model.rotationRules()); + setSecretRotationRulesType(secretRotationRulesType); + } else { + setSecretRotationRulesType(null); + } + + setTags(model.tags().stream().collect(Collectors.toMap(Tag::key, Tag::value))); + setVersionIdsToStages(model.versionIdsToStages()); + } + + private List convertTags(Map tags) { + return tags.entrySet().stream() + .map(e -> Tag.builder().key(e.getKey()).value(e.getValue()).build()) + .collect(Collectors.toList()); + } +} diff --git a/src/main/java/gyro/aws/secretsmanager/SecretRotationRulesType.java b/src/main/java/gyro/aws/secretsmanager/SecretRotationRulesType.java new file mode 100644 index 000000000..39ecc0643 --- /dev/null +++ b/src/main/java/gyro/aws/secretsmanager/SecretRotationRulesType.java @@ -0,0 +1,49 @@ +/* + * Copyright 2020, Brightspot. + * + * 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 gyro.aws.secretsmanager; + +import gyro.aws.Copyable; +import gyro.core.resource.Diffable; +import gyro.core.resource.Output; +import software.amazon.awssdk.services.secretsmanager.model.RotationRulesType; + +public class SecretRotationRulesType extends Diffable implements Copyable { + + private Long automaticallyAfterDays; + + /** + * The number of days between automatic scheduled rotations of the secret. + */ + @Output + public Long getAutomaticallyAfterDays() { + return automaticallyAfterDays; + } + + public void setAutomaticallyAfterDays(Long automaticallyAfterDays) { + this.automaticallyAfterDays = automaticallyAfterDays; + } + + @Override + public void copyFrom(RotationRulesType model) { + setAutomaticallyAfterDays(model.automaticallyAfterDays()); + } + + @Override + public String primaryKey() { + return ""; + } +} diff --git a/src/main/java/gyro/aws/secretsmanager/package-info.java b/src/main/java/gyro/aws/secretsmanager/package-info.java new file mode 100644 index 000000000..29f2a7185 --- /dev/null +++ b/src/main/java/gyro/aws/secretsmanager/package-info.java @@ -0,0 +1,21 @@ +/* + * Copyright 2020, Brightspot. + * + * 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. + */ + +@DocGroup("Secrets Manager") + +package gyro.aws.secretsmanager; + +import gyro.core.resource.DocGroup;