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.
Files changed (39) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/ruby.yml +33 -0
  3. data/.gitignore +2 -1
  4. data/.gitlab-ci.yml +10 -7
  5. data/.rubocop.yml +6 -9
  6. data/CHANGELOG.md +7 -0
  7. data/README.md +64 -16
  8. data/Rakefile +2 -2
  9. data/lib/passwordstate/client.rb +62 -16
  10. data/lib/passwordstate/errors.rb +6 -1
  11. data/lib/passwordstate/resource.rb +137 -53
  12. data/lib/passwordstate/resource_list.rb +56 -42
  13. data/lib/passwordstate/resources/active_directory.rb +23 -0
  14. data/lib/passwordstate/resources/address_book.rb +28 -0
  15. data/lib/passwordstate/resources/document.rb +32 -3
  16. data/lib/passwordstate/resources/folder.rb +6 -3
  17. data/lib/passwordstate/resources/host.rb +2 -0
  18. data/lib/passwordstate/resources/password.rb +50 -15
  19. data/lib/passwordstate/resources/password_list.rb +41 -8
  20. data/lib/passwordstate/resources/permission.rb +2 -0
  21. data/lib/passwordstate/resources/privileged_account.rb +32 -0
  22. data/lib/passwordstate/resources/remote_site.rb +26 -0
  23. data/lib/passwordstate/resources/report.rb +2 -0
  24. data/lib/passwordstate/util.rb +22 -2
  25. data/lib/passwordstate/version.rb +3 -1
  26. data/lib/passwordstate.rb +4 -1
  27. data/passwordstate.gemspec +9 -3
  28. data/test/client_test.rb +39 -0
  29. data/test/fixtures/get_password.json +31 -0
  30. data/test/fixtures/get_password_list.json +42 -0
  31. data/test/fixtures/get_password_otp.json +5 -0
  32. data/test/fixtures/password_list_search_passwords.json +60 -0
  33. data/test/fixtures/update_password.json +31 -0
  34. data/test/fixtures/update_password_managed.json +8 -0
  35. data/test/passwordstate_test.rb +10 -2
  36. data/test/resources/password_list_test.rb +81 -0
  37. data/test/resources/password_test.rb +105 -0
  38. data/test/test_helper.rb +8 -0
  39. metadata +56 -15
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 9df4373808dbc9ab6b38c17bfa109d60e0c32b6aa5c0cc399045960478e238d0
4
- data.tar.gz: 7cee656fb7e2311d8c6ca8dbb210061df3110b2822998f88e1d0a9c31c0f5ccb
3
+ metadata.gz: 2548f3e172e66faaa1d3a80f2e366161a61694a1874bffa97e68246d0bfbc091
4
+ data.tar.gz: 273d810d08aafd3849a42ff5397f4d6e123b1557f33f17590106758ee17773dc
5
5
  SHA512:
6
- metadata.gz: d9838dc2ca7fcaffdcef5443d7e0ec2ffc25d0cc282c9e22646c6467bffd4c1d19a544f50eeb915457afcd96ea07874773768d0829125f3e40fb4943f8faa458
7
- data.tar.gz: a5520cbc1051f6e534aa270aaf63bb92b24ba8241a920f3e8f4c4055ca2dd352528a85d1ed55a1bbbba9e7d101bc716b057f438b7e1fbdd3a5ad2f9031947af9
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
@@ -4,7 +4,8 @@
4
4
  /coverage/
5
5
  /doc/
6
6
  /pkg/
7
- /spec/reports/
7
+ /test/reports/
8
8
  /tmp/
9
9
  /vendor/
10
10
  Gemfile.lock
11
+ *.gem
data/.gitlab-ci.yml CHANGED
@@ -1,5 +1,5 @@
1
1
  ---
