btps_client 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/README.md +444 -0
- data/lib/btps_client/api_operations/actionable.rb +22 -0
- data/lib/btps_client/api_operations/creatable.rb +16 -0
- data/lib/btps_client/api_operations/deletable.rb +15 -0
- data/lib/btps_client/api_operations/listable.rb +16 -0
- data/lib/btps_client/api_operations/movable.rb +17 -0
- data/lib/btps_client/api_operations/nested_listable.rb +23 -0
- data/lib/btps_client/api_operations/retrievable.rb +15 -0
- data/lib/btps_client/api_operations/scalar_creatable.rb +31 -0
- data/lib/btps_client/api_operations/searchable.rb +16 -0
- data/lib/btps_client/api_operations/updatable.rb +16 -0
- data/lib/btps_client/api_operations/uploadable.rb +29 -0
- data/lib/btps_client/api_requestor.rb +143 -0
- data/lib/btps_client/api_resource.rb +45 -0
- data/lib/btps_client/btps_object.rb +58 -0
- data/lib/btps_client/client.rb +37 -0
- data/lib/btps_client/configuration.rb +71 -0
- data/lib/btps_client/errors.rb +39 -0
- data/lib/btps_client/resources/credential.rb +98 -0
- data/lib/btps_client/resources/folder.rb +36 -0
- data/lib/btps_client/resources/managed_account.rb +97 -0
- data/lib/btps_client/resources/request.rb +107 -0
- data/lib/btps_client/resources/safe.rb +77 -0
- data/lib/btps_client/resources/secret.rb +179 -0
- data/lib/btps_client/schema_builder.rb +50 -0
- data/lib/btps_client/services/auth_service.rb +57 -0
- data/lib/btps_client/services/credentials_service.rb +36 -0
- data/lib/btps_client/services/folders_service.rb +37 -0
- data/lib/btps_client/services/managed_accounts_service.rb +48 -0
- data/lib/btps_client/services/requests_service.rb +44 -0
- data/lib/btps_client/services/safes_service.rb +48 -0
- data/lib/btps_client/services/secrets_safe_service.rb +25 -0
- data/lib/btps_client/services/secrets_service.rb +96 -0
- data/lib/btps_client/validator.rb +129 -0
- data/lib/btps_client/version.rb +5 -0
- data/lib/btps_client.rb +64 -0
- metadata +183 -0
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'typhoeus'
|
|
4
|
+
require 'json'
|
|
5
|
+
|
|
6
|
+
module BtpsClient
|
|
7
|
+
# Single-responsibility HTTP layer.
|
|
8
|
+
# Builds requests, executes via Typhoeus, retries on 429/5xx,
|
|
9
|
+
# and maps HTTP status codes to the SDK error hierarchy.
|
|
10
|
+
# Never logs request bodies or bearer tokens.
|
|
11
|
+
class ApiRequestor
|
|
12
|
+
ERROR_MAP = {
|
|
13
|
+
400 => InvalidRequestError,
|
|
14
|
+
401 => AuthenticationError,
|
|
15
|
+
403 => PermissionError,
|
|
16
|
+
404 => NotFoundError,
|
|
17
|
+
409 => ConflictError,
|
|
18
|
+
422 => ValidationError,
|
|
19
|
+
429 => RateLimitError
|
|
20
|
+
}.freeze
|
|
21
|
+
|
|
22
|
+
def initialize(config)
|
|
23
|
+
@config = config
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
# Performs an HTTP request with automatic retry on retriable errors.
|
|
27
|
+
#
|
|
28
|
+
# @param method [Symbol] :get, :post, :put, :delete
|
|
29
|
+
# @param path [String] path relative to base_url (leading slash optional)
|
|
30
|
+
# @param params [Hash] query-string parameters
|
|
31
|
+
# @param body [Hash|String|nil] request body
|
|
32
|
+
# @param opts [Hash] { headers: {}, multipart: bool, form_encoded: bool }
|
|
33
|
+
# @return [Hash|Array|String|nil] parsed response body
|
|
34
|
+
def request(method, path, params: {}, body: nil, opts: {})
|
|
35
|
+
url = "#{@config.base_url}/#{path.to_s.sub(%r{^/}, '')}"
|
|
36
|
+
attempt = 0
|
|
37
|
+
|
|
38
|
+
begin
|
|
39
|
+
attempt += 1
|
|
40
|
+
log(:debug, "→ #{method.upcase} #{url}#{" params=#{params}" unless params.empty?}")
|
|
41
|
+
response = execute(method, url, build_headers(opts), params, body, opts)
|
|
42
|
+
log(:debug, "← #{response.code} (attempt #{attempt})")
|
|
43
|
+
if response.code.zero?
|
|
44
|
+
raise ApiError.new(
|
|
45
|
+
"Network error: #{response.return_message} (curl code #{response.return_code})",
|
|
46
|
+
http_status: 0, http_body: nil
|
|
47
|
+
)
|
|
48
|
+
end
|
|
49
|
+
handle_response(response)
|
|
50
|
+
rescue RateLimitError, ApiError => e
|
|
51
|
+
log(:warn, "Retriable error (attempt #{attempt}/#{@config.max_retries}): #{e.message}")
|
|
52
|
+
raise if attempt > @config.max_retries
|
|
53
|
+
|
|
54
|
+
sleep(backoff(attempt))
|
|
55
|
+
retry
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
private
|
|
60
|
+
|
|
61
|
+
def execute(method, url, headers, params, body, opts)
|
|
62
|
+
req_opts = {
|
|
63
|
+
method: method,
|
|
64
|
+
headers: headers,
|
|
65
|
+
ssl_verifypeer: @config.verify_ssl,
|
|
66
|
+
timeout: @config.timeout,
|
|
67
|
+
connecttimeout: @config.connect_timeout
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
req_opts[:params] = params unless params.nil? || params.empty?
|
|
71
|
+
|
|
72
|
+
if opts[:multipart]
|
|
73
|
+
req_opts[:body] = body
|
|
74
|
+
elsif opts[:form_encoded]
|
|
75
|
+
req_opts[:body] = body
|
|
76
|
+
req_opts[:headers] = req_opts[:headers].merge('Content-Type' => 'application/x-www-form-urlencoded')
|
|
77
|
+
elsif !body.nil?
|
|
78
|
+
encoded_body = body.is_a?(String) ? body : body.to_json
|
|
79
|
+
req_opts[:body] = encoded_body
|
|
80
|
+
req_opts[:headers] = req_opts[:headers].merge('Content-Type' => 'application/json')
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
Typhoeus::Request.new(url, req_opts).run
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
def handle_response(response)
|
|
87
|
+
status = response.code
|
|
88
|
+
body = response.body
|
|
89
|
+
request_id = response.headers&.[]('X-Request-Id')
|
|
90
|
+
|
|
91
|
+
return parse_success_body(body) if status >= 200 && status < 300
|
|
92
|
+
|
|
93
|
+
raise_for_status(status, body, request_id)
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
def parse_success_body(body)
|
|
97
|
+
return nil if body.nil? || body.strip.empty?
|
|
98
|
+
|
|
99
|
+
JSON.parse(body)
|
|
100
|
+
rescue JSON::ParserError
|
|
101
|
+
body
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
def raise_for_status(status, body, request_id)
|
|
105
|
+
error_class = ERROR_MAP[status] || (status >= 500 ? ApiError : BtpsError)
|
|
106
|
+
log(:error, "HTTP #{status} (request_id=#{request_id}): #{body}")
|
|
107
|
+
raise error_class.new(parse_error_message(body), http_status: status, http_body: body, request_id: request_id)
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
def parse_error_message(body)
|
|
111
|
+
return nil if body.nil? || body.strip.empty?
|
|
112
|
+
|
|
113
|
+
parsed = JSON.parse(body)
|
|
114
|
+
extract_error_message(parsed)
|
|
115
|
+
rescue JSON::ParserError
|
|
116
|
+
body
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
def extract_error_message(parsed)
|
|
120
|
+
return parsed.to_s unless parsed.is_a?(Hash)
|
|
121
|
+
|
|
122
|
+
parsed['message'] || parsed['Message'] || parsed['error'] || parsed.to_s
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
def build_headers(opts = {})
|
|
126
|
+
headers = {
|
|
127
|
+
'Accept' => 'application/json',
|
|
128
|
+
'User-Agent' => "btps_client/#{BtpsClient::VERSION} Ruby/#{RUBY_VERSION}"
|
|
129
|
+
}
|
|
130
|
+
headers['Authorization'] = "Bearer #{@config.access_token}" if @config.access_token
|
|
131
|
+
headers.merge(opts.fetch(:headers, {}))
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
def backoff(attempt)
|
|
135
|
+
base = [(2**(attempt - 1)) * 0.5, 32].min
|
|
136
|
+
base * (0.5 + (rand * 0.5)) # jitter: randomise between 50% and 100% of base
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
def log(level, message)
|
|
140
|
+
@config.logger&.public_send(level, "[BtpsClient] #{message}")
|
|
141
|
+
end
|
|
142
|
+
end
|
|
143
|
+
end
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module BtpsClient
|
|
4
|
+
# Abstract marker for all API resource classes.
|
|
5
|
+
# Subclasses declare their base path with the `resource_path` class macro
|
|
6
|
+
# and extend only the operation mixins that correspond to real API endpoints.
|
|
7
|
+
class ApiResource < BtpsObject
|
|
8
|
+
# Class macro — call once per subclass to set the URL path segment.
|
|
9
|
+
# resource_path 'secrets-safe/folders'
|
|
10
|
+
def self.resource_path(path = nil)
|
|
11
|
+
return @resource_path = path if path
|
|
12
|
+
|
|
13
|
+
@resource_path || raise(NotImplementedError, "#{name} must declare resource_path")
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
# Returns the full collection URL, or the member URL when an id is given.
|
|
17
|
+
def self.resource_url(id = nil)
|
|
18
|
+
id ? "#{resource_path}/#{id}" : resource_path
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
# Class macro — declares validation constraints inline on each resource.
|
|
22
|
+
# The block is evaluated inside a SchemaBuilder instance:
|
|
23
|
+
#
|
|
24
|
+
# param_schema :create do
|
|
25
|
+
# required 'Name', type: :string, min_length: 1, max_length: 256
|
|
26
|
+
# optional 'Description', type: :string, max_length: 256
|
|
27
|
+
# end
|
|
28
|
+
#
|
|
29
|
+
# Calling param_schema with the same operation again overwrites the
|
|
30
|
+
# previous schema for that class.
|
|
31
|
+
def self.param_schema(operation, &block)
|
|
32
|
+
@param_schemas ||= {}
|
|
33
|
+
builder = SchemaBuilder.new
|
|
34
|
+
builder.instance_eval(&block)
|
|
35
|
+
@param_schemas[operation.to_sym] = builder.to_h
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
# Returns the schema Hash registered for +operation+, or nil when none
|
|
39
|
+
# has been declared (meaning validation is skipped for that operation).
|
|
40
|
+
def self.schema_for(operation)
|
|
41
|
+
@param_schemas ||= {}
|
|
42
|
+
@param_schemas[operation.to_sym]
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
end
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module BtpsClient
|
|
4
|
+
# Immutable base class for all deserialized API responses.
|
|
5
|
+
# Converts PascalCase JSON keys into snake_case Ruby reader methods.
|
|
6
|
+
# Instances are frozen after initialization — no mutations allowed.
|
|
7
|
+
class BtpsObject
|
|
8
|
+
attr_reader :raw_response
|
|
9
|
+
|
|
10
|
+
# Ruby built-in method names that must never be overwritten.
|
|
11
|
+
# API keys that snake_case to any of these are accessible via #[] instead.
|
|
12
|
+
RESERVED_METHODS = (BasicObject.instance_methods | Object.instance_methods)
|
|
13
|
+
.map(&:to_s).freeze
|
|
14
|
+
|
|
15
|
+
def initialize(data = {})
|
|
16
|
+
data = {} if data.nil?
|
|
17
|
+
@raw_response = data.dup.freeze
|
|
18
|
+
|
|
19
|
+
data.each_key do |key|
|
|
20
|
+
attr_name = snake_case(key.to_s)
|
|
21
|
+
next if RESERVED_METHODS.include?(attr_name)
|
|
22
|
+
|
|
23
|
+
val = sanitize(data[key])
|
|
24
|
+
# Define per-instance readers via the singleton class so different
|
|
25
|
+
# instances with different keys do not pollute each other's class.
|
|
26
|
+
singleton_class.define_method(attr_name) { val }
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
freeze
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
# Fetch any field by its original API key name (string or symbol).
|
|
33
|
+
# Useful for fields whose snake_case name collides with a Ruby built-in
|
|
34
|
+
# (e.g. 'ObjectId' → 'object_id') and therefore have no reader method.
|
|
35
|
+
def [](key)
|
|
36
|
+
@raw_response[key.to_s] || @raw_response[key.to_sym]
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
private
|
|
40
|
+
|
|
41
|
+
# Converts PascalCase / camelCase key strings into snake_case.
|
|
42
|
+
def snake_case(str)
|
|
43
|
+
str
|
|
44
|
+
.gsub(/([A-Z]+)([A-Z][a-z])/, '\1_\2')
|
|
45
|
+
.gsub(/([a-z\d])([A-Z])/, '\1_\2')
|
|
46
|
+
.downcase
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
# Recursively wraps nested hashes as BtpsObjects and freezes arrays.
|
|
50
|
+
def sanitize(value)
|
|
51
|
+
case value
|
|
52
|
+
when Hash then BtpsObject.new(value)
|
|
53
|
+
when Array then value.map { |v| v.is_a?(Hash) ? BtpsObject.new(v) : v }.freeze
|
|
54
|
+
else value
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
end
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module BtpsClient
|
|
4
|
+
# User-facing entry point. Instantiate with per-client config overrides
|
|
5
|
+
# or rely on the global BtpsClient.configure block.
|
|
6
|
+
class Client
|
|
7
|
+
def initialize(**opts)
|
|
8
|
+
base = BtpsClient.configuration.to_h
|
|
9
|
+
@config = Configuration.new(base.merge(opts))
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
# Returns the SecretsSafeService aggregator (folders / safes / secrets).
|
|
13
|
+
def secrets_safe
|
|
14
|
+
@secrets_safe ||= Services::SecretsSafeService.new(@config)
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
# Returns the AuthService for sign-in, sign-out, and OAuth token acquisition.
|
|
18
|
+
def auth
|
|
19
|
+
@auth ||= Services::AuthService.new(@config)
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
# Returns the ManagedAccountsService.
|
|
23
|
+
def managed_accounts
|
|
24
|
+
@managed_accounts ||= Services::ManagedAccountsService.new(@config)
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
# Returns the RequestsService.
|
|
28
|
+
def requests
|
|
29
|
+
@requests ||= Services::RequestsService.new(@config)
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
# Returns the CredentialsService.
|
|
33
|
+
def credentials
|
|
34
|
+
@credentials ||= Services::CredentialsService.new(@config)
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module BtpsClient
|
|
4
|
+
# Holds all SDK configuration.
|
|
5
|
+
# Build via BtpsClient.configure block or pass overrides to Configuration.new.
|
|
6
|
+
# Per-client overrides can be passed to Client.new — each Client gets its own
|
|
7
|
+
# Configuration copy so changes to one do not affect others.
|
|
8
|
+
class Configuration
|
|
9
|
+
DEFAULTS = {
|
|
10
|
+
scheme: 'https',
|
|
11
|
+
host: nil,
|
|
12
|
+
port: nil,
|
|
13
|
+
base_path: 'api/public/v3',
|
|
14
|
+
verify_ssl: true,
|
|
15
|
+
timeout: 30,
|
|
16
|
+
connect_timeout: 10,
|
|
17
|
+
max_retries: 3,
|
|
18
|
+
logger: nil
|
|
19
|
+
}.freeze
|
|
20
|
+
|
|
21
|
+
ALLOWED_ATTRS = DEFAULTS.keys.freeze
|
|
22
|
+
|
|
23
|
+
attr_accessor :access_token, :client_id, :client_secret
|
|
24
|
+
attr_reader(*ALLOWED_ATTRS)
|
|
25
|
+
|
|
26
|
+
def initialize(overrides = {})
|
|
27
|
+
overrides = overrides.transform_keys(&:to_sym)
|
|
28
|
+
invalid = overrides.keys - ALLOWED_ATTRS - %i[access_token client_id client_secret]
|
|
29
|
+
raise ArgumentError, "Unknown configuration keys: #{invalid.join(', ')}" unless invalid.empty?
|
|
30
|
+
|
|
31
|
+
ALLOWED_ATTRS.each do |key|
|
|
32
|
+
instance_variable_set(:"@#{key}", overrides.fetch(key, DEFAULTS[key]))
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
@access_token = overrides[:access_token] || ENV.fetch('BTPS_ACCESS_TOKEN', nil)
|
|
36
|
+
@client_id = overrides[:client_id] || ENV.fetch('BTPS_CLIENT_ID', nil)
|
|
37
|
+
@client_secret = overrides[:client_secret] || ENV.fetch('BTPS_CLIENT_SECRET', nil)
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
# Setters for use inside BtpsClient.configure { |c| c.host = '...' }
|
|
41
|
+
ALLOWED_ATTRS.each do |attr|
|
|
42
|
+
define_method(:"#{attr}=") { |val| instance_variable_set(:"@#{attr}", val) }
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
# Builds the full base URL used by ApiRequestor.
|
|
46
|
+
def base_url
|
|
47
|
+
port_segment = port ? ":#{port}" : ''
|
|
48
|
+
"#{scheme}://#{host}#{port_segment}/#{base_path}"
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
# Returns a safe string representation — credentials are redacted so this
|
|
52
|
+
# is safe to appear in logs, exception messages, and IRB sessions.
|
|
53
|
+
def inspect
|
|
54
|
+
attrs = ALLOWED_ATTRS.map { |k| "#{k}=#{public_send(k).inspect}" }
|
|
55
|
+
attrs << "access_token=#{@access_token ? '[REDACTED]' : 'nil'}"
|
|
56
|
+
attrs << "client_id=#{@client_id ? '[REDACTED]' : 'nil'}"
|
|
57
|
+
attrs << "client_secret=#{@client_secret ? '[REDACTED]' : 'nil'}"
|
|
58
|
+
"#<#{self.class} #{attrs.join(', ')}>"
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
# Returns all settings as a plain Hash — used by Client to merge global config
|
|
62
|
+
# with per-instance overrides.
|
|
63
|
+
def to_h
|
|
64
|
+
hash = ALLOWED_ATTRS.to_h { |key| [key, public_send(key)] }
|
|
65
|
+
hash[:access_token] = @access_token
|
|
66
|
+
hash[:client_id] = @client_id
|
|
67
|
+
hash[:client_secret] = @client_secret
|
|
68
|
+
hash
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
end
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module BtpsClient
|
|
4
|
+
# Base class for all SDK errors. Carries HTTP context without leaking credentials.
|
|
5
|
+
class BtpsError < StandardError
|
|
6
|
+
attr_reader :http_status, :http_body, :request_id
|
|
7
|
+
|
|
8
|
+
def initialize(message = nil, http_status: nil, http_body: nil, request_id: nil)
|
|
9
|
+
super(message)
|
|
10
|
+
@http_status = http_status
|
|
11
|
+
@http_body = http_body
|
|
12
|
+
@request_id = request_id
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
# HTTP 400
|
|
17
|
+
class InvalidRequestError < BtpsError; end
|
|
18
|
+
|
|
19
|
+
# HTTP 401
|
|
20
|
+
class AuthenticationError < BtpsError; end
|
|
21
|
+
|
|
22
|
+
# HTTP 403
|
|
23
|
+
class PermissionError < BtpsError; end
|
|
24
|
+
|
|
25
|
+
# HTTP 404
|
|
26
|
+
class NotFoundError < BtpsError; end
|
|
27
|
+
|
|
28
|
+
# HTTP 409
|
|
29
|
+
class ConflictError < BtpsError; end
|
|
30
|
+
|
|
31
|
+
# HTTP 422
|
|
32
|
+
class ValidationError < BtpsError; end
|
|
33
|
+
|
|
34
|
+
# HTTP 429
|
|
35
|
+
class RateLimitError < BtpsError; end
|
|
36
|
+
|
|
37
|
+
# HTTP 5xx
|
|
38
|
+
class ApiError < BtpsError; end
|
|
39
|
+
end
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module BtpsClient
|
|
4
|
+
module Resources
|
|
5
|
+
# Swagger tag: Credentials
|
|
6
|
+
# Endpoints:
|
|
7
|
+
# GET /credentials/{requestId} — retrieve password by request
|
|
8
|
+
# PUT /credentials — update credentials (bulk)
|
|
9
|
+
# GET /managedaccounts/{managedAccountID}/credentials — get current credentials
|
|
10
|
+
# PUT /managedaccounts/{managedAccountID}/credentials — set/update credentials
|
|
11
|
+
# POST /managedaccounts/{managedAccountID}/credentials/test — test credentials on system
|
|
12
|
+
# POST /managedaccounts/{managedAccountID}/credentials/change — trigger live rotation
|
|
13
|
+
class Credential < ApiResource
|
|
14
|
+
resource_path 'credentials'
|
|
15
|
+
|
|
16
|
+
# Update schema — maps to CredentialsUpdateModel (all optional)
|
|
17
|
+
param_schema :update do
|
|
18
|
+
optional 'Password', type: :string, nullable: true
|
|
19
|
+
optional 'Credentials', type: :string, nullable: true
|
|
20
|
+
optional 'PublicKey', type: :string, nullable: true
|
|
21
|
+
optional 'PrivateKey', type: :string, nullable: true
|
|
22
|
+
optional 'Passphrase', type: :string, nullable: true
|
|
23
|
+
optional 'UpdateSystem', type: :boolean, nullable: true
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
# Change schema — maps to CredentialsChangeModel (all optional)
|
|
27
|
+
param_schema :change do
|
|
28
|
+
optional 'Password', type: :string, nullable: true
|
|
29
|
+
optional 'Credentials', type: :string, nullable: true
|
|
30
|
+
optional 'PublicKey', type: :string, nullable: true
|
|
31
|
+
optional 'PrivateKey', type: :string, nullable: true
|
|
32
|
+
optional 'Passphrase', type: :string, nullable: true
|
|
33
|
+
optional 'UpdateSystem', type: :boolean, nullable: true
|
|
34
|
+
optional 'Queue', type: :boolean, nullable: true
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
# ── Request-scoped retrieve ────────────────────────────────────────────────
|
|
38
|
+
|
|
39
|
+
# GET /credentials/{requestId}
|
|
40
|
+
# Returns the password/credential for an active request as a BtpsObject with .value.
|
|
41
|
+
def self.retrieve_by_request(request_id, config: BtpsClient.config)
|
|
42
|
+
requestor = ApiRequestor.new(config)
|
|
43
|
+
data = requestor.request(:get, "#{resource_path}/#{request_id}")
|
|
44
|
+
BtpsObject.new('value' => data)
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
# ── Global (bulk) update ───────────────────────────────────────────────────
|
|
48
|
+
|
|
49
|
+
# PUT /credentials
|
|
50
|
+
# Updates credentials in bulk (not scoped to a specific managed account).
|
|
51
|
+
def self.update_global(params, config: BtpsClient.config)
|
|
52
|
+
Validator.validate!(schema_for(:update), params)
|
|
53
|
+
requestor = ApiRequestor.new(config)
|
|
54
|
+
requestor.request(:put, resource_path, body: params)
|
|
55
|
+
true
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
# ── Account-scoped methods ────────────────────────────────────────────────
|
|
59
|
+
|
|
60
|
+
# GET /managedaccounts/{managedAccountID}/credentials
|
|
61
|
+
# Retrieves current credentials for a managed account (returns BtpsObject with .value).
|
|
62
|
+
def self.retrieve_for_account(managed_account_id, params = {}, config: BtpsClient.config)
|
|
63
|
+
requestor = ApiRequestor.new(config)
|
|
64
|
+
data = requestor.request(:get, "managedaccounts/#{managed_account_id}/credentials",
|
|
65
|
+
params: params)
|
|
66
|
+
BtpsObject.new('value' => data)
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
# PUT /managedaccounts/{managedAccountID}/credentials
|
|
70
|
+
# Sets or updates the stored credentials for a managed account.
|
|
71
|
+
def self.update_for_account(managed_account_id, params, config: BtpsClient.config)
|
|
72
|
+
Validator.validate!(schema_for(:update), params)
|
|
73
|
+
requestor = ApiRequestor.new(config)
|
|
74
|
+
requestor.request(:put, "managedaccounts/#{managed_account_id}/credentials", body: params)
|
|
75
|
+
true
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
# POST /managedaccounts/{managedAccountID}/credentials/test
|
|
79
|
+
# Tests whether the stored credentials succeed on the target system.
|
|
80
|
+
def self.test_for_account(managed_account_id, config: BtpsClient.config)
|
|
81
|
+
requestor = ApiRequestor.new(config)
|
|
82
|
+
data = requestor.request(:post, "managedaccounts/#{managed_account_id}/credentials/test")
|
|
83
|
+
data ? BtpsObject.new(data) : true
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
# POST /managedaccounts/{managedAccountID}/credentials/change
|
|
87
|
+
# Triggers a live credential rotation on the target system.
|
|
88
|
+
def self.change_for_account(managed_account_id, params = {}, config: BtpsClient.config)
|
|
89
|
+
Validator.validate!(schema_for(:change), params)
|
|
90
|
+
requestor = ApiRequestor.new(config)
|
|
91
|
+
body = params.empty? ? nil : params
|
|
92
|
+
requestor.request(:post, "managedaccounts/#{managed_account_id}/credentials/change",
|
|
93
|
+
body: body)
|
|
94
|
+
true
|
|
95
|
+
end
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
end
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module BtpsClient
|
|
4
|
+
module Resources
|
|
5
|
+
# Swagger tag: Secrets Safe Folders
|
|
6
|
+
# Endpoints: GET/POST /secrets-safe/folders
|
|
7
|
+
# GET/PUT/DELETE /secrets-safe/folders/{folderId}
|
|
8
|
+
# PUT /secrets-safe/folders/{folderId}/move
|
|
9
|
+
class Folder < ApiResource
|
|
10
|
+
resource_path 'secrets-safe/folders'
|
|
11
|
+
|
|
12
|
+
extend ApiOperations::Listable # GET /secrets-safe/folders
|
|
13
|
+
extend ApiOperations::Retrievable # GET /secrets-safe/folders/{folderId}
|
|
14
|
+
extend ApiOperations::Creatable # POST /secrets-safe/folders
|
|
15
|
+
extend ApiOperations::Updatable # PUT /secrets-safe/folders/{folderId}
|
|
16
|
+
extend ApiOperations::Deletable # DELETE /secrets-safe/folders/{folderId}
|
|
17
|
+
extend ApiOperations::Movable # PUT /secrets-safe/folders/{folderId}/move
|
|
18
|
+
|
|
19
|
+
param_schema :create do
|
|
20
|
+
required 'Name', type: :string, min_length: 1, max_length: 256
|
|
21
|
+
optional 'Description', type: :string, max_length: 256, nullable: true
|
|
22
|
+
optional 'ParentId', type: :string, format: :uuid, nullable: true
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
param_schema :update do
|
|
26
|
+
required 'Name', type: :string, min_length: 1
|
|
27
|
+
optional 'Description', type: :string, max_length: 256, nullable: true
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
param_schema :move do
|
|
31
|
+
required 'DestinationFolderId', type: :string, format: :uuid
|
|
32
|
+
required 'DuplicateNameAction', type: :string, values: %w[Rename Replace Abort]
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module BtpsClient
|
|
4
|
+
module Resources
|
|
5
|
+
# Swagger tag: Managed Accounts
|
|
6
|
+
# Endpoints:
|
|
7
|
+
# GET /managedaccounts — list requestable accounts
|
|
8
|
+
# GET /managedaccounts/{id} — get by ID
|
|
9
|
+
# PUT /managedaccounts/{id} — update
|
|
10
|
+
# DELETE /managedaccounts/{id} — delete
|
|
11
|
+
# GET /managedsystems/{managedSystemID}/managedaccounts — list by managed system
|
|
12
|
+
# POST /managedsystems/{managedSystemID}/managedaccounts — create in managed system
|
|
13
|
+
# DELETE /managedsystems/{managedSystemID}/managedaccounts — delete all in system
|
|
14
|
+
# DELETE /managedsystems/{managedSystemID}/managedaccounts/{name} — delete by system + name
|
|
15
|
+
# GET /smartrules/{smartRuleID}/managedaccounts — list by smart rule
|
|
16
|
+
class ManagedAccount < ApiResource
|
|
17
|
+
resource_path 'managedaccounts'
|
|
18
|
+
|
|
19
|
+
extend ApiOperations::Listable # GET /managedaccounts
|
|
20
|
+
extend ApiOperations::Retrievable # GET /managedaccounts/{id}
|
|
21
|
+
extend ApiOperations::Updatable # PUT /managedaccounts/{id}
|
|
22
|
+
extend ApiOperations::Deletable # DELETE /managedaccounts/{id}
|
|
23
|
+
extend ApiOperations::NestedListable # list_for(parent_path, parent_id)
|
|
24
|
+
|
|
25
|
+
# Create schema — AccountName required, all others from ManagedAccountAddUpdateModel
|
|
26
|
+
param_schema :create do
|
|
27
|
+
required 'AccountName', type: :string, max_length: 245
|
|
28
|
+
optional 'Password', type: :string, max_length: 128, nullable: true
|
|
29
|
+
optional 'Description', type: :string, max_length: 1024, nullable: true
|
|
30
|
+
optional 'ApiEnabled', type: :boolean, nullable: true
|
|
31
|
+
optional 'PasswordRuleID', type: :integer, nullable: true
|
|
32
|
+
optional 'ReleaseDuration', type: :integer, minimum: 1, maximum: 525_600
|
|
33
|
+
optional 'MaxReleaseDuration', type: :integer, minimum: 1, maximum: 525_600
|
|
34
|
+
optional 'ISAReleaseDuration', type: :integer, minimum: 1, maximum: 525_600
|
|
35
|
+
optional 'AutoManagementFlag', type: :boolean, nullable: true
|
|
36
|
+
optional 'ChangeFrequencyType', type: :string, values: %w[first last xdays], nullable: true
|
|
37
|
+
optional 'ChangeFrequencyDays', type: :integer, minimum: 1, maximum: 999
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
# Update schema — all optional for PUT /managedaccounts/{id}
|
|
41
|
+
param_schema :update do
|
|
42
|
+
optional 'AccountName', type: :string, max_length: 245, nullable: true
|
|
43
|
+
optional 'Password', type: :string, max_length: 128, nullable: true
|
|
44
|
+
optional 'Description', type: :string, max_length: 1024, nullable: true
|
|
45
|
+
optional 'ApiEnabled', type: :boolean, nullable: true
|
|
46
|
+
optional 'PasswordRuleID', type: :integer, nullable: true
|
|
47
|
+
optional 'ReleaseDuration', type: :integer, minimum: 1, maximum: 525_600
|
|
48
|
+
optional 'MaxReleaseDuration', type: :integer, minimum: 1, maximum: 525_600
|
|
49
|
+
optional 'ISAReleaseDuration', type: :integer, minimum: 1, maximum: 525_600
|
|
50
|
+
optional 'AutoManagementFlag', type: :boolean, nullable: true
|
|
51
|
+
optional 'ChangeFrequencyType', type: :string, values: %w[first last xdays], nullable: true
|
|
52
|
+
optional 'ChangeFrequencyDays', type: :integer, minimum: 1, maximum: 999
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
# ── Managed System-scoped methods ──────────────────────────────────────────
|
|
56
|
+
|
|
57
|
+
# GET /managedsystems/{managedSystemID}/managedaccounts
|
|
58
|
+
# Lists all managed accounts for a specific managed system.
|
|
59
|
+
def self.list_by_system(system_id, params = {}, config: BtpsClient.config)
|
|
60
|
+
list_for('managedsystems', system_id, params: params, config: config)
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
# POST /managedsystems/{managedSystemID}/managedaccounts
|
|
64
|
+
# Creates a managed account within the given managed system.
|
|
65
|
+
def self.create_in_system(system_id, params, config: BtpsClient.config)
|
|
66
|
+
Validator.validate!(schema_for(:create), params)
|
|
67
|
+
requestor = ApiRequestor.new(config)
|
|
68
|
+
data = requestor.request(:post, "managedsystems/#{system_id}/managedaccounts", body: params)
|
|
69
|
+
new(data)
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
# DELETE /managedsystems/{managedSystemID}/managedaccounts
|
|
73
|
+
# Deletes ALL managed accounts within the given managed system.
|
|
74
|
+
def self.delete_all_in_system(system_id, config: BtpsClient.config)
|
|
75
|
+
requestor = ApiRequestor.new(config)
|
|
76
|
+
requestor.request(:delete, "managedsystems/#{system_id}/managedaccounts")
|
|
77
|
+
true
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
# DELETE /managedsystems/{managedSystemID}/managedaccounts/{accountName}
|
|
81
|
+
# Deletes a specific account by managed system ID and account name.
|
|
82
|
+
def self.delete_by_system_and_name(system_id, account_name, config: BtpsClient.config)
|
|
83
|
+
requestor = ApiRequestor.new(config)
|
|
84
|
+
requestor.request(:delete, "managedsystems/#{system_id}/managedaccounts/#{account_name}")
|
|
85
|
+
true
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
# ── Smart Rule-scoped method ────────────────────────────────────────────────
|
|
89
|
+
|
|
90
|
+
# GET /smartrules/{smartRuleID}/managedaccounts
|
|
91
|
+
# Lists all managed accounts matching a smart rule.
|
|
92
|
+
def self.list_by_smart_rule(smart_rule_id, params = {}, config: BtpsClient.config)
|
|
93
|
+
list_for('smartrules', smart_rule_id, params: params, config: config)
|
|
94
|
+
end
|
|
95
|
+
end
|
|
96
|
+
end
|
|
97
|
+
end
|