passwordstate 0.0.3 → 0.1.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.
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 +11 -0
  7. data/README.md +64 -16
  8. data/Rakefile +2 -2
  9. data/lib/passwordstate/client.rb +55 -16
  10. data/lib/passwordstate/errors.rb +6 -1
  11. data/lib/passwordstate/resource.rb +131 -49
  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: 1fb957850976d1356b56000ef102e03638ff25ded2fc7d978ec9cd4399a51b4d
4
- data.tar.gz: 2dc78e6bd303d713bc5e61e2fdfc608f1d65e826edbe04e7bb17698fdc6e88af
3
+ metadata.gz: ead3a53cdb04468a85069d55f1e6764562ea518dfb1ccb2e0b640c883ecfc85f
4
+ data.tar.gz: 4d4dee12f8a862302b80c7fdb2027a0fb98bfe0a7ab45ece68622a6e42a9ed6a
5
5
  SHA512:
6
- metadata.gz: 73027a2b795d5611b76c5733bc5ec1cbcde1de34cc956e48f3bc52c22f473df5d9ce0dcc54540688ef7481669d61db185711b64ada6f1d04528ff459dbd6c9bb
7
- data.tar.gz: 6a9bfb4fd0d9f9579876334c219f0403fdb35a3dbf871bb0a884c9de07f86f7f95cdde9cd1434cdba86b0adbabe8c4613e8a30beab0d49db6ca404d741713f1f
6
+ metadata.gz: f306e2cf53329fc8f83f6364d02ec940a3df971f1f9b79e9e9866f40084c75e1fd96aeef42bca640f98e7507bb799006603f2007cf3cd01e5004eaeb93695ff9
7
+ data.tar.gz: 1f744f7a0f2bdd43b536b71de33bd218f1d496b79fcbfc4d0d284afdf9f80b35307c2acc21165e94d692268c22203ba1adb0a470ef76ba246c6292458cd971a2
@@ -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,14 @@
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
+
8
+ ## v0.0.4 2019-10-23
9
+
10
+ - Fixed a client request issue due to a rubocop change
11
+
1
12
  ## v0.0.3 2019-10-23
2
13
 
3
14
  - Added method to check if resource types are available
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,8 +1,10 @@
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
@@ -34,22 +36,31 @@ module Passwordstate
34
36
  @http.read_timeout = sec if @http
35
37
  end
36
38
 
39
+ def address_book
40
+ ResourceList.new Passwordstate::Resources::AddressBook,
41
+ client: self
42
+ end
43
+
37
44
  def folders
38
- ResourceList.new self, Passwordstate::Resources::Folder,
45
+ ResourceList.new Passwordstate::Resources::Folder,
46
+ client: self,
39
47
  only: %i[all search post]
40
48
  end
41
49
 
42
50
  def hosts
43
- ResourceList.new self, Passwordstate::Resources::Host,
51
+ ResourceList.new Passwordstate::Resources::Host,
52
+ client: self,
44
53
  except: %i[search put]
45
54
  end
46
55
 
47
56
  def passwords
48
- ResourceList.new self, Passwordstate::Resources::Password
57
+ ResourceList.new Passwordstate::Resources::Password,
58
+ client: self
49
59
  end
50
60
 
51
61
  def password_lists
52
- ResourceList.new self, Passwordstate::Resources::PasswordList,
62
+ ResourceList.new Passwordstate::Resources::PasswordList,
63
+ client: self,
53
64
  except: %i[put delete]
54
65
  end
55
66
 
@@ -65,22 +76,32 @@ module Passwordstate
65
76
  html = request(:get, '', allow_html: true)
66
77
  version = html.find_line { |line| line.include? '<span>V</span>' }
67
78
  version = />(\d\.\d) \(Build (.+)\)</.match(version)
68
- "#{version[1]}.#{version[2]}"
79
+ "#{version[1]}.#{version[2]}" if version
69
80
  end
70
81
  end
71
82
 
72
83
  def version?(compare)
