envless-sdk 0.0.1
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/CHANGELOG.md +24 -0
- data/README.md +171 -0
- data/lib/envless/sdk/api_error.rb +54 -0
- data/lib/envless/sdk/bodies.rb +49 -0
- data/lib/envless/sdk/client.rb +39 -0
- data/lib/envless/sdk/clock.rb +24 -0
- data/lib/envless/sdk/connection_pool.rb +83 -0
- data/lib/envless/sdk/connector.rb +26 -0
- data/lib/envless/sdk/constants.rb +262 -0
- data/lib/envless/sdk/crypto/cipher.rb +58 -0
- data/lib/envless/sdk/crypto/codec.rb +23 -0
- data/lib/envless/sdk/crypto/envelope.rb +41 -0
- data/lib/envless/sdk/crypto/keys.rb +42 -0
- data/lib/envless/sdk/crypto/passphrase.rb +38 -0
- data/lib/envless/sdk/crypto/passphrase_rules.rb +42 -0
- data/lib/envless/sdk/crypto/values.rb +31 -0
- data/lib/envless/sdk/crypto/workspace_key.rb +40 -0
- data/lib/envless/sdk/crypto_messages.rb +31 -0
- data/lib/envless/sdk/default_client.rb +55 -0
- data/lib/envless/sdk/defaults.rb +32 -0
- data/lib/envless/sdk/envelope.rb +48 -0
- data/lib/envless/sdk/environment.rb +42 -0
- data/lib/envless/sdk/errors.rb +23 -0
- data/lib/envless/sdk/exchange.rb +27 -0
- data/lib/envless/sdk/helpers.rb +49 -0
- data/lib/envless/sdk/http.rb +46 -0
- data/lib/envless/sdk/listing.rb +54 -0
- data/lib/envless/sdk/messages.rb +43 -0
- data/lib/envless/sdk/net_http_adapter.rb +84 -0
- data/lib/envless/sdk/numbers.rb +44 -0
- data/lib/envless/sdk/options.rb +56 -0
- data/lib/envless/sdk/pagination.rb +31 -0
- data/lib/envless/sdk/paths.rb +70 -0
- data/lib/envless/sdk/received.rb +28 -0
- data/lib/envless/sdk/redirects.rb +56 -0
- data/lib/envless/sdk/resource.rb +50 -0
- data/lib/envless/sdk/resources/environments.rb +61 -0
- data/lib/envless/sdk/resources/invites.rb +28 -0
- data/lib/envless/sdk/resources/keys.rb +36 -0
- data/lib/envless/sdk/resources/me.rb +11 -0
- data/lib/envless/sdk/resources/members.rb +36 -0
- data/lib/envless/sdk/resources/product_access.rb +30 -0
- data/lib/envless/sdk/resources/products.rb +62 -0
- data/lib/envless/sdk/resources/projects.rb +42 -0
- data/lib/envless/sdk/resources/roles.rb +40 -0
- data/lib/envless/sdk/resources/variable_changes.rb +57 -0
- data/lib/envless/sdk/resources/variables.rb +49 -0
- data/lib/envless/sdk/resources/versions.rb +60 -0
- data/lib/envless/sdk/resources/webhook_deliveries.rb +33 -0
- data/lib/envless/sdk/resources/webhooks.rb +44 -0
- data/lib/envless/sdk/resources/workspace.rb +26 -0
- data/lib/envless/sdk/resources/workspace_domain.rb +31 -0
- data/lib/envless/sdk/resources/workspaces.rb +24 -0
- data/lib/envless/sdk/results.rb +43 -0
- data/lib/envless/sdk/retry_policy.rb +36 -0
- data/lib/envless/sdk/rotation/entry_rotator.rb +61 -0
- data/lib/envless/sdk/rotation/key_ring.rb +34 -0
- data/lib/envless/sdk/rotation/ledger.rb +36 -0
- data/lib/envless/sdk/rotation/runner.rb +45 -0
- data/lib/envless/sdk/rotation/variable_rotator.rb +38 -0
- data/lib/envless/sdk/text.rb +71 -0
- data/lib/envless/sdk/transport.rb +62 -0
- data/lib/envless/sdk/update_notice.rb +66 -0
- data/lib/envless/sdk/urls.rb +70 -0
- data/lib/envless/sdk/variable_names.rb +55 -0
- data/lib/envless/sdk/version.rb +5 -0
- data/lib/envless/sdk/webhooks/headers.rb +49 -0
- data/lib/envless/sdk/webhooks/verify.rb +58 -0
- data/lib/envless/sdk/wire.rb +77 -0
- data/lib/envless/sdk.rb +77 -0
- data/sig/envless/sdk/adapter.rbs +60 -0
- data/sig/envless/sdk/client.rbs +31 -0
- data/sig/envless/sdk/crypto.rbs +92 -0
- data/sig/envless/sdk/errors.rbs +37 -0
- data/sig/envless/sdk/http.rbs +31 -0
- data/sig/envless/sdk/pagination.rbs +81 -0
- data/sig/envless/sdk/results.rbs +62 -0
- data/sig/envless/sdk/rotation.rbs +86 -0
- data/sig/envless/sdk/settings.rbs +81 -0
- data/sig/envless/sdk/stdlib.rbs +8 -0
- data/sig/envless/sdk/text.rbs +52 -0
- data/sig/envless/sdk/transport.rbs +79 -0
- data/sig/envless/sdk/urls.rbs +73 -0
- data/sig/envless/sdk/webhooks.rbs +23 -0
- data/sig/envless/sdk.rbs +36 -0
- data/sig/generated/constants.rbs +57 -0
- data/sig/generated/resources.rbs +209 -0
- data/sig/generated/types.rbs +466 -0
- data/sig/manifest.yaml +3 -0
- metadata +134 -0
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Envless
|
|
4
|
+
module Resources
|
|
5
|
+
class Webhooks < Resource
|
|
6
|
+
def list(limit: nil, offset: nil, search: nil, order: nil, product: nil, timeout: nil)
|
|
7
|
+
page(PATHS[:WEBHOOKS], COLLECTIONS[:WEBHOOKS], Listing.of(limit:, offset:, search:, order:, product:), timeout)
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
def list_all(limit: nil, offset: nil, search: nil, order: nil, product: nil, timeout: nil)
|
|
11
|
+
all(PATHS[:WEBHOOKS], COLLECTIONS[:WEBHOOKS], Listing.of(limit:, offset:, search:, order:, product:), timeout)
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def iterate(limit: nil, offset: nil, search: nil, order: nil, product: nil, timeout: nil, &block)
|
|
15
|
+
each_item(PATHS[:WEBHOOKS], COLLECTIONS[:WEBHOOKS], Listing.of(limit:, offset:, search:, order:, product:),
|
|
16
|
+
timeout, &block)
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def get(id, timeout: nil)
|
|
20
|
+
request(Paths.webhook_path(id), timeout:)
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def create(body = nil, timeout: nil, **fields)
|
|
24
|
+
request(PATHS[:WEBHOOKS], method: HTTP_METHODS[:POST], body: body_of(body, fields), timeout:)
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def update(id, patch = nil, timeout: nil, **fields)
|
|
28
|
+
request(Paths.webhook_path(id), method: HTTP_METHODS[:PATCH], body: body_of(patch, fields), timeout:)
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def delete(id, timeout: nil)
|
|
32
|
+
request(Paths.webhook_path(id), method: HTTP_METHODS[:DELETE], timeout:)
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def rotate_secret(id, timeout: nil)
|
|
36
|
+
request("#{Paths.webhook_path(id)}/#{SEGMENTS[:ROTATE]}", method: HTTP_METHODS[:POST], timeout:)
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def list_event_types(timeout: nil)
|
|
40
|
+
request(PATHS[:WEBHOOK_EVENT_TYPES], timeout:)
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
end
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Envless
|
|
4
|
+
module Resources
|
|
5
|
+
class Workspace < Resource
|
|
6
|
+
attr_reader :domain
|
|
7
|
+
|
|
8
|
+
def initialize(transport)
|
|
9
|
+
@domain = WorkspaceDomain.new(transport)
|
|
10
|
+
super
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def get(timeout: nil)
|
|
14
|
+
request(PATHS[:WORKSPACE], timeout:)
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def update(patch = nil, timeout: nil, **fields)
|
|
18
|
+
request(PATHS[:WORKSPACE], method: HTTP_METHODS[:PATCH], body: body_of(patch, fields), timeout:)
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def delete(timeout: nil)
|
|
22
|
+
request(PATHS[:WORKSPACE], method: HTTP_METHODS[:DELETE], timeout:)
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Envless
|
|
4
|
+
module Resources
|
|
5
|
+
class WorkspaceDomain < Resource
|
|
6
|
+
def get(timeout: nil)
|
|
7
|
+
domain_of(request(PATHS[:WORKSPACE_DOMAIN], timeout:))
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
def claim(hostname, timeout: nil)
|
|
11
|
+
domain_of(request(PATHS[:WORKSPACE_DOMAIN], method: HTTP_METHODS[:POST], body: {"hostname" => hostname},
|
|
12
|
+
timeout:))
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def verify(reattempt: false, timeout: nil)
|
|
16
|
+
domain_of(request(PATHS[:WORKSPACE_DOMAIN_VERIFY], method: HTTP_METHODS[:POST],
|
|
17
|
+
body: {"reattempt" => reattempt == true}, timeout:))
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def remove(timeout: nil)
|
|
21
|
+
request(PATHS[:WORKSPACE_DOMAIN], method: HTTP_METHODS[:DELETE], timeout:)
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
private
|
|
25
|
+
|
|
26
|
+
def domain_of(data)
|
|
27
|
+
data.is_a?(Hash) ? data[:domain] : nil
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Envless
|
|
4
|
+
module Resources
|
|
5
|
+
class Workspaces < Resource
|
|
6
|
+
def list(limit: nil, offset: nil, search: nil, order: nil, product: nil, timeout: nil)
|
|
7
|
+
page(PATHS[:WORKSPACES], COLLECTIONS[:WORKSPACES], Listing.of(limit:, offset:, search:, order:, product:), timeout)
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
def list_all(limit: nil, offset: nil, search: nil, order: nil, product: nil, timeout: nil)
|
|
11
|
+
all(PATHS[:WORKSPACES], COLLECTIONS[:WORKSPACES], Listing.of(limit:, offset:, search:, order:, product:), timeout)
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def iterate(limit: nil, offset: nil, search: nil, order: nil, product: nil, timeout: nil, &block)
|
|
15
|
+
each_item(PATHS[:WORKSPACES], COLLECTIONS[:WORKSPACES], Listing.of(limit:, offset:, search:, order:, product:),
|
|
16
|
+
timeout, &block)
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def create(body = nil, timeout: nil, **fields)
|
|
20
|
+
request(PATHS[:WORKSPACES], method: HTTP_METHODS[:POST], body: body_of(body, fields), timeout:)
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Envless
|
|
4
|
+
Page = Data.define(:items, :pagination)
|
|
5
|
+
|
|
6
|
+
class Page
|
|
7
|
+
def has_more?
|
|
8
|
+
Wire.truthy?(pagination[:hasMore])
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def total_count
|
|
12
|
+
pagination[:totalCount]
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
ProductAccessPage = Data.define(:items, :pagination, :is_private)
|
|
17
|
+
|
|
18
|
+
class ProductAccessPage
|
|
19
|
+
def private?
|
|
20
|
+
is_private
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def has_more?
|
|
24
|
+
Wire.truthy?(pagination[:hasMore])
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def total_count
|
|
28
|
+
pagination[:totalCount]
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
Validation = Data.define(:valid, :error)
|
|
33
|
+
|
|
34
|
+
class Validation
|
|
35
|
+
def valid?
|
|
36
|
+
valid
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
RotationIssue = Data.define(:scope, :id, :field, :reason)
|
|
41
|
+
|
|
42
|
+
RotationResult = Data.define(:variables, :version_entries, :skipped, :failures)
|
|
43
|
+
end
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Envless
|
|
4
|
+
module RetryPolicy
|
|
5
|
+
MILLISECONDS = 1000.0
|
|
6
|
+
|
|
7
|
+
module_function
|
|
8
|
+
|
|
9
|
+
def retry_after_of(header)
|
|
10
|
+
header.nil? ? Float::NAN : Numbers.js_number(header)
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def delay(status, verb, attempt, max_retries, retry_after)
|
|
14
|
+
return nil if attempt >= max_retries
|
|
15
|
+
return nil unless Constants::RETRYABLE_STATUSES.include?(status) && Constants::RETRYABLE_METHODS.include?(verb)
|
|
16
|
+
|
|
17
|
+
asked = retry_after.finite? && retry_after.positive?
|
|
18
|
+
wait = asked ? retry_after * MILLISECONDS : (1 << (attempt + 1)) * Constants::RETRY[:BASE_BACKOFF_MS]
|
|
19
|
+
|
|
20
|
+
[wait, Constants::RETRY[:MAX_BACKOFF_MS]].min / MILLISECONDS
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def settle(verb, attempt, max_retries, response)
|
|
24
|
+
envelope = Envelope.read(response.status, response.reason, response.body)
|
|
25
|
+
return Reply.new(data: envelope[:data]) if Envelope.success?(response.status, envelope)
|
|
26
|
+
|
|
27
|
+
retry_after = retry_after_of(response.header(Constants::HEADER_KEYS[:RETRY_AFTER]))
|
|
28
|
+
wait = delay(response.status, verb, attempt, max_retries, retry_after)
|
|
29
|
+
raise Envelope.api_error(response.status, response.headers, envelope, retry_after) if wait.nil?
|
|
30
|
+
|
|
31
|
+
wait
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
Reply = Data.define(:data)
|
|
36
|
+
end
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Envless
|
|
4
|
+
module Rotation
|
|
5
|
+
class EntryRotator
|
|
6
|
+
SCOPE = Constants::ROTATION_SCOPES[:VERSION_ENTRY]
|
|
7
|
+
FIELDS = Constants::ROTATION_FIELDS
|
|
8
|
+
|
|
9
|
+
def initialize(client, ring, ledger, scope, product)
|
|
10
|
+
@client = client
|
|
11
|
+
@ring = ring
|
|
12
|
+
@ledger = ledger
|
|
13
|
+
@scope = scope
|
|
14
|
+
@product = product
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def rotate(batch)
|
|
18
|
+
rewritten = batch.filter_map { |entry| attempt(entry) }
|
|
19
|
+
rewritten.empty? ? 0 : write(rewritten)
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
private
|
|
23
|
+
|
|
24
|
+
def attempt(entry)
|
|
25
|
+
rotated(entry)
|
|
26
|
+
rescue => error
|
|
27
|
+
@ledger.failed(SCOPE, Ledger.identifier(entry), error)
|
|
28
|
+
nil
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def write(rewritten)
|
|
32
|
+
project, environment = @scope
|
|
33
|
+
result = @client.versions.reencrypt_entries(project, environment, rewritten, product: @product)
|
|
34
|
+
updated = result.is_a?(Hash) ? result[:updated] : nil
|
|
35
|
+
updated.is_a?(Integer) ? updated : 0
|
|
36
|
+
rescue => error
|
|
37
|
+
rewritten.each { |written| @ledger.failed(SCOPE, written[:id], error) }
|
|
38
|
+
0
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def rotated(entry)
|
|
42
|
+
id = Ledger.identifier(entry)
|
|
43
|
+
value = entry[:value]
|
|
44
|
+
return strand(entry, id) unless Wire.truthy?(value) && @ledger.rotatable?(SCOPE, id, FIELDS[:VALUE], value)
|
|
45
|
+
|
|
46
|
+
sealed = @ring.rotate(value)
|
|
47
|
+
default = entry[:defaultValue]
|
|
48
|
+
rotatable = Wire.truthy?(default) && @ledger.rotatable?(SCOPE, id, FIELDS[:DEFAULT_VALUE], default)
|
|
49
|
+
|
|
50
|
+
{id:, value: sealed, defaultValue: rotatable ? @ring.rotate(default) : nil}
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def strand(entry, id)
|
|
54
|
+
default = entry[:defaultValue]
|
|
55
|
+
field = FIELDS[:DEFAULT_VALUE]
|
|
56
|
+
@ledger.stranded(SCOPE, id, field) if Wire.truthy?(default) && @ledger.rotatable?(SCOPE, id, field, default)
|
|
57
|
+
nil
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
end
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Envless
|
|
4
|
+
module Rotation
|
|
5
|
+
class KeyRing
|
|
6
|
+
def initialize(workspace_unique_id, current, following)
|
|
7
|
+
@workspace = workspace_unique_id
|
|
8
|
+
@current = current
|
|
9
|
+
@following = following
|
|
10
|
+
@opening = {}
|
|
11
|
+
@sealing = nil
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def rotate(value)
|
|
15
|
+
plaintext = Crypto::Cipher.decrypt(opening_key(Crypto::Envelope.parse(value).iterations), value)
|
|
16
|
+
Crypto::Cipher.encrypt_with_key(sealing_key, plaintext)
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
private
|
|
20
|
+
|
|
21
|
+
def opening_key(iterations)
|
|
22
|
+
@opening[iterations] ||= if iterations.nil?
|
|
23
|
+
Crypto::Keys.derive_workspace_key(@current, @workspace)
|
|
24
|
+
else
|
|
25
|
+
Crypto::Keys.derive_key(@current, Crypto::Keys.workspace_salt(@workspace), iterations)
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def sealing_key
|
|
30
|
+
@sealing ||= Crypto::Keys.derive_workspace_key(@following, @workspace)
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Envless
|
|
4
|
+
module Rotation
|
|
5
|
+
class Ledger
|
|
6
|
+
def initialize
|
|
7
|
+
@skipped = []
|
|
8
|
+
@failures = []
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def rotatable?(scope, id, field, value)
|
|
12
|
+
return true if Crypto::Values.ciphertext?(value)
|
|
13
|
+
|
|
14
|
+
@skipped << RotationIssue.new(scope:, id:, field:, reason: Constants::ROTATION_MESSAGES[:NOT_CIPHERTEXT])
|
|
15
|
+
false
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def stranded(scope, id, field)
|
|
19
|
+
@failures << RotationIssue.new(scope:, id:, field:, reason: Constants::ROTATION_MESSAGES[:DEFAULT_STRANDED])
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def failed(scope, id, error)
|
|
23
|
+
@failures << RotationIssue.new(scope:, id:, field: nil, reason: error.message)
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def result(variables, version_entries)
|
|
27
|
+
RotationResult.new(variables:, version_entries:, skipped: @skipped.dup, failures: @failures.dup)
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def self.identifier(item)
|
|
31
|
+
id = item.is_a?(Hash) ? item[:id] : nil
|
|
32
|
+
id.is_a?(String) ? id : ""
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Envless
|
|
4
|
+
module Rotation
|
|
5
|
+
class Runner
|
|
6
|
+
def initialize(client:, workspace_unique_id:, from:, to:, project:, environment:, product: nil)
|
|
7
|
+
ring = KeyRing.new(workspace_unique_id, from, to)
|
|
8
|
+
@client = client
|
|
9
|
+
@following = to
|
|
10
|
+
@scope = [project, environment]
|
|
11
|
+
@product = product
|
|
12
|
+
@ledger = Ledger.new
|
|
13
|
+
@variables = VariableRotator.new(client, ring, @ledger, product)
|
|
14
|
+
@entries = EntryRotator.new(client, ring, @ledger, @scope, product)
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def run
|
|
18
|
+
strength = Crypto::Passphrase.strength(@following)
|
|
19
|
+
raise ArgumentError, format(CryptoMessages::WEAK_ROTATION, error: strength.error) unless strength.valid?
|
|
20
|
+
|
|
21
|
+
project, environment = @scope
|
|
22
|
+
variables = @client.variables.list_all(project, environment, product: @product)
|
|
23
|
+
entries = @client.versions.list_all_entries(project, environment, product: @product)
|
|
24
|
+
batches = entries.each_slice(Constants::REENCRYPT_BATCH_SIZE).to_a
|
|
25
|
+
|
|
26
|
+
rotated, version_entries = totals(variables, batches)
|
|
27
|
+
@ledger.result(rotated, version_entries)
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
private
|
|
31
|
+
|
|
32
|
+
def totals(variables, batches)
|
|
33
|
+
rotated = 0
|
|
34
|
+
version_entries = 0
|
|
35
|
+
|
|
36
|
+
[variables.size, batches.size].max.times do |index|
|
|
37
|
+
rotated += @variables.rotate(variables.fetch(index)) if index < variables.size
|
|
38
|
+
version_entries += @entries.rotate(batches.fetch(index)) if index < batches.size
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
[rotated, version_entries]
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
end
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Envless
|
|
4
|
+
module Rotation
|
|
5
|
+
class VariableRotator
|
|
6
|
+
SCOPE = Constants::ROTATION_SCOPES[:VARIABLE]
|
|
7
|
+
FIELDS = [Constants::ROTATION_FIELDS[:VALUE], Constants::ROTATION_FIELDS[:DEFAULT_VALUE]].freeze
|
|
8
|
+
|
|
9
|
+
def initialize(client, ring, ledger, product)
|
|
10
|
+
@client = client
|
|
11
|
+
@ring = ring
|
|
12
|
+
@ledger = ledger
|
|
13
|
+
@product = product
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def rotate(variable)
|
|
17
|
+
rotated(variable)
|
|
18
|
+
rescue => error
|
|
19
|
+
@ledger.failed(SCOPE, Ledger.identifier(variable), error)
|
|
20
|
+
0
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
private
|
|
24
|
+
|
|
25
|
+
def rotated(variable)
|
|
26
|
+
id = Ledger.identifier(variable)
|
|
27
|
+
patch = FIELDS.filter_map do |field|
|
|
28
|
+
value = variable[field.to_sym]
|
|
29
|
+
[field.to_sym, @ring.rotate(value)] if Wire.truthy?(value) && @ledger.rotatable?(SCOPE, id, field, value)
|
|
30
|
+
end
|
|
31
|
+
return 0 if patch.empty?
|
|
32
|
+
|
|
33
|
+
@client.variables.update_by_id(id, product: @product, **patch.to_h)
|
|
34
|
+
1
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Envless
|
|
4
|
+
module Text
|
|
5
|
+
BYTE_ORDER_MARK = ""
|
|
6
|
+
HTTP_WHITESPACE = " \t\r\n"
|
|
7
|
+
JS_WHITESPACE = [0x09, 0x0A, 0x0B, 0x0C, 0x0D, 0x20, 0xA0, 0x1680, *(0x2000..0x200A).to_a, 0x2028, 0x2029, 0x202F,
|
|
8
|
+
0x205F, 0x3000, 0xFEFF].pack("U*").freeze
|
|
9
|
+
JS_TRIM = /\A[#{Regexp.escape(JS_WHITESPACE)}]+|[#{Regexp.escape(JS_WHITESPACE)}]+\z/
|
|
10
|
+
HTTP_TRIM = /\A[ \t\r\n]+|[ \t\r\n]+\z/
|
|
11
|
+
TOKEN = /\A[\x21-\x7e]+\z/
|
|
12
|
+
HEADER_VALUE = /\A[\x20-\x7e]*\z/
|
|
13
|
+
TRANSCODABLE = [Encoding::UTF_8, Encoding::BINARY, Encoding::US_ASCII].freeze
|
|
14
|
+
|
|
15
|
+
module_function
|
|
16
|
+
|
|
17
|
+
def utf8(text)
|
|
18
|
+
converted = if TRANSCODABLE.include?(text.encoding)
|
|
19
|
+
text.dup.force_encoding(Encoding::UTF_8)
|
|
20
|
+
else
|
|
21
|
+
text.encode(Encoding::UTF_8, invalid: :replace, undef: :replace)
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
converted.valid_encoding? ? converted : converted.scrub
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def strict(text, name)
|
|
28
|
+
raise ArgumentError, format(CryptoMessages::TEXT_SHAPE, name:) unless text.is_a?(String)
|
|
29
|
+
|
|
30
|
+
converted = TRANSCODABLE.include?(text.encoding) ? text.dup.force_encoding(Encoding::UTF_8) : text.encode(Encoding::UTF_8)
|
|
31
|
+
raise ArgumentError, format(CryptoMessages::UTF8_SHAPE, name:) unless converted.valid_encoding?
|
|
32
|
+
|
|
33
|
+
converted
|
|
34
|
+
rescue EncodingError
|
|
35
|
+
raise ArgumentError, format(CryptoMessages::UTF8_SHAPE, name:)
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def decode(bytes)
|
|
39
|
+
utf8(bytes.b).delete_prefix(BYTE_ORDER_MARK)
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def js_trim(text)
|
|
43
|
+
utf8(text).gsub(JS_TRIM, "")
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def http_trim(text)
|
|
47
|
+
utf8(text).gsub(HTTP_TRIM, "")
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def token?(text)
|
|
51
|
+
TOKEN.match?(text)
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def header_value?(text)
|
|
55
|
+
HEADER_VALUE.match?(text)
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def utf16_units(text)
|
|
59
|
+
utf8(text).codepoints.flat_map do |point|
|
|
60
|
+
next [point] if point < 0x10000
|
|
61
|
+
|
|
62
|
+
offset = point - 0x10000
|
|
63
|
+
[0xD800 + (offset >> 10), 0xDC00 + (offset & 0x3FF)]
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def utf16_length(text)
|
|
68
|
+
utf8(text).codepoints.sum { |point| (point < 0x10000) ? 1 : 2 }
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
end
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Envless
|
|
4
|
+
class Transport
|
|
5
|
+
attr_reader :base_url, :max_retries, :timeout, :user_agent
|
|
6
|
+
|
|
7
|
+
def initialize(token:, base_url:, adapter: nil, max_retries: nil, timeout: nil, user_agent: nil,
|
|
8
|
+
disable_update_notice: false)
|
|
9
|
+
@token = token
|
|
10
|
+
@base_url = Urls.valid_base_url(base_url)
|
|
11
|
+
@max_retries = Options.valid_retries(max_retries)
|
|
12
|
+
@timeout = timeout.nil? ? Defaults::TIMEOUT : Options.valid_timeout(timeout)
|
|
13
|
+
@user_agent = Options.user_agent(user_agent)
|
|
14
|
+
@owned = adapter ? nil : NetHttpAdapter.new
|
|
15
|
+
@adapter = @owned || Options.adapter(adapter)
|
|
16
|
+
UpdateNotice.check unless disable_update_notice
|
|
17
|
+
freeze
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def request(path, method: Constants::HTTP_METHODS[:GET], query: nil, body: nil, timeout: nil)
|
|
21
|
+
verb = Options.verb(method)
|
|
22
|
+
url = Urls.build_url(@base_url, path, query)
|
|
23
|
+
content = body.nil? ? nil : Wire.encode(body)
|
|
24
|
+
seconds = timeout.nil? ? @timeout : Options.valid_timeout(timeout)
|
|
25
|
+
outbound = HttpRequest.new(method: verb, url:, headers: headers(!content.nil?), body: content, timeout: seconds)
|
|
26
|
+
attempt = 0
|
|
27
|
+
|
|
28
|
+
loop do
|
|
29
|
+
outcome = RetryPolicy.settle(verb, attempt, @max_retries, Exchange.call(@adapter, outbound, seconds, @base_url))
|
|
30
|
+
return outcome.data if outcome.is_a?(Reply)
|
|
31
|
+
|
|
32
|
+
attempt += 1
|
|
33
|
+
pause(outcome)
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def close
|
|
38
|
+
@owned&.close
|
|
39
|
+
nil
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def inspect
|
|
43
|
+
"#<#{self.class.name} base_url=#{@base_url.inspect}>"
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
private
|
|
47
|
+
|
|
48
|
+
def headers(with_body)
|
|
49
|
+
headers = {
|
|
50
|
+
Constants::HEADER_KEYS[:AUTHORIZATION] => "Bearer #{@token}",
|
|
51
|
+
Constants::HEADER_KEYS[:ACCEPT] => Constants::CONTENT_TYPES[:JSON],
|
|
52
|
+
Constants::HEADER_KEYS[:USER_AGENT] => @user_agent
|
|
53
|
+
}
|
|
54
|
+
headers[Constants::HEADER_KEYS[:CONTENT_TYPE]] = Constants::CONTENT_TYPES[:JSON] if with_body
|
|
55
|
+
headers
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def pause(seconds)
|
|
59
|
+
sleep(seconds)
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
end
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Envless
|
|
4
|
+
module UpdateNotice
|
|
5
|
+
@checked = false
|
|
6
|
+
@lock = Mutex.new
|
|
7
|
+
|
|
8
|
+
class << self
|
|
9
|
+
def check
|
|
10
|
+
return nil unless claim
|
|
11
|
+
|
|
12
|
+
Thread.new { announce }
|
|
13
|
+
nil
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def claim
|
|
17
|
+
@lock.synchronize do
|
|
18
|
+
wanted = !@checked && !disabled?
|
|
19
|
+
@checked = true
|
|
20
|
+
wanted
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def disabled?
|
|
25
|
+
!ENV.fetch(Defaults::UPDATE_NOTICE_ENV_DISABLE, "").empty? || !$stdout.tty?
|
|
26
|
+
rescue IOError, NoMethodError
|
|
27
|
+
true
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def announce
|
|
31
|
+
latest = latest_version
|
|
32
|
+
warn(notice(latest)) if latest && newer?(latest, VERSION)
|
|
33
|
+
rescue
|
|
34
|
+
nil
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def latest_version
|
|
38
|
+
seconds = Defaults::UPDATE_NOTICE_TIMEOUT
|
|
39
|
+
response = Net::HTTP.start(Defaults::UPDATE_NOTICE_HOST, 443, use_ssl: true, open_timeout: seconds,
|
|
40
|
+
read_timeout: seconds, write_timeout: seconds) { |http| http.request_get(Defaults::UPDATE_NOTICE_PATH) }
|
|
41
|
+
return nil unless response.is_a?(Net::HTTPSuccess)
|
|
42
|
+
|
|
43
|
+
version = JSON.parse(response.body.to_s)["version"]
|
|
44
|
+
version.is_a?(String) ? version : nil
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def newer?(candidate, current)
|
|
48
|
+
left = parts(candidate)
|
|
49
|
+
right = parts(current)
|
|
50
|
+
return false if left.nil? || right.nil?
|
|
51
|
+
|
|
52
|
+
(left <=> right) == 1
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def parts(value)
|
|
56
|
+
numbers = value.to_s.strip.delete_prefix("v").split(".").map { |part| Integer(part, 10, exception: false) }
|
|
57
|
+
(numbers.size == 3 && numbers.none?(&:nil?)) ? numbers.compact : nil
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def notice(latest)
|
|
61
|
+
format(Messages::UPDATE_AVAILABLE, icon: Defaults::UPDATE_NOTICE_ICON, package: Defaults::UPDATE_NOTICE_PACKAGE,
|
|
62
|
+
latest:, current: VERSION, command: Defaults::UPDATE_NOTICE_COMMAND, changelog: Defaults::UPDATE_NOTICE_CHANGELOG_URL)
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
end
|