Cybersecurity-Projects/PROJECTS/intermediate/credential-rotation-enforcer/src/cre/aws/secrets_client.cr

99 lines
3.3 KiB
Crystal

# ===================
# ©AngelaMos | 2026
# secrets_client.cr
# ===================
require "http/client"
require "json"
require "uuid"
require "./signer"
require "../http/retry"
module CRE::Aws
class AwsApiError < Exception
getter status : Int32
getter aws_code : String?
def initialize(message : String, @status : Int32, @aws_code : String? = nil)
super(message)
end
end
class SecretsManagerClient
AWSCURRENT = "AWSCURRENT"
AWSPENDING = "AWSPENDING"
AWSPREVIOUS = "AWSPREVIOUS"
record SecretVersion, version_id : String, secret_string : String?
def initialize(
@access_key_id : String,
@secret_access_key : String,
@region : String,
@endpoint : String? = nil,
@session_token : String? = nil,
)
@signer = SigV4.new(@access_key_id, @secret_access_key, @region, "secretsmanager", @session_token)
end
# Stages a new secret version with the AWSPENDING label.
def put_secret_value(secret_id : String, secret_string : String, version_stages : Array(String) = [AWSPENDING]) : SecretVersion
payload = {
"SecretId" => secret_id,
"SecretString" => secret_string,
"ClientRequestToken" => UUID.random.to_s,
"VersionStages" => version_stages,
}.to_json
json = call("PutSecretValue", payload)
SecretVersion.new(json["VersionId"].as_s, secret_string)
end
def get_secret_value(secret_id : String, version_id : String? = nil, version_stage : String? = nil) : SecretVersion
payload_h = {"SecretId" => secret_id}
payload_h["VersionId"] = version_id if version_id
payload_h["VersionStage"] = version_stage if version_stage
payload = payload_h.to_json
json = call("GetSecretValue", payload)
SecretVersion.new(
json["VersionId"].as_s,
json["SecretString"]?.try(&.as_s),
)
end
def update_secret_version_stage(secret_id : String, version_stage : String, move_to_version_id : String? = nil, remove_from_version_id : String? = nil) : Nil
payload_h = {
"SecretId" => secret_id,
"VersionStage" => version_stage,
}
payload_h["MoveToVersionId"] = move_to_version_id if move_to_version_id
payload_h["RemoveFromVersionId"] = remove_from_version_id if remove_from_version_id
call("UpdateSecretVersionStage", payload_h.to_json)
end
private def call(action : String, body : String) : JSON::Any
uri = URI.parse(@endpoint || "https://secretsmanager.#{@region}.amazonaws.com/")
headers = HTTP::Headers{
"Content-Type" => "application/x-amz-json-1.1",
"X-Amz-Target" => "secretsmanager.#{action}",
}
@signer.sign("POST", uri, headers, body)
response = CRE::Http.request("POST", uri.to_s, headers, body, label: "aws.#{action}")
raise AwsApiError.new(error_message(response), response.status_code, error_code(response)) unless response.status_code < 300
response.body.empty? ? JSON::Any.new(Hash(String, JSON::Any).new) : JSON.parse(response.body)
end
private def error_message(resp : HTTP::Client::Response) : String
"AWS #{resp.status_code}: #{resp.body[0, 200]?}"
end
private def error_code(resp : HTTP::Client::Response) : String?
return nil if resp.body.empty?
JSON.parse(resp.body)["__type"]?.try(&.as_s)
rescue
nil
end
end
end