2
- image: "ruby:2.4"
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
- stage: deploy
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
- # rspec:
29
- # script:
30
- # - rspec spec
31
+ rake:
32
+ script:
33
+ - bundle exec rake
data/.rubocop.yml CHANGED
@@ -1,10 +1,11 @@
1
1
  ---
2
2
  AllCops:
3
- TargetRubyVersion: 2.4
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/IndentHeredoc:
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 ruby gem for communicating with a Passwordstate instance
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 http://iti.gitlab-pages.liu.se/ruby-passwordstate
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
- ```irb
26
- irb(main):001:0> require 'passwordstate'
27
- irb(main):002:0> client = Passwordstate::Client.new 'https://passwordstate', username: 'user', password: 'password'
28
- irb(main):003:0> # Passwordstate::Client.new 'https://passwordstate', apikey: 'key'
29
- irb(main):004:0> client.folders
30
- => [#<Passwordstate::Resources::Folder:0x000055ed493636e8 @folder_name="Example", @folder_id=2, @tree_path="\\Example">, #<Passwordstate::Resources::Folder:0x000055ed49361fa0 @folder_name="Folder", @folder_id=3, @tree_path="\\Example\\Folder">]
31
- irb(main):005:0> client.password_lists.get(7).passwords
32
- => [#<Passwordstate::Resources::Password:0x0000555fda8acdb8 @title="Webserver1", @user_name="test_web_account", @account_type_id=0, @password="[ REDACTED ]", @allow_export=false, @password_id=2>, #<Passwordstate::Resources::Password:0x0000555fda868640 @title="Webserver2", @user_name="test_web_account2", @account_type_id=0, @password="[ REDACTED ]", @allow_export=false, @password_id=3>, #<Passwordstate::Resources::Password:0x0000555fda84da48 @title="Webserver3", @user_name="test_web_account3", @account_type_id=0, @password="[ REDACTED ]", @allow_export=false, @password_id=4>]
33
- irb(main):006:0> pw = client.password_lists.first.passwords.create title: 'example', user_name: 'someone', generate_password: true
34
- => #<Passwordstate::Resources::Password:0x0000555fdaf9ce98 @title="example", @user_name="someone", @account_type_id=0, @password="[ REDACTED ]", @allow_export=true, @password_id=12, @generate_password=true, @password_list_id=6>
35
- irb(main):007:0> pw.password
36
- => "millionfE2rMrcb2LngBTHnDyxdpsGSmK3"
37
- irb(main):008:0> pw.delete
38
- => true
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
@@ -1,5 +1,5 @@
1
- require "bundler/gem_tasks"
2
- require "rake/testtask"
1
+ require 'bundler/gem_tasks'
2
+ require 'rake/testtask'
3
3
 
4
4
  Rake::TestTask.new(:test) do |t|
5
5
  t.libs << "test"
@@ -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}".freeze
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 self, Passwordstate::Resources::Folder,
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 self, Passwordstate::Resources::Host,
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 self, Passwordstate::Resources::Password
63
+ ResourceList.new Passwordstate::Resources::Password,
64
+ client: self
49
65
  end
50
66
 
51
67
  def password_lists
52
- ResourceList.new self, Passwordstate::Resources::PasswordList,
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, options = {})
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(options.fetch(:query)) if options.key? :query
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'] = options.fetch(:reason) if options.key?(:reason) && version?('>= 8.4.8449')
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&.first
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, data&.fetch('errors', []) || [])
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 inspect
118
- "#{to_s[0..-2]} #{instance_variables.reject { |k| %i[@auth_data @http @logger].include? k }.map { |k| "#{k}=#{instance_variable_get(k).inspect}" }.join ', '}>"
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 = true)
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
- clean_body = clean_body.each { |k, v| v.replace('[ REDACTED ]') if k.is_a?(String) && %w[password apikey].include?(k.downcase) }.to_json if http.body
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
@@ -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
- super "Passwordstate responded with an error to the request:\n#{errors.map { |err| err['message'] || err['phrase'] }.join('; ')}"
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 = [])