vault 0.11.0 → 0.14.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 +5 -5
- data/.circleci/config.yml +42 -0
- data/.gitignore +1 -0
- data/CHANGELOG.md +40 -0
- data/README.md +11 -3
- data/lib/vault/api.rb +2 -0
- data/lib/vault/api/auth.rb +30 -5
- data/lib/vault/api/kv.rb +207 -0
- data/lib/vault/api/secret.rb +12 -0
- data/lib/vault/api/sys.rb +1 -0
- data/lib/vault/api/sys/mount.rb +7 -2
- data/lib/vault/api/sys/namespace.rb +85 -0
- data/lib/vault/api/transform.rb +29 -0
- data/lib/vault/api/transform/alphabet.rb +43 -0
- data/lib/vault/api/transform/role.rb +42 -0
- data/lib/vault/api/transform/template.rb +54 -0
- data/lib/vault/api/transform/transformation.rb +61 -0
- data/lib/vault/client.rb +18 -4
- data/lib/vault/configurable.rb +1 -0
- data/lib/vault/defaults.rb +7 -0
- data/lib/vault/request.rb +1 -0
- data/lib/vault/version.rb +1 -1
- data/vault.gemspec +4 -4
- metadata +25 -19
- data/.travis.yml +0 -26
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
|
-
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: cd39dd364209eee99a269200710d73fbdd4d9fe7007b5b8db78d1861b5671c53
|
4
|
+
data.tar.gz: 3b9cd61321644f7a45d87e7f08dadd7cb6c20da6537e46a01f82b2975d302852
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 9a5e084c51cf4528095d4911acf8b983e1b61673441862fee7a58c640faae85bd81157a2a53d64db3fe8387136f8ceff7da2ea33e295ad6c8813b1e9fd3ff4a0
|
7
|
+
data.tar.gz: '0256808c555dece8ff08124fbdfad7a92b054db0c85f3e0e3d907d28b30c9d3f5d4b9e9a96287c1f2b8697e06dfdab57f8ddc0ee66a0947e780b7833e196bbc2'
|
@@ -0,0 +1,42 @@
|
|
1
|
+
version: 2.1
|
2
|
+
|
3
|
+
references:
|
4
|
+
images:
|
5
|
+
ubuntu: &UBUNTU_IMAGE ubuntu-1604:201903-01
|
6
|
+
|
7
|
+
jobs:
|
8
|
+
test:
|
9
|
+
machine:
|
10
|
+
image: *UBUNTU_IMAGE
|
11
|
+
parameters:
|
12
|
+
ruby-version:
|
13
|
+
type: string
|
14
|
+
vault-version:
|
15
|
+
type: string
|
16
|
+
steps:
|
17
|
+
- checkout
|
18
|
+
- run:
|
19
|
+
name: Install vault
|
20
|
+
command: |
|
21
|
+
curl -sLo vault.zip https://releases.hashicorp.com/vault/<< parameters.vault-version >>/vault_<< parameters.vault-version >>_linux_amd64.zip
|
22
|
+
unzip vault.zip
|
23
|
+
mkdir -p ~/bin
|
24
|
+
mv vault ~/bin
|
25
|
+
export PATH="~/bin:$PATH"
|
26
|
+
- run:
|
27
|
+
name: Run tests
|
28
|
+
command: |
|
29
|
+
export VAULT_VERSION=<< parameters.vault-version >>
|
30
|
+
rvm use << parameters.ruby-version >> --install --binary --fuzzy
|
31
|
+
bundle install --jobs=3 --retry=3 --path=vendor/bundle
|
32
|
+
bundle exec rake
|
33
|
+
|
34
|
+
workflows:
|
35
|
+
run-tests:
|
36
|
+
jobs:
|
37
|
+
- test:
|
38
|
+
matrix:
|
39
|
+
parameters:
|
40
|
+
ruby-version: ["2.2", "2.3", "2.4"]
|
41
|
+
vault-version: ["1.0.3", "1.1.5", "1.2.4", "1.3.0"]
|
42
|
+
name: test-ruby-<< matrix.ruby-version >>-vault-<< matrix.vault-version >>
|
data/.gitignore
CHANGED
data/CHANGELOG.md
CHANGED
@@ -1,5 +1,45 @@
|
|
1
1
|
# Vault Ruby Changelog
|
2
2
|
|
3
|
+
## v0.14.0 (May 28, 2020)
|
4
|
+
|
5
|
+
IMPROVEMENTS
|
6
|
+
|
7
|
+
- Added support for the Transform Secrets Engine
|
8
|
+
|
9
|
+
## v0.13.2 (May 7, 2020)
|
10
|
+
|
11
|
+
BUG FIXES
|
12
|
+
|
13
|
+
- Fixed the ability to use namespace as an option for each request. Previously, that option was ignored.
|
14
|
+
- aws-sigv4 gem was unlocked after a bug in 1.1.2 broke CI
|
15
|
+
|
16
|
+
## v0.13.1 (April 28, 2020)
|
17
|
+
|
18
|
+
IMPROVEMENTS
|
19
|
+
|
20
|
+
- Added support for defining a namespace when initializing the client, as well as options for changing the namespace via method.
|
21
|
+
- Added support for sys/namespaces API. Ability to Get, Create, Delete, and List namespaces has been provided.
|
22
|
+
|
23
|
+
## v0.13.0 (October 1, 2019)
|
24
|
+
|
25
|
+
IMPROVEMENTS
|
26
|
+
|
27
|
+
- Add support for versioned KV secrets in the client
|
28
|
+
|
29
|
+
## v0.12.0 (August 14, 2018)
|
30
|
+
|
31
|
+
IMPROVEMENTS
|
32
|
+
|
33
|
+
- Expose the github login path as an optional argument
|
34
|
+
- Support HTTP basic auth [GH-181]
|
35
|
+
- Expose the AWS IAM path to use [GH-180]
|
36
|
+
- Add GCP Auth [GH-173]
|
37
|
+
- Add shutdown functionality to close persistent connections [GH-175]
|
38
|
+
|
39
|
+
BUG FIXES
|
40
|
+
|
41
|
+
- Specifing the hostname for SNI didn't work. The functionality has been disabled for now.
|
42
|
+
|
3
43
|
## v0.11.0 (March 19, 2018)
|
4
44
|
|
5
45
|
IMPROVEMENTS
|
data/README.md
CHANGED
@@ -28,6 +28,8 @@ Start a Vault client:
|
|
28
28
|
```ruby
|
29
29
|
Vault.address = "http://127.0.0.1:8200" # Also reads from ENV["VAULT_ADDR"]
|
30
30
|
Vault.token = "abcd-1234" # Also reads from ENV["VAULT_TOKEN"]
|
31
|
+
# Optional - if using the Namespace enterprise feature
|
32
|
+
# Vault.namespace = "my-namespace" # Also reads from ENV["VAULT_NAMESPACE"]
|
31
33
|
|
32
34
|
Vault.sys.mounts #=> { :secret => #<struct Vault::Mount type="generic", description="generic secret storage"> }
|
33
35
|
```
|
@@ -43,6 +45,8 @@ Vault.configure do |config|
|
|
43
45
|
|
44
46
|
# The token to authenticate with Vault, also read as ENV["VAULT_TOKEN"]
|
45
47
|
config.token = "abcd-1234"
|
48
|
+
# Optional - if using the Namespace enterprise feature
|
49
|
+
# config.namespace = "my-namespace" # Also reads from ENV["VAULT_NAMESPACE"]
|
46
50
|
|
47
51
|
# Proxy connection information, also read as ENV["VAULT_PROXY_(thing)"]
|
48
52
|
config.proxy_address = "..."
|
@@ -85,7 +89,8 @@ And if you want to authenticate with a `AWS EC2` :
|
|
85
89
|
# Export VAULT_ADDR to ENV then
|
86
90
|
# Get the pkcs7 value from AWS
|
87
91
|
signature = `curl http://169.254.169.254/latest/dynamic/instance-identity/pkcs7`
|
88
|
-
|
92
|
+
iam_role = `curl http://169.254.169.254/latest/meta-data/iam/security-credentials/`
|
93
|
+
vault_token = Vault.auth.aws_ec2(iam_role, signature, nil)
|
89
94
|
vault_client = Vault::Client.new(address: ENV["VAULT_ADDR"], token: vault_token.auth.client_token)
|
90
95
|
```
|
91
96
|
|
@@ -117,7 +122,9 @@ For advanced users, the first argument of the block is the attempt number and th
|
|
117
122
|
|
118
123
|
```ruby
|
119
124
|
Vault.with_retries(Vault::HTTPConnectionError, Vault::HTTPError) do |attempt, e|
|
120
|
-
|
125
|
+
if e
|
126
|
+
log "Received exception #{e} from Vault - attempt #{attempt}"
|
127
|
+
end
|
121
128
|
Vault.logical.read("secret/bacon")
|
122
129
|
end
|
123
130
|
```
|
@@ -206,7 +213,8 @@ Development
|
|
206
213
|
|
207
214
|
Important Notes:
|
208
215
|
|
209
|
-
- **All new features must include test coverage.** At a bare minimum, Unit tests are required. It is preferred if you include
|
216
|
+
- **All new features must include test coverage.** At a bare minimum, Unit tests are required. It is preferred if you include integration tests as well.
|
210
217
|
- **The tests must be be idempotent.** The HTTP calls made during a test should be able to be run over and over.
|
211
218
|
- **Tests are order independent.** The default RSpec configuration randomizes the test order, so this should not be a problem.
|
212
219
|
- **Integration tests require Vault** Vault must be available in the path for the integration tests to pass.
|
220
|
+
- **In order to be considered an integration test:** The test MUST use the `vault_test_client` or `vault_redirect_test_client` as the client. This spawns a process, or uses an already existing process from another test, to run against.
|
data/lib/vault/api.rb
CHANGED
@@ -5,8 +5,10 @@ module Vault
|
|
5
5
|
require_relative "api/auth_tls"
|
6
6
|
require_relative "api/auth"
|
7
7
|
require_relative "api/help"
|
8
|
+
require_relative "api/kv"
|
8
9
|
require_relative "api/logical"
|
9
10
|
require_relative "api/secret"
|
10
11
|
require_relative "api/sys"
|
12
|
+
require_relative "api/transform"
|
11
13
|
end
|
12
14
|
end
|
data/lib/vault/api/auth.rb
CHANGED
@@ -155,9 +155,9 @@ module Vault
|
|
155
155
|
# @param [String] github_token
|
156
156
|
#
|
157
157
|
# @return [Secret]
|
158
|
-
def github(github_token)
|
158
|
+
def github(github_token, path="/v1/auth/github/login")
|
159
159
|
payload = {token: github_token}
|
160
|
-
json = client.post(
|
160
|
+
json = client.post(path, JSON.fast_generate(payload))
|
161
161
|
secret = Secret.decode(json)
|
162
162
|
client.token = secret.auth.client_token
|
163
163
|
return secret
|
@@ -193,7 +193,7 @@ module Vault
|
|
193
193
|
# for future requests.
|
194
194
|
#
|
195
195
|
# @example
|
196
|
-
# Vault.auth.aws_iam("dev-role-iam", Aws::
|
196
|
+
# Vault.auth.aws_iam("dev-role-iam", Aws::InstanceProfileCredentials.new, "vault.example.com", "https://sts.us-east-2.amazonaws.com") #=> #<Vault::Secret lease_id="">
|
197
197
|
#
|
198
198
|
# @param [String] role
|
199
199
|
# @param [CredentialProvider] credentials_provider
|
@@ -202,14 +202,17 @@ module Vault
|
|
202
202
|
# As of Jan 2018, Vault will accept ANY or NO header if none is configured by the Vault server admin
|
203
203
|
# @param [String] sts_endpoint optional
|
204
204
|
# https://docs.aws.amazon.com/IAM/latest/UserGuide/id_credentials_temp_enable-regions.html
|
205
|
+
# @param [String] route optional
|
205
206
|
# @return [Secret]
|
206
|
-
def aws_iam(role, credentials_provider, iam_auth_header_value = nil, sts_endpoint = 'https://sts.amazonaws.com')
|
207
|
+
def aws_iam(role, credentials_provider, iam_auth_header_value = nil, sts_endpoint = 'https://sts.amazonaws.com', route = nil)
|
207
208
|
require "aws-sigv4"
|
208
209
|
require "base64"
|
209
210
|
|
210
211
|
request_body = 'Action=GetCallerIdentity&Version=2011-06-15'
|
211
212
|
request_method = 'POST'
|
212
213
|
|
214
|
+
route ||= '/v1/auth/aws/login'
|
215
|
+
|
213
216
|
vault_headers = {
|
214
217
|
'User-Agent' => Vault::Client::USER_AGENT,
|
215
218
|
'Content-Type' => 'application/x-www-form-urlencoded; charset=utf-8'
|
@@ -236,7 +239,29 @@ module Vault
|
|
236
239
|
iam_request_body: Base64.strict_encode64(request_body)
|
237
240
|
}
|
238
241
|
|
239
|
-
json = client.post(
|
242
|
+
json = client.post(route, JSON.fast_generate(payload))
|
243
|
+
secret = Secret.decode(json)
|
244
|
+
client.token = secret.auth.client_token
|
245
|
+
return secret
|
246
|
+
end
|
247
|
+
|
248
|
+
# Authenticate via the GCP authentication method. If authentication is
|
249
|
+
# successful, the resulting token will be stored on the client and used
|
250
|
+
# for future requests.
|
251
|
+
#
|
252
|
+
# @example
|
253
|
+
# Vault.auth.gcp("read-only", "jwt", "gcp") #=> #<Vault::Secret lease_id="">
|
254
|
+
#
|
255
|
+
# @param [String] role
|
256
|
+
# @param [String] jwt
|
257
|
+
# jwt returned by the instance identity metadata, or iam api
|
258
|
+
# @param [String] path optional
|
259
|
+
# the path were the gcp auth backend is mounted
|
260
|
+
#
|
261
|
+
# @return [Secret]
|
262
|
+
def gcp(role, jwt, path = 'gcp')
|
263
|
+
payload = { role: role, jwt: jwt }
|
264
|
+
json = client.post("/v1/auth/#{CGI.escape(path)}/login", JSON.fast_generate(payload))
|
240
265
|
secret = Secret.decode(json)
|
241
266
|
client.token = secret.auth.client_token
|
242
267
|
return secret
|
data/lib/vault/api/kv.rb
ADDED
@@ -0,0 +1,207 @@
|
|
1
|
+
require_relative "secret"
|
2
|
+
require_relative "../client"
|
3
|
+
require_relative "../request"
|
4
|
+
require_relative "../response"
|
5
|
+
|
6
|
+
module Vault
|
7
|
+
class Client
|
8
|
+
# A proxy to the {KV} methods.
|
9
|
+
# @return [KV]
|
10
|
+
def kv(mount)
|
11
|
+
KV.new(self, mount)
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
class KV < Request
|
16
|
+
attr_reader :mount
|
17
|
+
|
18
|
+
def initialize(client, mount)
|
19
|
+
super client
|
20
|
+
|
21
|
+
@mount = mount
|
22
|
+
end
|
23
|
+
|
24
|
+
# List the names of secrets at the given path, if the path supports
|
25
|
+
# listing. If the the path does not exist, an empty array will be returned.
|
26
|
+
#
|
27
|
+
# @example
|
28
|
+
# Vault.kv("secret").list("foo") #=> ["bar", "baz"]
|
29
|
+
#
|
30
|
+
# @param [String] path
|
31
|
+
# the path to list
|
32
|
+
#
|
33
|
+
# @return [Array<String>]
|
34
|
+
def list(path = "", options = {})
|
35
|
+
headers = extract_headers!(options)
|
36
|
+
json = client.list("/v1/#{mount}/metadata/#{encode_path(path)}", {}, headers)
|
37
|
+
json[:data][:keys] || []
|
38
|
+
rescue HTTPError => e
|
39
|
+
return [] if e.code == 404
|
40
|
+
raise
|
41
|
+
end
|
42
|
+
|
43
|
+
# Read the secret at the given path. If the secret does not exist, +nil+
|
44
|
+
# will be returned. The latest version is returned by default, but you
|
45
|
+
# can request a specific version.
|
46
|
+
#
|
47
|
+
# @example
|
48
|
+
# Vault.kv("secret").read("password") #=> #<Vault::Secret lease_id="">
|
49
|
+
#
|
50
|
+
# @param [String] path
|
51
|
+
# the path to read
|
52
|
+
# @param [Integer] version
|
53
|
+
# the version of the secret
|
54
|
+
#
|
55
|
+
# @return [Secret, nil]
|
56
|
+
def read(path, version = nil, options = {})
|
57
|
+
headers = extract_headers!(options)
|
58
|
+
params = {}
|
59
|
+
params[:version] = version unless version.nil?
|
60
|
+
|
61
|
+
json = client.get("/v1/#{mount}/data/#{encode_path(path)}", params, headers)
|
62
|
+
return Secret.decode(json[:data])
|
63
|
+
rescue HTTPError => e
|
64
|
+
return nil if e.code == 404
|
65
|
+
raise
|
66
|
+
end
|
67
|
+
|
68
|
+
# Read the metadata of a secret at the given path. If the secret does not
|
69
|
+
# exist, nil will be returned.
|
70
|
+
#
|
71
|
+
# @example
|
72
|
+
# Vault.kv("secret").read_metadata("password") => {...}
|
73
|
+
#
|
74
|
+
# @param [String] path
|
75
|
+
# the path to read
|
76
|
+
#
|
77
|
+
# @return [Hash, nil]
|
78
|
+
def read_metadata(path)
|
79
|
+
client.get("/v1/#{mount}/metadata/#{encode_path(path)}")[:data]
|
80
|
+
rescue HTTPError => e
|
81
|
+
return nil if e.code == 404
|
82
|
+
raise
|
83
|
+
end
|
84
|
+
|
85
|
+
# Write the secret at the given path with the given data. Note that the
|
86
|
+
# data must be a {Hash}!
|
87
|
+
#
|
88
|
+
# @example
|
89
|
+
# Vault.logical.write("secret/password", value: "secret") #=> #<Vault::Secret lease_id="">
|
90
|
+
#
|
91
|
+
# @param [String] path
|
92
|
+
# the path to write
|
93
|
+
# @param [Hash] data
|
94
|
+
# the data to write
|
95
|
+
#
|
96
|
+
# @return [Secret]
|
97
|
+
def write(path, data = {}, options = {})
|
98
|
+
headers = extract_headers!(options)
|
99
|
+
json = client.post("/v1/#{mount}/data/#{encode_path(path)}", JSON.fast_generate(:data => data), headers)
|
100
|
+
if json.nil?
|
101
|
+
return true
|
102
|
+
else
|
103
|
+
return Secret.decode(json)
|
104
|
+
end
|
105
|
+
end
|
106
|
+
|
107
|
+
# Write the metadata of a secret at the given path. Note that the data must
|
108
|
+
# be a {Hash}.
|
109
|
+
#
|
110
|
+
# @example
|
111
|
+
# Vault.kv("secret").write_metadata("password", max_versions => 3)
|
112
|
+
#
|
113
|
+
# @param [String] path
|
114
|
+
# the path to write
|
115
|
+
# @param [Hash] metadata
|
116
|
+
# the metadata to write
|
117
|
+
#
|
118
|
+
# @return [true]
|
119
|
+
def write_metadata(path, metadata = {})
|
120
|
+
client.post("/v1/#{mount}/metadata/#{encode_path(path)}", JSON.fast_generate(metadata))
|
121
|
+
|
122
|
+
true
|
123
|
+
end
|
124
|
+
|
125
|
+
# Delete the secret at the given path. If the secret does not exist, vault
|
126
|
+
# will still return true.
|
127
|
+
#
|
128
|
+
# @example
|
129
|
+
# Vault.logical.delete("secret/password") #=> true
|
130
|
+
#
|
131
|
+
# @param [String] path
|
132
|
+
# the path to delete
|
133
|
+
#
|
134
|
+
# @return [true]
|
135
|
+
def delete(path)
|
136
|
+
client.delete("/v1/#{mount}/data/#{encode_path(path)}")
|
137
|
+
|
138
|
+
true
|
139
|
+
end
|
140
|
+
|
141
|
+
# Mark specific versions of a secret as deleted.
|
142
|
+
#
|
143
|
+
# @example
|
144
|
+
# Vault.kv("secret").delete_versions("password", [1, 2])
|
145
|
+
#
|
146
|
+
# @param [String] path
|
147
|
+
# the path to remove versions from
|
148
|
+
# @param [Array<Integer>] versions
|
149
|
+
# an array of versions to remove
|
150
|
+
#
|
151
|
+
# @return [true]
|
152
|
+
def delete_versions(path, versions)
|
153
|
+
client.post("/v1/#{mount}/delete/#{encode_path(path)}", JSON.fast_generate(versions: versions))
|
154
|
+
|
155
|
+
true
|
156
|
+
end
|
157
|
+
|
158
|
+
# Mark specific versions of a secret as active.
|
159
|
+
#
|
160
|
+
# @example
|
161
|
+
# Vault.kv("secret").undelete_versions("password", [1, 2])
|
162
|
+
#
|
163
|
+
# @param [String] path
|
164
|
+
# the path to enable versions for
|
165
|
+
# @param [Array<Integer>] versions
|
166
|
+
# an array of versions to mark as undeleted
|
167
|
+
#
|
168
|
+
# @return [true]
|
169
|
+
def undelete_versions(path, versions)
|
170
|
+
client.post("/v1/#{mount}/undelete/#{encode_path(path)}", JSON.fast_generate(versions: versions))
|
171
|
+
|
172
|
+
true
|
173
|
+
end
|
174
|
+
|
175
|
+
# Completely remove a secret and its metadata.
|
176
|
+
#
|
177
|
+
# @example
|
178
|
+
# Vault.kv("secret").destroy("password")
|
179
|
+
#
|
180
|
+
# @param [String] path
|
181
|
+
# the path to remove
|
182
|
+
#
|
183
|
+
# @return [true]
|
184
|
+
def destroy(path)
|
185
|
+
client.delete("/v1/#{mount}/metadata/#{encode_path(path)}")
|
186
|
+
|
187
|
+
true
|
188
|
+
end
|
189
|
+
|
190
|
+
# Completely remove specific versions of a secret.
|
191
|
+
#
|
192
|
+
# @example
|
193
|
+
# Vault.kv("secret").destroy_versions("password", [1, 2])
|
194
|
+
#
|
195
|
+
# @param [String] path
|
196
|
+
# the path to remove versions from
|
197
|
+
# @param [Array<Integer>] versions
|
198
|
+
# an array of versions to destroy
|
199
|
+
#
|
200
|
+
# @return [true]
|
201
|
+
def destroy_versions(path, versions)
|
202
|
+
client.post("/v1/#{mount}/destroy/#{encode_path(path)}", JSON.fast_generate(versions: versions))
|
203
|
+
|
204
|
+
true
|
205
|
+
end
|
206
|
+
end
|
207
|
+
end
|
data/lib/vault/api/secret.rb
CHANGED
@@ -32,6 +32,18 @@ module Vault
|
|
32
32
|
# @return [Hash<Symbol, Object>]
|
33
33
|
field :data, freeze: true
|
34
34
|
|
35
|
+
# @!attribute [r] metadata
|
36
|
+
# Read-only metadata information related to the secret.
|
37
|
+
#
|
38
|
+
# @example Reading metadata
|
39
|
+
# secret = Vault.logical(:versioned).read("secret", "foo")
|
40
|
+
# secret.metadata[:created_time] #=> "2018-12-08T04:22:54.168065Z"
|
41
|
+
# secret.metadata[:version] #=> 1
|
42
|
+
# secret.metadata[:destroyed] #=> false
|
43
|
+
#
|
44
|
+
# @return [Hash<Symbol, Object>]
|
45
|
+
field :metadata, freeze: true
|
46
|
+
|
35
47
|
# @!attribute [r] lease_duration
|
36
48
|
# The number of seconds this lease is valid. If this number is 0 or nil,
|
37
49
|
# the secret does not expire.
|
data/lib/vault/api/sys.rb
CHANGED
data/lib/vault/api/sys/mount.rb
CHANGED
@@ -16,6 +16,11 @@ module Vault
|
|
16
16
|
# Type of the mount.
|
17
17
|
# @return [String]
|
18
18
|
field :type
|
19
|
+
|
20
|
+
# @!attribute [r] type
|
21
|
+
# Options given to the mount.
|
22
|
+
# @return [Hash<Symbol, Object>]
|
23
|
+
field :options
|
19
24
|
end
|
20
25
|
|
21
26
|
class Sys < Request
|
@@ -44,8 +49,8 @@ module Vault
|
|
44
49
|
# the type of mount
|
45
50
|
# @param [String] description
|
46
51
|
# a human-friendly description (optional)
|
47
|
-
def mount(path, type, description = nil)
|
48
|
-
payload =
|
52
|
+
def mount(path, type, description = nil, options = {})
|
53
|
+
payload = options.merge type: type
|
49
54
|
payload[:description] = description if !description.nil?
|
50
55
|
|
51
56
|
client.post("/v1/sys/mounts/#{encode_path(path)}", JSON.fast_generate(payload))
|
@@ -0,0 +1,85 @@
|
|
1
|
+
module Vault
|
2
|
+
class Namespace < Response
|
3
|
+
# @!attribute [r] id
|
4
|
+
# ID of the namespace
|
5
|
+
# @return [String]
|
6
|
+
field :id
|
7
|
+
|
8
|
+
# @!attribute [r] path
|
9
|
+
# Path of the namespace, includes parent paths if nested.
|
10
|
+
# @return [String]
|
11
|
+
field :path
|
12
|
+
end
|
13
|
+
|
14
|
+
class Sys
|
15
|
+
# List all namespaces in a given scope. Ignores nested namespaces.
|
16
|
+
#
|
17
|
+
# @example
|
18
|
+
# Vault.sys.namespaces #=> { :foo => #<struct Vault::Namespace id="xxxx1", path="foo/" }
|
19
|
+
#
|
20
|
+
# @return [Hash<Symbol, Namespace>]
|
21
|
+
#
|
22
|
+
# NOTE: Due to a bug in Vault Enterprise, to be fixed soon, this method CAN return a pure JSON string if a scoping namespace is provided.
|
23
|
+
def namespaces(scoped=nil)
|
24
|
+
path = ["v1", scoped, "sys", "namespaces"].compact
|
25
|
+
json = client.list(path.join("/"))
|
26
|
+
json = json[:data] if json[:data]
|
27
|
+
if json[:key_info]
|
28
|
+
json = json[:key_info]
|
29
|
+
hash = {}
|
30
|
+
json.each do |k,v|
|
31
|
+
hash[k.to_s.chomp("/").to_sym] = Namespace.decode(v)
|
32
|
+
end
|
33
|
+
hash
|
34
|
+
else
|
35
|
+
json
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
# Create a namespace. Nests the namespace if a namespace header is provided.
|
40
|
+
#
|
41
|
+
# @example
|
42
|
+
# Vault.sys.create_namespace("foo")
|
43
|
+
#
|
44
|
+
# @param [String] namespace
|
45
|
+
# the potential path of the namespace, without any parent path provided
|
46
|
+
#
|
47
|
+
# @return [true]
|
48
|
+
def create_namespace(namespace)
|
49
|
+
client.put("/v1/sys/namespaces/#{namespace}", {})
|
50
|
+
return true
|
51
|
+
end
|
52
|
+
|
53
|
+
# Delete a namespace. Raises an error if the namespace provided is not empty.
|
54
|
+
#
|
55
|
+
# @example
|
56
|
+
# Vault.sys.delete_namespace("foo")
|
57
|
+
#
|
58
|
+
# @param [String] namespace
|
59
|
+
# the path of the namespace to be deleted
|
60
|
+
#
|
61
|
+
# @return [true]
|
62
|
+
def delete_namespace(namespace)
|
63
|
+
client.delete("/v1/sys/namespaces/#{namespace}")
|
64
|
+
return true
|
65
|
+
end
|
66
|
+
|
67
|
+
# Retrieve a namespace by path.
|
68
|
+
#
|
69
|
+
# @example
|
70
|
+
# Vault.sys.get_namespace("foo")
|
71
|
+
#
|
72
|
+
# @param [String] namespace
|
73
|
+
# the path of the namespace ot be retrieved
|
74
|
+
#
|
75
|
+
# @return [Namespace]
|
76
|
+
def get_namespace(namespace)
|
77
|
+
json = client.get("/v1/sys/namespaces/#{namespace}")
|
78
|
+
if data = json.dig(:data)
|
79
|
+
Namespace.decode(data)
|
80
|
+
else
|
81
|
+
json
|
82
|
+
end
|
83
|
+
end
|
84
|
+
end
|
85
|
+
end
|
@@ -0,0 +1,29 @@
|
|
1
|
+
require_relative '../client'
|
2
|
+
require_relative '../request'
|
3
|
+
|
4
|
+
module Vault
|
5
|
+
class Client
|
6
|
+
# A proxy to the {Transform} methods.
|
7
|
+
# @return [Transform]
|
8
|
+
def transform
|
9
|
+
@transform ||= Transform.new(self)
|
10
|
+
end
|
11
|
+
end
|
12
|
+
|
13
|
+
class Transform < Request
|
14
|
+
def encode(role_name:, **opts)
|
15
|
+
opts ||= {}
|
16
|
+
client.post("/v1/transform/encode/#{encode_path(role_name)}", JSON.fast_generate(opts))
|
17
|
+
end
|
18
|
+
|
19
|
+
def decode(role_name:, **opts)
|
20
|
+
opts ||= {}
|
21
|
+
client.post("/v1/transform/decode/#{encode_path(role_name)}", JSON.fast_generate(opts))
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
require_relative 'transform/alphabet'
|
27
|
+
require_relative 'transform/role'
|
28
|
+
require_relative 'transform/template'
|
29
|
+
require_relative 'transform/transformation'
|
@@ -0,0 +1,43 @@
|
|
1
|
+
require_relative '../../request'
|
2
|
+
require_relative '../../response'
|
3
|
+
|
4
|
+
module Vault
|
5
|
+
class Transform < Request
|
6
|
+
class Alphabet < Response
|
7
|
+
# @!attribute [r] id
|
8
|
+
# String listing all possible characters of the alphabet
|
9
|
+
# @return [String]
|
10
|
+
field :alphabet
|
11
|
+
end
|
12
|
+
|
13
|
+
def create_alphabet(name, alphabet:, **opts)
|
14
|
+
opts ||= {}
|
15
|
+
opts[:alphabet] = alphabet
|
16
|
+
client.post("/v1/transform/alphabet/#{encode_path(name)}", JSON.fast_generate(opts))
|
17
|
+
return true
|
18
|
+
end
|
19
|
+
|
20
|
+
def get_alphabet(name)
|
21
|
+
json = client.get("/v1/transform/alphabet/#{encode_path(name)}")
|
22
|
+
if data = json.dig(:data)
|
23
|
+
Alphabet.decode(data)
|
24
|
+
else
|
25
|
+
json
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
def delete_alphabet(name)
|
30
|
+
client.delete("/v1/transform/alphabet/#{encode_path(name)}")
|
31
|
+
true
|
32
|
+
end
|
33
|
+
|
34
|
+
def alphabets
|
35
|
+
json = client.list("/v1/transform/alphabet")
|
36
|
+
if keys = json.dig(:data, :keys)
|
37
|
+
keys
|
38
|
+
else
|
39
|
+
json
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
@@ -0,0 +1,42 @@
|
|
1
|
+
require_relative '../../request'
|
2
|
+
require_relative '../../response'
|
3
|
+
|
4
|
+
module Vault
|
5
|
+
class Transform < Request
|
6
|
+
class Role < Response
|
7
|
+
# @!attribute [r] transformations
|
8
|
+
# Array of all transformations the role has access to
|
9
|
+
# @return [Array<String>]
|
10
|
+
field :transformations
|
11
|
+
end
|
12
|
+
|
13
|
+
def create_role(name, **opts)
|
14
|
+
opts ||= {}
|
15
|
+
client.post("/v1/transform/role/#{encode_path(name)}", JSON.fast_generate(opts))
|
16
|
+
return true
|
17
|
+
end
|
18
|
+
|
19
|
+
def get_role(name)
|
20
|
+
json = client.get("/v1/transform/role/#{encode_path(name)}")
|
21
|
+
if data = json.dig(:data)
|
22
|
+
Role.decode(data)
|
23
|
+
else
|
24
|
+
json
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
def delete_role(name)
|
29
|
+
client.delete("/v1/transform/role/#{encode_path(name)}")
|
30
|
+
true
|
31
|
+
end
|
32
|
+
|
33
|
+
def roles
|
34
|
+
json = client.list("/v1/transform/role")
|
35
|
+
if keys = json.dig(:data, :keys)
|
36
|
+
keys
|
37
|
+
else
|
38
|
+
json
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
@@ -0,0 +1,54 @@
|
|
1
|
+
require_relative '../../request'
|
2
|
+
require_relative '../../response'
|
3
|
+
|
4
|
+
module Vault
|
5
|
+
class Transform < Request
|
6
|
+
class Template < Response
|
7
|
+
# @!attribute [r] alphabet
|
8
|
+
# Name of the alphabet to be used in the template
|
9
|
+
# @return [String]
|
10
|
+
field :alphabet
|
11
|
+
|
12
|
+
# @!attribute [r] pattern
|
13
|
+
# Regex string to detect and match for the template
|
14
|
+
# @return [String]
|
15
|
+
field :pattern
|
16
|
+
|
17
|
+
# @!attribute [r] type
|
18
|
+
# Type of the template, currently, only "regex" is supported
|
19
|
+
# @return [String]
|
20
|
+
field :type
|
21
|
+
end
|
22
|
+
|
23
|
+
def create_template(name, type:, pattern:, **opts)
|
24
|
+
opts ||= {}
|
25
|
+
opts[:type] = type
|
26
|
+
opts[:pattern] = pattern
|
27
|
+
client.post("/v1/transform/template/#{encode_path(name)}", JSON.fast_generate(opts))
|
28
|
+
return true
|
29
|
+
end
|
30
|
+
|
31
|
+
def get_template(name)
|
32
|
+
json = client.get("/v1/transform/template/#{encode_path(name)}")
|
33
|
+
if data = json.dig(:data)
|
34
|
+
Template.decode(data)
|
35
|
+
else
|
36
|
+
json
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
def delete_template(name)
|
41
|
+
client.delete("/v1/transform/template/#{encode_path(name)}")
|
42
|
+
true
|
43
|
+
end
|
44
|
+
|
45
|
+
def templates
|
46
|
+
json = client.list("/v1/transform/template")
|
47
|
+
if keys = json.dig(:data, :keys)
|
48
|
+
keys
|
49
|
+
else
|
50
|
+
json
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
@@ -0,0 +1,61 @@
|
|
1
|
+
require_relative '../../request'
|
2
|
+
require_relative '../../response'
|
3
|
+
|
4
|
+
module Vault
|
5
|
+
class Transform < Request
|
6
|
+
class Transformation < Response
|
7
|
+
# @!attribute [r] allowed_roles
|
8
|
+
# Array of role names that are allowed to use this transformation
|
9
|
+
# @return [Array<String>]
|
10
|
+
field :allowed_roles
|
11
|
+
|
12
|
+
# @!attribute [r] templates
|
13
|
+
# Array of template names accessible to this transformation
|
14
|
+
# @return [Array<String>]
|
15
|
+
field :templates
|
16
|
+
|
17
|
+
# @!attribute [r] tweak_source
|
18
|
+
# String representing how a tweak is provided for this transformation.
|
19
|
+
# Available tweaks are "supplied", "generated", and "internal"
|
20
|
+
# @return [String]
|
21
|
+
field :tweak_source
|
22
|
+
|
23
|
+
# @!attribute [r] type
|
24
|
+
# String representing the type of transformation this is.
|
25
|
+
# Available types are "fpe", and "masking"
|
26
|
+
# @return [String]
|
27
|
+
field :type
|
28
|
+
end
|
29
|
+
|
30
|
+
def create_transformation(name, type:, template:, **opts)
|
31
|
+
opts ||= {}
|
32
|
+
opts[:type] = type
|
33
|
+
opts[:template] = template
|
34
|
+
client.post("/v1/transform/transformation/#{encode_path(name)}", JSON.fast_generate(opts))
|
35
|
+
return true
|
36
|
+
end
|
37
|
+
|
38
|
+
def get_transformation(name)
|
39
|
+
json = client.get("/v1/transform/transformation/#{encode_path(name)}")
|
40
|
+
if data = json.dig(:data)
|
41
|
+
Transformation.decode(data)
|
42
|
+
else
|
43
|
+
json
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
def delete_transformation(name)
|
48
|
+
client.delete("/v1/transform/transformation/#{encode_path(name)}")
|
49
|
+
true
|
50
|
+
end
|
51
|
+
|
52
|
+
def transformations
|
53
|
+
json = client.list("/v1/transform/transformation")
|
54
|
+
if keys = json.dig(:data, :keys)
|
55
|
+
keys
|
56
|
+
else
|
57
|
+
json
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
data/lib/vault/client.rb
CHANGED
@@ -16,6 +16,9 @@ module Vault
|
|
16
16
|
# The name of the header used to hold the Vault token.
|
17
17
|
TOKEN_HEADER = "X-Vault-Token".freeze
|
18
18
|
|
19
|
+
# The name of the header used to hold the Namespace.
|
20
|
+
NAMESPACE_HEADER = "X-Vault-Namespace".freeze
|
21
|
+
|
19
22
|
# The name of the header used to hold the wrapped request ttl.
|
20
23
|
WRAP_TTL_HEADER = "X-Vault-Wrap-TTL".freeze
|
21
24
|
|
@@ -85,10 +88,6 @@ module Vault
|
|
85
88
|
|
86
89
|
@nhp = PersistentHTTP.new("vault-ruby", nil, pool_size)
|
87
90
|
|
88
|
-
if hostname
|
89
|
-
@nhp.hostname = hostname
|
90
|
-
end
|
91
|
-
|
92
91
|
if proxy_address
|
93
92
|
proxy_uri = URI.parse "http://#{proxy_address}"
|
94
93
|
|
@@ -158,6 +157,12 @@ module Vault
|
|
158
157
|
|
159
158
|
private :pool
|
160
159
|
|
160
|
+
# Shutdown any open pool connections. Pool will be recreated upon next request.
|
161
|
+
def shutdown
|
162
|
+
@nhp.shutdown()
|
163
|
+
@nhp = nil
|
164
|
+
end
|
165
|
+
|
161
166
|
# Creates and yields a new client object with the given token. This may be
|
162
167
|
# used safely in a threadsafe manner because the original client remains
|
163
168
|
# unchanged. The value of the block is returned.
|
@@ -236,6 +241,9 @@ module Vault
|
|
236
241
|
# Build the URI and request object from the given information
|
237
242
|
uri = build_uri(verb, path, data)
|
238
243
|
request = class_for_request(verb).new(uri.request_uri)
|
244
|
+
if uri.userinfo()
|
245
|
+
request.basic_auth uri.user, uri.password
|
246
|
+
end
|
239
247
|
|
240
248
|
if proxy_address and uri.scheme.downcase == "https"
|
241
249
|
raise SecurityError, "no direct https connection to vault"
|
@@ -250,6 +258,12 @@ module Vault
|
|
250
258
|
headers[TOKEN_HEADER] ||= token
|
251
259
|
end
|
252
260
|
|
261
|
+
# Add the Vault Namespace header - users could still override this on a
|
262
|
+
# per-request basis
|
263
|
+
if !namespace.nil?
|
264
|
+
headers[NAMESPACE_HEADER] ||= namespace
|
265
|
+
end
|
266
|
+
|
253
267
|
# Add headers
|
254
268
|
headers.each do |key, value|
|
255
269
|
request.add_field(key, value)
|
data/lib/vault/configurable.rb
CHANGED
data/lib/vault/defaults.rb
CHANGED
data/lib/vault/request.rb
CHANGED
data/lib/vault/version.rb
CHANGED
data/vault.gemspec
CHANGED
@@ -21,10 +21,10 @@ Gem::Specification.new do |spec|
|
|
21
21
|
|
22
22
|
spec.add_runtime_dependency "aws-sigv4"
|
23
23
|
|
24
|
-
spec.add_development_dependency "bundler"
|
25
|
-
spec.add_development_dependency "pry"
|
24
|
+
spec.add_development_dependency "bundler", "~> 2"
|
25
|
+
spec.add_development_dependency "pry", "~> 0.13.1"
|
26
26
|
spec.add_development_dependency "rake", "~> 12.0"
|
27
27
|
spec.add_development_dependency "rspec", "~> 3.5"
|
28
|
-
spec.add_development_dependency "yard"
|
29
|
-
spec.add_development_dependency "webmock", "~>
|
28
|
+
spec.add_development_dependency "yard", "~> 0.9.24"
|
29
|
+
spec.add_development_dependency "webmock", "~> 3.8.3"
|
30
30
|
end
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: vault
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.14.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Seth Vargo
|
8
8
|
autorequire:
|
9
9
|
bindir: exe
|
10
10
|
cert_chain: []
|
11
|
-
date:
|
11
|
+
date: 2020-05-28 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: aws-sigv4
|
@@ -28,30 +28,30 @@ dependencies:
|
|
28
28
|
name: bundler
|
29
29
|
requirement: !ruby/object:Gem::Requirement
|
30
30
|
requirements:
|
31
|
-
- - "
|
31
|
+
- - "~>"
|
32
32
|
- !ruby/object:Gem::Version
|
33
|
-
version: '
|
33
|
+
version: '2'
|
34
34
|
type: :development
|
35
35
|
prerelease: false
|
36
36
|
version_requirements: !ruby/object:Gem::Requirement
|
37
37
|
requirements:
|
38
|
-
- - "
|
38
|
+
- - "~>"
|
39
39
|
- !ruby/object:Gem::Version
|
40
|
-
version: '
|
40
|
+
version: '2'
|
41
41
|
- !ruby/object:Gem::Dependency
|
42
42
|
name: pry
|
43
43
|
requirement: !ruby/object:Gem::Requirement
|
44
44
|
requirements:
|
45
|
-
- - "
|
45
|
+
- - "~>"
|
46
46
|
- !ruby/object:Gem::Version
|
47
|
-
version:
|
47
|
+
version: 0.13.1
|
48
48
|
type: :development
|
49
49
|
prerelease: false
|
50
50
|
version_requirements: !ruby/object:Gem::Requirement
|
51
51
|
requirements:
|
52
|
-
- - "
|
52
|
+
- - "~>"
|
53
53
|
- !ruby/object:Gem::Version
|
54
|
-
version:
|
54
|
+
version: 0.13.1
|
55
55
|
- !ruby/object:Gem::Dependency
|
56
56
|
name: rake
|
57
57
|
requirement: !ruby/object:Gem::Requirement
|
@@ -84,30 +84,30 @@ dependencies:
|
|
84
84
|
name: yard
|
85
85
|
requirement: !ruby/object:Gem::Requirement
|
86
86
|
requirements:
|
87
|
-
- - "
|
87
|
+
- - "~>"
|
88
88
|
- !ruby/object:Gem::Version
|
89
|
-
version:
|
89
|
+
version: 0.9.24
|
90
90
|
type: :development
|
91
91
|
prerelease: false
|
92
92
|
version_requirements: !ruby/object:Gem::Requirement
|
93
93
|
requirements:
|
94
|
-
- - "
|
94
|
+
- - "~>"
|
95
95
|
- !ruby/object:Gem::Version
|
96
|
-
version:
|
96
|
+
version: 0.9.24
|
97
97
|
- !ruby/object:Gem::Dependency
|
98
98
|
name: webmock
|
99
99
|
requirement: !ruby/object:Gem::Requirement
|
100
100
|
requirements:
|
101
101
|
- - "~>"
|
102
102
|
- !ruby/object:Gem::Version
|
103
|
-
version:
|
103
|
+
version: 3.8.3
|
104
104
|
type: :development
|
105
105
|
prerelease: false
|
106
106
|
version_requirements: !ruby/object:Gem::Requirement
|
107
107
|
requirements:
|
108
108
|
- - "~>"
|
109
109
|
- !ruby/object:Gem::Version
|
110
|
-
version:
|
110
|
+
version: 3.8.3
|
111
111
|
description: Vault is a Ruby API client for interacting with a Vault server.
|
112
112
|
email:
|
113
113
|
- sethvargo@gmail.com
|
@@ -115,9 +115,9 @@ executables: []
|
|
115
115
|
extensions: []
|
116
116
|
extra_rdoc_files: []
|
117
117
|
files:
|
118
|
+
- ".circleci/config.yml"
|
118
119
|
- ".gitignore"
|
119
120
|
- ".rspec"
|
120
|
-
- ".travis.yml"
|
121
121
|
- CHANGELOG.md
|
122
122
|
- Gemfile
|
123
123
|
- LICENSE
|
@@ -130,6 +130,7 @@ files:
|
|
130
130
|
- lib/vault/api/auth_tls.rb
|
131
131
|
- lib/vault/api/auth_token.rb
|
132
132
|
- lib/vault/api/help.rb
|
133
|
+
- lib/vault/api/kv.rb
|
133
134
|
- lib/vault/api/logical.rb
|
134
135
|
- lib/vault/api/secret.rb
|
135
136
|
- lib/vault/api/sys.rb
|
@@ -140,8 +141,14 @@ files:
|
|
140
141
|
- lib/vault/api/sys/leader.rb
|
141
142
|
- lib/vault/api/sys/lease.rb
|
142
143
|
- lib/vault/api/sys/mount.rb
|
144
|
+
- lib/vault/api/sys/namespace.rb
|
143
145
|
- lib/vault/api/sys/policy.rb
|
144
146
|
- lib/vault/api/sys/seal.rb
|
147
|
+
- lib/vault/api/transform.rb
|
148
|
+
- lib/vault/api/transform/alphabet.rb
|
149
|
+
- lib/vault/api/transform/role.rb
|
150
|
+
- lib/vault/api/transform/template.rb
|
151
|
+
- lib/vault/api/transform/transformation.rb
|
145
152
|
- lib/vault/client.rb
|
146
153
|
- lib/vault/configurable.rb
|
147
154
|
- lib/vault/defaults.rb
|
@@ -177,8 +184,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
177
184
|
- !ruby/object:Gem::Version
|
178
185
|
version: '0'
|
179
186
|
requirements: []
|
180
|
-
|
181
|
-
rubygems_version: 2.6.14
|
187
|
+
rubygems_version: 3.1.2
|
182
188
|
signing_key:
|
183
189
|
specification_version: 4
|
184
190
|
summary: Vault is a Ruby API client for interacting with a Vault server.
|
data/.travis.yml
DELETED
@@ -1,26 +0,0 @@
|
|
1
|
-
dist: trusty
|
2
|
-
sudo: false
|
3
|
-
language: ruby
|
4
|
-
cache: bundler
|
5
|
-
|
6
|
-
env:
|
7
|
-
- VAULT_VERSION=0.8.3
|
8
|
-
- VAULT_VERSION=0.7.3
|
9
|
-
- VAULT_VERSION=0.6.5
|
10
|
-
- VAULT_VERSION=0.5.3
|
11
|
-
|
12
|
-
before_install:
|
13
|
-
- curl -sLo vault.zip https://releases.hashicorp.com/vault/${VAULT_VERSION}/vault_${VAULT_VERSION}_linux_amd64.zip
|
14
|
-
- unzip vault.zip
|
15
|
-
- mkdir -p ~/bin
|
16
|
-
- mv vault ~/bin
|
17
|
-
- export PATH="~/bin:$PATH"
|
18
|
-
|
19
|
-
branches:
|
20
|
-
only:
|
21
|
-
- master
|
22
|
-
|
23
|
-
rvm:
|
24
|
-
- 2.2
|
25
|
-
- 2.3
|
26
|
-
- 2.4
|