passwordstate 0.0.4 → 0.1.1

Sign up to get free protection for your applications and to get access to all the features.
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 = [])