99 lines
3.3 KiB
Crystal
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
|