84
+ if version.nil?
85
+ logger.debug 'Unable to detect Passwordstate version, assuming recent enough.'
86
+ return true
87
+ end
88
+
73
89
  Gem::Dependency.new(to_s, compare).match?(to_s, version)
74
90
  end
75
91
 
76
92
  def require_version(compare)
93
+ if version.nil?
94
+ logger.debug 'Unable to detect Passwordstate version, assuming recent enough.'
95
+ return true
96
+ end
97
+
77
98
  raise "Your version of Passwordstate (#{version}) doesn't support the requested feature" unless version? compare
78
99
  end
79
100
 
80
- def request(method, api_path, options = {})
101
+ def request(method, api_path, query: nil, reason: nil, **options)
81
102
  uri = URI(server_url + "/#{api_type}/" + api_path)
82
- uri.query = URI.encode_www_form(options.fetch(:query)) if options.key? :query
83
- uri.query = nil unless uri.query&.any?
103
+ uri.query = URI.encode_www_form(query) unless query.nil?
104
+ uri.query = nil if uri.query.nil? || uri.query.empty?
84
105
 
85
106
  req_obj = Net::HTTP.const_get(method.to_s.capitalize.to_sym).new uri
86
107
  if options.key? :body
@@ -92,7 +113,7 @@ module Passwordstate
92
113
  req_obj.ntlm_auth(auth_data[:username], auth_data[:password]) if api_type == :winapi
93
114
  headers.each { |h, v| req_obj[h] = v }
94
115
  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')
116
+ req_obj['Reason'] = reason if !reason.nil? && version?('>= 8.4.8449')
96
117
 
97
118
  print_http req_obj
98
119
  res_obj = http.request req_obj
@@ -104,9 +125,11 @@ module Passwordstate
104
125
  if data
105
126
  return data if res_obj.is_a? Net::HTTPSuccess
106
127
 
107
- data = data&.first
128
+ # data = data.first if data.is_a? Array
129
+ # parsed = data.fetch('errors', []) if data.is_a?(Hash) && data.key?('errors')
130
+ parsed = [data].flatten
108
131
 
109
- raise Passwordstate::HTTPError.new_by_code(res_obj.code, req_obj, res_obj, data&.fetch('errors', []) || [])
132
+ raise Passwordstate::HTTPError.new_by_code(res_obj.code, req_obj, res_obj, parsed || [])
110
133
  else
111
134
  return res_obj.body if res_obj.is_a?(Net::HTTPSuccess) && options.fetch(:allow_html, true)
112
135
 
@@ -114,10 +137,18 @@ module Passwordstate
114
137
  end
115
138
  end
116
139
 
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 ', '}>"
140
+ def pretty_print_instance_variables
141
+ instance_variables.reject { |k| %i[@auth_data @http @logger].include? k }.sort
119
142
  end
120
143
 
144
+ def pretty_print(pp)
145
+ return pp.pp self if respond_to? :mocha_inspect
146
+
147
+ pp.pp_object self
148
+ end
149
+
150
+ alias inspect pretty_print_inspect
151
+
121
152
  private
122
153
 
123
154
  def http
@@ -130,7 +161,7 @@ module Passwordstate
130
161
  @http.start
131
162
  end
132
163
 
133
- def print_http(http, truncate = true)
164
+ def print_http(http, truncate: true)
134
165
  return unless logger.debug?
135
166
 
136
167
  if http.is_a? Net::HTTPRequest
@@ -147,9 +178,17 @@ module Passwordstate
147
178
 
148
179
  return if http.body.nil?
149
180
 
181
+ body_cleaner = lambda do |obj|
182
+ obj.each { |k, v| v.replace('[ REDACTED ]') if k.is_a?(String) && %w[password apikey].include?(k.downcase) } if obj.is_a? Hash
183
+ end
184
+
150
185
  clean_body = JSON.parse(http.body) rescue nil
151
186
  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
187
+ if clean_body.is_a? Array
188
+ clean_body.each { |val| body_cleaner.call(val) }
189
+ else
190
+ body_cleaner.call(clean_body)
191
+ end
153
192
  else
154
193
  clean_body = http.body
155
194
  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 = [])