passwordstate 0.0.4 → 0.1.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 +4 -4
- data/.github/workflows/ruby.yml +33 -0
- data/.gitignore +2 -1
- data/.gitlab-ci.yml +10 -7
- data/.rubocop.yml +6 -9
- data/CHANGELOG.md +7 -0
- data/README.md +64 -16
- data/Rakefile +2 -2
- data/lib/passwordstate/client.rb +62 -16
- data/lib/passwordstate/errors.rb +6 -1
- data/lib/passwordstate/resource.rb +137 -53
- data/lib/passwordstate/resource_list.rb +56 -42
- data/lib/passwordstate/resources/active_directory.rb +23 -0
- data/lib/passwordstate/resources/address_book.rb +28 -0
- data/lib/passwordstate/resources/document.rb +32 -3
- data/lib/passwordstate/resources/folder.rb +6 -3
- data/lib/passwordstate/resources/host.rb +2 -0
- data/lib/passwordstate/resources/password.rb +50 -15
- data/lib/passwordstate/resources/password_list.rb +41 -8
- data/lib/passwordstate/resources/permission.rb +2 -0
- data/lib/passwordstate/resources/privileged_account.rb +32 -0
- data/lib/passwordstate/resources/remote_site.rb +26 -0
- data/lib/passwordstate/resources/report.rb +2 -0
- data/lib/passwordstate/util.rb +22 -2
- data/lib/passwordstate/version.rb +3 -1
- data/lib/passwordstate.rb +4 -1
- data/passwordstate.gemspec +9 -3
- data/test/client_test.rb +39 -0
- data/test/fixtures/get_password.json +31 -0
- data/test/fixtures/get_password_list.json +42 -0
- data/test/fixtures/get_password_otp.json +5 -0
- data/test/fixtures/password_list_search_passwords.json +60 -0
- data/test/fixtures/update_password.json +31 -0
- data/test/fixtures/update_password_managed.json +8 -0
- data/test/passwordstate_test.rb +10 -2
- data/test/resources/password_list_test.rb +81 -0
- data/test/resources/password_test.rb +105 -0
- data/test/test_helper.rb +8 -0
- metadata +56 -15
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 2548f3e172e66faaa1d3a80f2e366161a61694a1874bffa97e68246d0bfbc091
|
4
|
+
data.tar.gz: 273d810d08aafd3849a42ff5397f4d6e123b1557f33f17590106758ee17773dc
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: c515ecce21af9bdd9e6c72b85041bd979cd23c3d3bb0927c17f665cd7077834a239daaa5f3001a2251d6fa6914e884f39941ab91c9cc33926f9fa194529f84dd
|
7
|
+
data.tar.gz: e087dc037ce713911d626876677a9aacc818261a717a3e867524fb3aece1d4a28f2dde71329c3896fcda2afbde808021b729f3ec7121800e29d5cb5df3fa2500
|
@@ -0,0 +1,33 @@
|
|
1
|
+
---
|
2
|
+
name: Ruby
|
3
|
+
|
4
|
+
on:
|
5
|
+
push:
|
6
|
+
tags: ["*"]
|
7
|
+
branches: ["master"]
|
8
|
+
pull_request:
|
9
|
+
branches: ["master"]
|
10
|
+
|
11
|
+
permissions:
|
12
|
+
contents: read
|
13
|
+
|
14
|
+
jobs:
|
15
|
+
tests:
|
16
|
+
runs-on: ubuntu-latest
|
17
|
+
strategy:
|
18
|
+
matrix:
|
19
|
+
ruby-version: ['2.7', '3.0', '3.1']
|
20
|
+
|
21
|
+
steps:
|
22
|
+
- uses: actions/checkout@v3
|
23
|
+
- name: Set up Ruby
|
24
|
+
uses: ruby/setup-ruby@v1
|
25
|
+
with:
|
26
|
+
ruby-version: ${{ matrix.ruby-version }}
|
27
|
+
bundler-cache: true
|
28
|
+
- name: Install rubocop
|
29
|
+
run: gem install -N rubocop
|
30
|
+
- name: Rubocop
|
31
|
+
run: rubocop lib/
|
32
|
+
- name: Run tests
|
33
|
+
run: bundle exec rake
|
data/.gitignore
CHANGED
data/.gitlab-ci.yml
CHANGED
@@ -1,5 +1,5 @@
|
|
1
1
|
---
|
2
|
-
image: "ruby:2.
|
2
|
+
image: "ruby:2.7"
|
3
3
|
|
4
4
|
# Cache gems in between builds
|
5
5
|
cache:
|
@@ -12,12 +12,15 @@ before_script:
|
|
12
12
|
|
13
13
|
rubocop:
|
14
14
|
script:
|
15
|
-
- bundle exec rubocop
|
15
|
+
- bundle exec rubocop lib/ -f p -f ju -o junit.xml
|
16
|
+
artifacts:
|
17
|
+
reports:
|
18
|
+
junit: junit.xml
|
16
19
|
|
17
20
|
pages:
|
18
|
-
|
21
|
+
before_script: []
|
19
22
|
script:
|
20
|
-
- gem install yard
|
23
|
+
- gem install yard redcarpet
|
21
24
|
- yard doc -o public/
|
22
25
|
artifacts:
|
23
26
|
paths:
|
@@ -25,6 +28,6 @@ pages:
|
|
25
28
|
only:
|
26
29
|
- master
|
27
30
|
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
+
rake:
|
32
|
+
script:
|
33
|
+
- bundle exec rake
|
data/.rubocop.yml
CHANGED
@@ -1,10 +1,11 @@
|
|
1
1
|
---
|
2
2
|
AllCops:
|
3
|
-
TargetRubyVersion: 2.
|
3
|
+
TargetRubyVersion: 2.7
|
4
4
|
Exclude:
|
5
5
|
- '*.spec'
|
6
6
|
- 'Rakefile'
|
7
7
|
- 'vendor/**/*'
|
8
|
+
NewCops: enable
|
8
9
|
|
9
10
|
# Don't enforce documentation
|
10
11
|
Style/Documentation:
|
@@ -22,7 +23,7 @@ Style/SafeNavigation:
|
|
22
23
|
Layout/ClosingHeredocIndentation:
|
23
24
|
Enabled: false
|
24
25
|
|
25
|
-
Layout/
|
26
|
+
Layout/HeredocIndentation:
|
26
27
|
Enabled: false
|
27
28
|
|
28
29
|
Metrics/PerceivedComplexity:
|
@@ -34,19 +35,15 @@ Metrics/CyclomaticComplexity:
|
|
34
35
|
Style/RescueModifier:
|
35
36
|
Enabled: false
|
36
37
|
|
38
|
+
Layout/LineLength:
|
39
|
+
Max: 140
|
40
|
+
|
37
41
|
Metrics/MethodLength:
|
38
42
|
Max: 40
|
39
43
|
|
40
|
-
Metrics/LineLength:
|
41
|
-
Max: 190
|
42
|
-
|
43
44
|
Metrics/AbcSize:
|
44
45
|
Enabled: false
|
45
46
|
|
46
|
-
Performance/FixedSize:
|
47
|
-
Exclude:
|
48
|
-
- 'test/**/*'
|
49
|
-
|
50
47
|
Metrics/BlockLength:
|
51
48
|
Exclude:
|
52
49
|
- 'test/**/*'
|
data/CHANGELOG.md
CHANGED
@@ -1,3 +1,10 @@
|
|
1
|
+
## v0.1.0 2022-12-05
|
2
|
+
|
3
|
+
- Reduced data transferred for regular queries
|
4
|
+
- Fixed support for more modern Ruby (3+)
|
5
|
+
- Fixed error handling and changes for newer Passwordstate versions
|
6
|
+
- Improved pretty printing when debugging
|
7
|
+
|
1
8
|
## v0.0.4 2019-10-23
|
2
9
|
|
3
10
|
- Fixed a client request issue due to a rubocop change
|
data/README.md
CHANGED
@@ -1,8 +1,8 @@
|
|
1
1
|
# Passwordstate
|
2
2
|
|
3
|
-
A
|
3
|
+
A Ruby gem for communicating with a [Passwordstate](https://clickstudios.com.au/passwordstate.aspx) instance
|
4
4
|
|
5
|
-
The documentation for the development version can be found at
|
5
|
+
The documentation for the development version can be found at https://iti.gitlab-pages.liu.se/ruby-passwordstate
|
6
6
|
|
7
7
|
## Installation
|
8
8
|
|
@@ -22,22 +22,70 @@ Or install it yourself as:
|
|
22
22
|
|
23
23
|
## Usage example
|
24
24
|
|
25
|
-
```
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
25
|
+
```ruby
|
26
|
+
require 'passwordstate'
|
27
|
+
client = Passwordstate::Client.new 'https://passwordstate.example.com', username: 'user', password: 'password'
|
28
|
+
# Passwordstate::Client.new 'https://passwordstate.example.com', apikey: 'key'
|
29
|
+
# #<Passwordstate::Client:0x0000559eb1fabec8
|
30
|
+
# @headers=
|
31
|
+
# {"accept"=>"application/json", "user-agent"=>"RubyPasswordstate/0.1.0"},
|
32
|
+
# @server_url=#<URI::HTTPS https://passwordstate.example.com>,
|
33
|
+
# @timeout=15,
|
34
|
+
# @validate_certificate=true>
|
35
|
+
|
36
|
+
client.folders
|
37
|
+
# [#<Passwordstate::Resources::Folder:0x000055ed493636e8
|
38
|
+
# @folder_name="Example",
|
39
|
+
# @folder_id=2,
|
40
|
+
# @tree_path="\\Example">,
|
41
|
+
# #<Passwordstate::Resources::Folder:0x000055ed49361fa0
|
42
|
+
# @folder_name="Folder",
|
43
|
+
# @folder_id=3,
|
44
|
+
# @tree_path="\\Example\\Folder">]
|
45
|
+
|
46
|
+
client.password_lists.get(7).passwords
|
47
|
+
# [#<Passwordstate::Resources::Password:0x0000555fda8acdb8
|
48
|
+
# @title="Webserver1",
|
49
|
+
# @user_name="test_web_account",
|
50
|
+
# @account_type_id=0,
|
51
|
+
# @password="[ REDACTED ]",
|
52
|
+
# @allow_export=false,
|
53
|
+
# @password_id=2>,
|
54
|
+
# #<Passwordstate::Resources::Password:0x0000555fda868640
|
55
|
+
# @title="Webserver2",
|
56
|
+
# @user_name="test_web_account2",
|
57
|
+
# @account_type_id=0,
|
58
|
+
# @password="[ REDACTED ]",
|
59
|
+
# @allow_export=false,
|
60
|
+
# @password_id=3>,
|
61
|
+
# #<Passwordstate::Resources::Password:0x0000555fda84da48
|
62
|
+
# @title="Webserver3",
|
63
|
+
# @user_name="test_web_account3",
|
64
|
+
# @account_type_id=0,
|
65
|
+
# @password="[ REDACTED ]",
|
66
|
+
# @allow_export=false,
|
67
|
+
# @password_id=4>]
|
68
|
+
|
69
|
+
pw = client.password_lists.first.passwords.create title: 'example', user_name: 'someone', generate_password: true
|
70
|
+
# #<Passwordstate::Resources::Password:0x0000555fdaf9ce98
|
71
|
+
# @title="example",
|
72
|
+
# @user_name="someone",
|
73
|
+
# @account_type_id=0,
|
74
|
+
# @password="[ REDACTED ]",
|
75
|
+
# @allow_export=true,
|
76
|
+
# @password_id=12,
|
77
|
+
# @generate_password=true,
|
78
|
+
# @password_list_id=6>
|
79
|
+
|
80
|
+
pw.password
|
81
|
+
# "millionfE2rMrcb2LngBTHnDyxdpsGSmK3"
|
82
|
+
|
83
|
+
pw.delete
|
84
|
+
# true
|
39
85
|
```
|
40
86
|
|
87
|
+
A larger - and much more convoluted - example can be found at https://github.com/ananace/foreman_passwordstate/
|
88
|
+
|
41
89
|
## Contributing
|
42
90
|
|
43
91
|
Bug reports and pull requests are welcome on GitHub at https://github.com/ananace/ruby-passwordstate
|
data/Rakefile
CHANGED
data/lib/passwordstate/client.rb
CHANGED
@@ -1,15 +1,17 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
require 'json'
|
2
4
|
|
3
5
|
module Passwordstate
|
4
6
|
class Client
|
5
|
-
USER_AGENT = "RubyPasswordstate/#{Passwordstate::VERSION}"
|
7
|
+
USER_AGENT = "RubyPasswordstate/#{Passwordstate::VERSION}"
|
6
8
|
DEFAULT_HEADERS = {
|
7
9
|
'accept' => 'application/json',
|
8
10
|
'user-agent' => USER_AGENT
|
9
11
|
}.freeze
|
10
12
|
|
11
13
|
attr_accessor :server_url, :auth_data, :headers, :validate_certificate
|
12
|
-
attr_reader :timeout
|
14
|
+
attr_reader :open_timeout, :timeout
|
13
15
|
attr_writer :api_type
|
14
16
|
|
15
17
|
def initialize(url, options = {})
|
@@ -18,6 +20,7 @@ module Passwordstate
|
|
18
20
|
@headers = DEFAULT_HEADERS
|
19
21
|
@auth_data = options.select { |k, _v| %i[apikey username password].include? k }
|
20
22
|
@api_type = options.fetch(:api_type) if options.key? :api_type
|
23
|
+
@open_timeout = options.fetch(:open_timeout, 5)
|
21
24
|
@timeout = options.fetch(:timeout, 15)
|
22
25
|
end
|
23
26
|
|
@@ -29,27 +32,41 @@ module Passwordstate
|
|
29
32
|
@api_type || (auth_data.key?(:apikey) ? :api : :winapi)
|
30
33
|
end
|
31
34
|
|
35
|
+
def open_timeout=(sec)
|
36
|
+
@open_timeout = sec
|
37
|
+
@http.open_timeout = sec if @http
|
38
|
+
end
|
39
|
+
|
32
40
|
def timeout=(sec)
|
33
41
|
@timeout = sec
|
34
42
|
@http.read_timeout = sec if @http
|
35
43
|
end
|
36
44
|
|
45
|
+
def address_book
|
46
|
+
ResourceList.new Passwordstate::Resources::AddressBook,
|
47
|
+
client: self
|
48
|
+
end
|
49
|
+
|
37
50
|
def folders
|
38
|
-
ResourceList.new
|
51
|
+
ResourceList.new Passwordstate::Resources::Folder,
|
52
|
+
client: self,
|
39
53
|
only: %i[all search post]
|
40
54
|
end
|
41
55
|
|
42
56
|
def hosts
|
43
|
-
ResourceList.new
|
57
|
+
ResourceList.new Passwordstate::Resources::Host,
|
58
|
+
client: self,
|
44
59
|
except: %i[search put]
|
45
60
|
end
|
46
61
|
|
47
62
|
def passwords
|
48
|
-
ResourceList.new
|
63
|
+
ResourceList.new Passwordstate::Resources::Password,
|
64
|
+
client: self
|
49
65
|
end
|
50
66
|
|
51
67
|
def password_lists
|
52
|
-
ResourceList.new
|
68
|
+
ResourceList.new Passwordstate::Resources::PasswordList,
|
69
|
+
client: self,
|
53
70
|
except: %i[put delete]
|
54
71
|
end
|
55
72
|
|
@@ -65,21 +82,31 @@ module Passwordstate
|
|
65
82
|
html = request(:get, '', allow_html: true)
|
66
83
|
version = html.find_line { |line| line.include? '<span>V</span>' }
|
67
84
|
version = />(\d\.\d) \(Build (.+)\)</.match(version)
|
68
|
-
"#{version[1]}.#{version[2]}"
|
85
|
+
"#{version[1]}.#{version[2]}" if version
|
69
86
|
end
|
70
87
|
end
|
71
88
|
|
72
89
|
def version?(compare)
|
90
|
+
if version.nil?
|
91
|
+
logger.debug 'Unable to detect Passwordstate version, assuming recent enough.'
|
92
|
+
return true
|
93
|
+
end
|
94
|
+
|
73
95
|
Gem::Dependency.new(to_s, compare).match?(to_s, version)
|
74
96
|
end
|
75
97
|
|
76
98
|
def require_version(compare)
|
99
|
+
if version.nil?
|
100
|
+
logger.debug 'Unable to detect Passwordstate version, assuming recent enough.'
|
101
|
+
return true
|
102
|
+
end
|
103
|
+
|
77
104
|
raise "Your version of Passwordstate (#{version}) doesn't support the requested feature" unless version? compare
|
78
105
|
end
|
79
106
|
|
80
|
-
def request(method, api_path,
|
107
|
+
def request(method, api_path, query: nil, reason: nil, **options)
|
81
108
|
uri = URI(server_url + "/#{api_type}/" + api_path)
|
82
|
-
uri.query = URI.encode_www_form(
|
109
|
+
uri.query = URI.encode_www_form(query) unless query.nil?
|
83
110
|
uri.query = nil if uri.query.nil? || uri.query.empty?
|
84
111
|
|
85
112
|
req_obj = Net::HTTP.const_get(method.to_s.capitalize.to_sym).new uri
|
@@ -92,7 +119,7 @@ module Passwordstate
|
|
92
119
|
req_obj.ntlm_auth(auth_data[:username], auth_data[:password]) if api_type == :winapi
|
93
120
|
headers.each { |h, v| req_obj[h] = v }
|
94
121
|
req_obj['APIKey'] = auth_data[:apikey] if api_type == :api
|
95
|
-
req_obj['Reason'] =
|
122
|
+
req_obj['Reason'] = reason if !reason.nil? && version?('>= 8.4.8449')
|
96
123
|
|
97
124
|
print_http req_obj
|
98
125
|
res_obj = http.request req_obj
|
@@ -104,9 +131,11 @@ module Passwordstate
|
|
104
131
|
if data
|
105
132
|
return data if res_obj.is_a? Net::HTTPSuccess
|
106
133
|
|
107
|
-
data = data
|
134
|
+
# data = data.first if data.is_a? Array
|
135
|
+
# parsed = data.fetch('errors', []) if data.is_a?(Hash) && data.key?('errors')
|
136
|
+
parsed = [data].flatten
|
108
137
|
|
109
|
-
raise Passwordstate::HTTPError.new_by_code(res_obj.code, req_obj, res_obj,
|
138
|
+
raise Passwordstate::HTTPError.new_by_code(res_obj.code, req_obj, res_obj, parsed || [])
|
110
139
|
else
|
111
140
|
return res_obj.body if res_obj.is_a?(Net::HTTPSuccess) && options.fetch(:allow_html, true)
|
112
141
|
|
@@ -114,23 +143,32 @@ module Passwordstate
|
|
114
143
|
end
|
115
144
|
end
|
116
145
|
|
117
|
-
def
|
118
|
-
|
146
|
+
def pretty_print_instance_variables
|
147
|
+
instance_variables.reject { |k| %i[@auth_data @http @logger].include? k }.sort
|
148
|
+
end
|
149
|
+
|
150
|
+
def pretty_print(pp)
|
151
|
+
return pp.pp self if respond_to? :mocha_inspect
|
152
|
+
|
153
|
+
pp.pp_object self
|
119
154
|
end
|
120
155
|
|
156
|
+
alias inspect pretty_print_inspect
|
157
|
+
|
121
158
|
private
|
122
159
|
|
123
160
|
def http
|
124
161
|
@http ||= Net::HTTP.new server_url.host, server_url.port
|
125
162
|
return @http if @http.active?
|
126
163
|
|
164
|
+
@http.open_timeout = @open_timeout if @open_timeout
|
127
165
|
@http.read_timeout = @timeout if @timeout
|
128
166
|
@http.use_ssl = server_url.scheme == 'https'
|
129
167
|
@http.verify_mode = validate_certificate ? ::OpenSSL::SSL::VERIFY_NONE : nil
|
130
168
|
@http.start
|
131
169
|
end
|
132
170
|
|
133
|
-
def print_http(http, truncate
|
171
|
+
def print_http(http, truncate: true)
|
134
172
|
return unless logger.debug?
|
135
173
|
|
136
174
|
if http.is_a? Net::HTTPRequest
|
@@ -147,9 +185,17 @@ module Passwordstate
|
|
147
185
|
|
148
186
|
return if http.body.nil?
|
149
187
|
|
188
|
+
body_cleaner = lambda do |obj|
|
189
|
+
obj.each { |k, v| v.replace('[ REDACTED ]') if k.is_a?(String) && %w[password apikey].include?(k.downcase) } if obj.is_a? Hash
|
190
|
+
end
|
191
|
+
|
150
192
|
clean_body = JSON.parse(http.body) rescue nil
|
151
193
|
if clean_body
|
152
|
-
|
194
|
+
if clean_body.is_a? Array
|
195
|
+
clean_body.each { |val| body_cleaner.call(val) }
|
196
|
+
else
|
197
|
+
body_cleaner.call(clean_body)
|
198
|
+
end
|
153
199
|
else
|
154
200
|
clean_body = http.body
|
155
201
|
end
|
data/lib/passwordstate/errors.rb
CHANGED
@@ -1,6 +1,10 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
module Passwordstate
|
2
4
|
class PasswordstateError < RuntimeError; end
|
3
5
|
|
6
|
+
class NotAcceptableError < PasswordstateError; end
|
7
|
+
|
4
8
|
class HTTPError < PasswordstateError
|
5
9
|
attr_reader :code, :request, :response, :errors
|
6
10
|
|
@@ -10,7 +14,8 @@ module Passwordstate
|
|
10
14
|
@response = response
|
11
15
|
@errors = errors
|
12
16
|
|
13
|
-
|
17
|
+
errorstr = errors.map { |err| err['message'] || err['phrase'] || err['error'] }.join('; ')
|
18
|
+
super "Passwordstate responded with an error to the request:\n#{errorstr}"
|
14
19
|
end
|
15
20
|
|
16
21
|
def self.new_by_code(code, req, res, errors = [])
|