passwordstate 0.0.4 → 0.1.0

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 +54 -15
  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: 9df4373808dbc9ab6b38c17bfa109d60e0c32b6aa5c0cc399045960478e238d0
4
- data.tar.gz: 7cee656fb7e2311d8c6ca8dbb210061df3110b2822998f88e1d0a9c31c0f5ccb
3
+ metadata.gz: ead3a53cdb04468a85069d55f1e6764562ea518dfb1ccb2e0b640c883ecfc85f
4
+ data.tar.gz: 4d4dee12f8a862302b80c7fdb2027a0fb98bfe0a7ab45ece68622a6e42a9ed6a
5
5
  SHA512:
6
- metadata.gz: d9838dc2ca7fcaffdcef5443d7e0ec2ffc25d0cc282c9e22646c6467bffd4c1d19a544f50eeb915457afcd96ea07874773768d0829125f3e40fb4943f8faa458
7
- data.tar.gz: a5520cbc1051f6e534aa270aaf63bb92b24ba8241a920f3e8f4c4055ca2dd352528a85d1ed55a1bbbba9e7d101bc716b057f438b7e1fbdd3a5ad2f9031947af9
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,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,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,21 +76,31 @@ 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
103
+ uri.query = URI.encode_www_form(query) unless query.nil?
83
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
@@ -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 = [])