passwordstate 0.0.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 +7 -0
- data/.gitignore +10 -0
- data/.gitlab-ci.yml +19 -0
- data/.rubocop.yml +65 -0
- data/Gemfile +6 -0
- data/LICENSE.txt +21 -0
- data/README.md +46 -0
- data/Rakefile +10 -0
- data/lib/passwordstate.rb +20 -0
- data/lib/passwordstate/client.rb +138 -0
- data/lib/passwordstate/errors.rb +17 -0
- data/lib/passwordstate/resource.rb +222 -0
- data/lib/passwordstate/resource_list.rb +126 -0
- data/lib/passwordstate/resources/document.rb +20 -0
- data/lib/passwordstate/resources/folder.rb +25 -0
- data/lib/passwordstate/resources/host.rb +32 -0
- data/lib/passwordstate/resources/password.rb +134 -0
- data/lib/passwordstate/resources/password_list.rb +54 -0
- data/lib/passwordstate/resources/report.rb +54 -0
- data/lib/passwordstate/util.rb +77 -0
- data/lib/passwordstate/version.rb +3 -0
- data/passwordstate.gemspec +25 -0
- data/test/passwordstate_test.rb +11 -0
- data/test/test_helper.rb +4 -0
- metadata +153 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: ab1c9b2c6cb3727612e991d77631ddf61d266c7a0b5ba26a5c835b5d349c51e6
|
4
|
+
data.tar.gz: 18f155663d7592f40c0dd54b09b13d075c0e2d1265a1e993ad1c2cc7811e1e3d
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: d3ad29d420d8bfb75bd7418747933716914b311fd87c8d50ee83451aed8de2a06d363ec2709823b3a3be11bfe68ea510359ab93638a483d45dcdd80fb1744b37
|
7
|
+
data.tar.gz: 6ddc4c5bcd72c5a03da7f046eeca3fec8113ba23c88d71a5d45901923ff01b0294eddf9dfa02cd937bc70698869bbdd68beb69673732a177879087ea159a251d
|
data/.gitignore
ADDED
data/.gitlab-ci.yml
ADDED
@@ -0,0 +1,19 @@
|
|
1
|
+
---
|
2
|
+
image: "ruby:2.4"
|
3
|
+
|
4
|
+
# Cache gems in between builds
|
5
|
+
cache:
|
6
|
+
paths:
|
7
|
+
- vendor/ruby
|
8
|
+
|
9
|
+
before_script:
|
10
|
+
- gem install bundler --no-ri --no-rdoc
|
11
|
+
- bundle install -j $(nproc) --path vendor
|
12
|
+
|
13
|
+
rubocop:
|
14
|
+
script:
|
15
|
+
- bundle exec rubocop
|
16
|
+
|
17
|
+
# rspec:
|
18
|
+
# script:
|
19
|
+
# - rspec spec
|
data/.rubocop.yml
ADDED
@@ -0,0 +1,65 @@
|
|
1
|
+
---
|
2
|
+
AllCops:
|
3
|
+
TargetRubyVersion: 2.4
|
4
|
+
Exclude:
|
5
|
+
- '*.spec'
|
6
|
+
- 'Rakefile'
|
7
|
+
- 'vendor/**/*'
|
8
|
+
|
9
|
+
# Don't enforce documentation
|
10
|
+
Style/Documentation:
|
11
|
+
Enabled: false
|
12
|
+
|
13
|
+
Style/FrozenStringLiteralComment:
|
14
|
+
Enabled: false
|
15
|
+
|
16
|
+
Style/MultilineBlockChain:
|
17
|
+
Enabled: false
|
18
|
+
|
19
|
+
Style/SafeNavigation:
|
20
|
+
Enabled: false
|
21
|
+
|
22
|
+
Layout/ClosingHeredocIndentation:
|
23
|
+
Enabled: false
|
24
|
+
|
25
|
+
Layout/IndentHeredoc:
|
26
|
+
Enabled: false
|
27
|
+
|
28
|
+
Metrics/PerceivedComplexity:
|
29
|
+
Enabled: false
|
30
|
+
|
31
|
+
Metrics/CyclomaticComplexity:
|
32
|
+
Enabled: false
|
33
|
+
|
34
|
+
Style/RescueModifier:
|
35
|
+
Enabled: false
|
36
|
+
|
37
|
+
Metrics/MethodLength:
|
38
|
+
Max: 40
|
39
|
+
|
40
|
+
Metrics/LineLength:
|
41
|
+
Max: 190
|
42
|
+
|
43
|
+
Metrics/AbcSize:
|
44
|
+
Enabled: false
|
45
|
+
|
46
|
+
Performance/FixedSize:
|
47
|
+
Exclude:
|
48
|
+
- 'test/**/*'
|
49
|
+
|
50
|
+
Metrics/BlockLength:
|
51
|
+
Exclude:
|
52
|
+
- 'test/**/*'
|
53
|
+
|
54
|
+
Metrics/ClassLength:
|
55
|
+
Max: 200
|
56
|
+
Exclude:
|
57
|
+
- 'test/**/*'
|
58
|
+
|
59
|
+
Lint/AmbiguousBlockAssociation:
|
60
|
+
Enabled: false
|
61
|
+
|
62
|
+
Style/ClassAndModuleChildren:
|
63
|
+
Exclude:
|
64
|
+
- 'test/**/*'
|
65
|
+
- 'app/controllers/concerns/foreman/**/*'
|
data/Gemfile
ADDED
data/LICENSE.txt
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
The MIT License (MIT)
|
2
|
+
|
3
|
+
Copyright (c) 2018 Alexander Olofsson
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
7
|
+
in the Software without restriction, including without limitation the rights
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
10
|
+
furnished to do so, subject to the following conditions:
|
11
|
+
|
12
|
+
The above copyright notice and this permission notice shall be included in
|
13
|
+
all copies or substantial portions of the Software.
|
14
|
+
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
21
|
+
THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,46 @@
|
|
1
|
+
# Passwordstate
|
2
|
+
|
3
|
+
A ruby gem for communicating with a Passwordstate instance
|
4
|
+
|
5
|
+
## Installation
|
6
|
+
|
7
|
+
Add this line to your application's Gemfile:
|
8
|
+
|
9
|
+
```ruby
|
10
|
+
gem 'passwordstate'
|
11
|
+
```
|
12
|
+
|
13
|
+
And then execute:
|
14
|
+
|
15
|
+
$ bundle
|
16
|
+
|
17
|
+
Or install it yourself as:
|
18
|
+
|
19
|
+
$ gem install passwordstate
|
20
|
+
|
21
|
+
## Usage example
|
22
|
+
|
23
|
+
```irb
|
24
|
+
irb(main):001:0> require 'passwordstate'
|
25
|
+
irb(main):002:0> client = Passwordstate::Client.new 'https://passwordstate', username: 'user', password: 'password'
|
26
|
+
irb(main):003:0> # Passwordstate::Client.new 'https://passwordstate', apikey: 'key'
|
27
|
+
irb(main):004:0> client.folders
|
28
|
+
=> [#<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">]
|
29
|
+
irb(main):005:0> client.password_lists.get(7).passwords
|
30
|
+
=> [#<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>]
|
31
|
+
irb(main):006:0> pw = client.password_lists.first.passwords.create title: 'example', user_name: 'someone', generate_password: true
|
32
|
+
=> #<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>
|
33
|
+
irb(main):007:0> pw.password
|
34
|
+
=> "millionfE2rMrcb2LngBTHnDyxdpsGSmK3"
|
35
|
+
irb(main):008:0> pw.delete
|
36
|
+
=> true
|
37
|
+
```
|
38
|
+
|
39
|
+
## Contributing
|
40
|
+
|
41
|
+
Bug reports and pull requests are welcome on GitHub at https://github.com/ananace/ruby-passwordstate
|
42
|
+
The project lives at https://gitlab.liu.se/ITI/ruby-passwordstate
|
43
|
+
|
44
|
+
## License
|
45
|
+
|
46
|
+
The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
|
data/Rakefile
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
require 'logging'
|
2
|
+
require 'passwordstate/client'
|
3
|
+
require 'passwordstate/errors'
|
4
|
+
require 'passwordstate/resource'
|
5
|
+
require 'passwordstate/resource_list'
|
6
|
+
require 'passwordstate/util'
|
7
|
+
require 'passwordstate/version'
|
8
|
+
|
9
|
+
module Passwordstate
|
10
|
+
def self.debug!
|
11
|
+
logger.level = :debug
|
12
|
+
end
|
13
|
+
|
14
|
+
def self.logger
|
15
|
+
@logger ||= Logging.logger[self].tap do |logger|
|
16
|
+
logger.add_appenders Logging.appenders.stdout
|
17
|
+
logger.level = :warn
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
@@ -0,0 +1,138 @@
|
|
1
|
+
require 'json'
|
2
|
+
|
3
|
+
module Passwordstate
|
4
|
+
class Client
|
5
|
+
USER_AGENT = "RubyPasswordstate/#{Passwordstate::VERSION}".freeze
|
6
|
+
DEFAULT_HEADERS = {
|
7
|
+
'accept' => 'application/json',
|
8
|
+
'user-agent' => USER_AGENT
|
9
|
+
}.freeze
|
10
|
+
|
11
|
+
attr_accessor :server_url, :auth_data, :headers, :validate_certificate
|
12
|
+
attr_writer :api_type
|
13
|
+
|
14
|
+
def initialize(url, options = {})
|
15
|
+
@server_url = URI(url)
|
16
|
+
@validate_certificate = true
|
17
|
+
@headers = DEFAULT_HEADERS
|
18
|
+
@auth_data = options.select { |k, _v| %i[apikey username password].include? k }
|
19
|
+
@api_type = options.fetch(:api_type) if options.key? :api_type
|
20
|
+
end
|
21
|
+
|
22
|
+
def logger
|
23
|
+
@logger ||= Logging.logger[self]
|
24
|
+
end
|
25
|
+
|
26
|
+
def api_type
|
27
|
+
@api_type || (auth_data.key?(:apikey) ? :api : :winapi)
|
28
|
+
end
|
29
|
+
|
30
|
+
def folders
|
31
|
+
ResourceList.new self, Passwordstate::Resources::Folder,
|
32
|
+
only: %i[all search post]
|
33
|
+
end
|
34
|
+
|
35
|
+
def hosts
|
36
|
+
ResourceList.new self, Passwordstate::Resources::Host,
|
37
|
+
only: %i[search post delete]
|
38
|
+
end
|
39
|
+
|
40
|
+
def passwords
|
41
|
+
ResourceList.new self, Passwordstate::Resources::Password
|
42
|
+
end
|
43
|
+
|
44
|
+
def password_lists
|
45
|
+
ResourceList.new self, Passwordstate::Resources::PasswordList,
|
46
|
+
except: %i[put delete]
|
47
|
+
end
|
48
|
+
|
49
|
+
def valid?
|
50
|
+
version
|
51
|
+
true
|
52
|
+
rescue StandardError
|
53
|
+
false
|
54
|
+
end
|
55
|
+
|
56
|
+
def version
|
57
|
+
@version ||= begin
|
58
|
+
html = request(:get, '', allow_html: true)
|
59
|
+
version = html.find_line { |line| line.include? '<span>V</span>' }
|
60
|
+
version = />(\d\.\d) \(Build (.+)\)</.match(version)
|
61
|
+
"#{version[1]}.#{version[2]}"
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
def request(method, api_path, options = {})
|
66
|
+
uri = URI(server_url + "/#{api_type}/" + api_path)
|
67
|
+
uri.query = URI.encode_www_form(options.fetch(:query)) if options.key? :query
|
68
|
+
uri.query = nil if uri.query&.empty?
|
69
|
+
|
70
|
+
req_obj = Net::HTTP.const_get(method.to_s.capitalize.to_sym).new uri
|
71
|
+
if options.key? :body
|
72
|
+
req_obj.body = options.fetch(:body)
|
73
|
+
req_obj.body = req_obj.body.to_json unless req_obj.body.is_a?(String)
|
74
|
+
req_obj['content-type'] = 'application/json'
|
75
|
+
end
|
76
|
+
|
77
|
+
req_obj.ntlm_auth(auth_data[:username], auth_data[:password]) if api_type == :winapi
|
78
|
+
headers.each { |h, v| req_obj[h] = v }
|
79
|
+
req_obj['APIKey'] = auth_data[:apikey] if api_type == :api
|
80
|
+
|
81
|
+
print_http req_obj
|
82
|
+
res_obj = http.request req_obj
|
83
|
+
print_http res_obj
|
84
|
+
|
85
|
+
return true if res_obj.is_a? Net::HTTPNoContent
|
86
|
+
|
87
|
+
data = JSON.parse(res_obj.body) rescue nil
|
88
|
+
if data
|
89
|
+
return data if res_obj.is_a? Net::HTTPSuccess
|
90
|
+
data = data&.first
|
91
|
+
raise Passwordstate::HTTPError.new(res_obj.code, data&.fetch('errors', []) || [])
|
92
|
+
else
|
93
|
+
return res_obj.body if options.fetch(:allow_html, false)
|
94
|
+
raise Passwordstate::PasswordstateError, 'Response was not parseable as JSON'
|
95
|
+
end
|
96
|
+
end
|
97
|
+
|
98
|
+
def inspect
|
99
|
+
"#{to_s[0..-2]} #{instance_variables.reject { |k| %i[@auth_data @http @logger].include? k }.map { |k| "#{k}=#{instance_variable_get(k).inspect}" }.join ', '}>"
|
100
|
+
end
|
101
|
+
|
102
|
+
private
|
103
|
+
|
104
|
+
def http
|
105
|
+
@http ||= Net::HTTP.new server_url.host, server_url.port
|
106
|
+
return @http if @http.active?
|
107
|
+
|
108
|
+
@http.use_ssl = server_url.scheme == 'https'
|
109
|
+
@http.verify_mode = validate_certificate ? ::OpenSSL::SSL::VERIFY_NONE : nil
|
110
|
+
@http.start
|
111
|
+
end
|
112
|
+
|
113
|
+
def print_http(http)
|
114
|
+
return unless logger.debug?
|
115
|
+
|
116
|
+
if http.is_a? Net::HTTPRequest
|
117
|
+
dir = '>'
|
118
|
+
logger.debug "#{dir} Sending a #{http.method} request to `#{http.path}`:"
|
119
|
+
else
|
120
|
+
dir = '<'
|
121
|
+
logger.debug "#{dir} Received a #{http.code} #{http.message} response:"
|
122
|
+
end
|
123
|
+
http.to_hash.map { |k, v| "#{k}: #{%w[authorization apikey].include?(k.downcase) ? '[redacted]' : v.join(', ')}" }.each do |h|
|
124
|
+
logger.debug "#{dir} #{h}"
|
125
|
+
end
|
126
|
+
logger.debug dir
|
127
|
+
|
128
|
+
return if http.body.nil?
|
129
|
+
clean_body = JSON.parse(http.body) rescue nil
|
130
|
+
if clean_body
|
131
|
+
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
|
132
|
+
else
|
133
|
+
clean_body = http.body
|
134
|
+
end
|
135
|
+
logger.debug "#{dir} #{clean_body.length < 200 ? clean_body : clean_body.slice(0..200) + "... [truncated, #{clean_body.length} Bytes]"}" if clean_body
|
136
|
+
end
|
137
|
+
end
|
138
|
+
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
module Passwordstate
|
2
|
+
class PasswordstateError < RuntimeError; end
|
3
|
+
|
4
|
+
class HTTPError < PasswordstateError
|
5
|
+
attr_reader :code, :errors
|
6
|
+
|
7
|
+
def initialize(code, errors = [])
|
8
|
+
@code = code.to_i
|
9
|
+
@errors = errors
|
10
|
+
|
11
|
+
super <<-ERRMSG
|
12
|
+
Passwordstate responded with an error to the request;
|
13
|
+
#{errors.map { |err| err['message'] || err['phrase'] }.join(', ')}
|
14
|
+
ERRMSG
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
@@ -0,0 +1,222 @@
|
|
1
|
+
module Passwordstate
|
2
|
+
# A simple resource DSL
|
3
|
+
class Resource
|
4
|
+
attr_reader :client
|
5
|
+
|
6
|
+
def get(query = {})
|
7
|
+
set! self.class.get(client, send(self.class.index_field), query)
|
8
|
+
end
|
9
|
+
|
10
|
+
def put(body = {}, query = {})
|
11
|
+
to_send = modified.merge(self.class.index_field => send(self.class.index_field))
|
12
|
+
set! self.class.put(client, to_send.merge(body), query).first
|
13
|
+
end
|
14
|
+
|
15
|
+
def post(body = {}, query = {})
|
16
|
+
set! self.class.post(client, attributes.merge(body), query)
|
17
|
+
end
|
18
|
+
|
19
|
+
def delete(query = {})
|
20
|
+
self.class.delete(client, send(self.class.index_field), query)
|
21
|
+
end
|
22
|
+
|
23
|
+
def initialize(data)
|
24
|
+
@client = data.delete :_client
|
25
|
+
set! data, false
|
26
|
+
old
|
27
|
+
end
|
28
|
+
|
29
|
+
def stored?
|
30
|
+
!send(self.class.index_field).nil?
|
31
|
+
end
|
32
|
+
|
33
|
+
def self.all(client, query = {})
|
34
|
+
path = query.fetch(:_api_path, api_path)
|
35
|
+
query = passwordstateify_hash query.reject { |k| k.to_s.start_with? '_' }
|
36
|
+
|
37
|
+
[client.request(:get, path, query: query)].flatten.map do |object|
|
38
|
+
new object.merge(_client: client)
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
def self.get(client, object, query = {})
|
43
|
+
path = query.fetch(:_api_path, api_path)
|
44
|
+
query = passwordstateify_hash query.reject { |k| k.to_s.start_with? '_' }
|
45
|
+
|
46
|
+
object = object.send(object.class.send(index_field)) if object.is_a? Resource
|
47
|
+
resp = client.request(:get, "#{path}/#{object}", query: query).map do |data|
|
48
|
+
new data.merge(_client: client)
|
49
|
+
end
|
50
|
+
return resp.first if resp.one? || resp.empty?
|
51
|
+
resp
|
52
|
+
end
|
53
|
+
|
54
|
+
def self.post(client, data, query = {})
|
55
|
+
path = query.fetch(:_api_path, api_path)
|
56
|
+
data = passwordstateify_hash data
|
57
|
+
query = passwordstateify_hash query.reject { |k| k.to_s.start_with? '_' }
|
58
|
+
|
59
|
+
new [client.request(:post, path, body: data, query: query)].flatten.first.merge(_client: client)
|
60
|
+
end
|
61
|
+
|
62
|
+
def self.put(client, data, query = {})
|
63
|
+
path = query.fetch(:_api_path, api_path)
|
64
|
+
data = passwordstateify_hash data
|
65
|
+
query = passwordstateify_hash query.reject { |k| k.to_s.start_with? '_' }
|
66
|
+
|
67
|
+
client.request :put, path, body: data, query: query
|
68
|
+
end
|
69
|
+
|
70
|
+
def self.delete(client, object, query = {})
|
71
|
+
path = query.fetch(:_api_path, api_path)
|
72
|
+
query = passwordstateify_hash query.reject { |k| k.to_s.start_with? '_' }
|
73
|
+
|
74
|
+
object = object.send(object.class.send(index_field)) if object.is_a? Resource
|
75
|
+
client.request :delete, "#{path}/#{object}", query: query
|
76
|
+
end
|
77
|
+
|
78
|
+
def self.passwordstateify_hash(hash)
|
79
|
+
Hash[hash.map { |k, v| [ruby_to_passwordstate_field(k), v] }]
|
80
|
+
end
|
81
|
+
|
82
|
+
def api_path
|
83
|
+
self.class.instance_variable_get :@api_path
|
84
|
+
end
|
85
|
+
|
86
|
+
def attributes(ignore_redact = true)
|
87
|
+
Hash[(self.class.send(:accessor_field_names) + self.class.send(:read_field_names) + self.class.send(:write_field_names)).map do |field|
|
88
|
+
redact = self.class.send(:field_options)[field]&.fetch(:redact, false) && !ignore_redact
|
89
|
+
value = instance_variable_get("@#{field}".to_sym) unless redact
|
90
|
+
value = '[ REDACTED ]' if redact
|
91
|
+
[field, value]
|
92
|
+
end].reject { |_k, v| v.nil? }
|
93
|
+
end
|
94
|
+
|
95
|
+
def inspect
|
96
|
+
"#{to_s[0..-2]} #{attributes(false).reject { |_k, v| v.nil? }.map { |k, v| "@#{k}=#{v.inspect}" }.join(', ')}>"
|
97
|
+
end
|
98
|
+
|
99
|
+
protected
|
100
|
+
|
101
|
+
def modified
|
102
|
+
attribs = attributes
|
103
|
+
attribs.reject { |field| old[field] == attribs[field] }
|
104
|
+
end
|
105
|
+
|
106
|
+
def modified?(field)
|
107
|
+
modified.include? field
|
108
|
+
end
|
109
|
+
|
110
|
+
def old
|
111
|
+
@old ||= attributes.dup
|
112
|
+
end
|
113
|
+
|
114
|
+
def set!(data, store_old = true)
|
115
|
+
@old = attributes.dup if store_old
|
116
|
+
data = data.attributes if data.is_a? Passwordstate::Resource
|
117
|
+
data.each do |key, value|
|
118
|
+
field = self.class.passwordstate_to_ruby_field(key)
|
119
|
+
opts = self.class.send(:field_options)[field]
|
120
|
+
|
121
|
+
value = nil if value.is_a?(String) && value.empty?
|
122
|
+
|
123
|
+
if !value.nil? && opts&.key?(:is)
|
124
|
+
klass = opts.fetch(:is)
|
125
|
+
parsed_value = klass.send :parse, value rescue nil if klass.respond_to? :parse
|
126
|
+
parsed_value ||= klass.send :new, value rescue nil if klass.respond_to? :new
|
127
|
+
end
|
128
|
+
|
129
|
+
instance_variable_set "@#{field}".to_sym, parsed_value || value
|
130
|
+
end
|
131
|
+
self
|
132
|
+
end
|
133
|
+
|
134
|
+
class << self
|
135
|
+
alias search all
|
136
|
+
|
137
|
+
def api_path(path = nil)
|
138
|
+
@api_path = path unless path.nil?
|
139
|
+
@api_path
|
140
|
+
end
|
141
|
+
|
142
|
+
def index_field(field = nil)
|
143
|
+
@index_field = field unless field.nil?
|
144
|
+
@index_field
|
145
|
+
end
|
146
|
+
|
147
|
+
def passwordstate_to_ruby_field(field)
|
148
|
+
opts = send(:field_options).find { |(_k, v)| v[:name] == field }
|
149
|
+
opts&.first || field.to_s.snake_case.to_sym
|
150
|
+
end
|
151
|
+
|
152
|
+
def ruby_to_passwordstate_field(field)
|
153
|
+
send(:field_options)[field]&.[](:name) || field.to_s.camel_case
|
154
|
+
end
|
155
|
+
|
156
|
+
protected
|
157
|
+
|
158
|
+
def accessor_field_names
|
159
|
+
@accessor_field_names ||= []
|
160
|
+
end
|
161
|
+
|
162
|
+
def read_field_names
|
163
|
+
@read_field_names ||= []
|
164
|
+
end
|
165
|
+
|
166
|
+
def write_field_names
|
167
|
+
@write_field_names ||= []
|
168
|
+
end
|
169
|
+
|
170
|
+
def field_options
|
171
|
+
@field_options ||= {}
|
172
|
+
end
|
173
|
+
|
174
|
+
def read_only
|
175
|
+
# TODO
|
176
|
+
end
|
177
|
+
|
178
|
+
def accessor_fields(*fields)
|
179
|
+
fields.each do |field|
|
180
|
+
if field.is_a? Symbol
|
181
|
+
accessor_field_names << field
|
182
|
+
attr_accessor field
|
183
|
+
else
|
184
|
+
field_options[accessor_field_names.last] = field
|
185
|
+
end
|
186
|
+
end
|
187
|
+
end
|
188
|
+
|
189
|
+
def read_fields(*fields)
|
190
|
+
fields.each do |field|
|
191
|
+
if field.is_a? Symbol
|
192
|
+
read_field_names << field
|
193
|
+
attr_reader field
|
194
|
+
else
|
195
|
+
field_options[read_field_names.last] = field
|
196
|
+
end
|
197
|
+
end
|
198
|
+
end
|
199
|
+
|
200
|
+
def write_fields(*fields)
|
201
|
+
fields.each do |field|
|
202
|
+
if field.is_a? Symbol
|
203
|
+
write_field_names << field
|
204
|
+
attr_writer field
|
205
|
+
else
|
206
|
+
field_options[write_field_names.last] = field
|
207
|
+
end
|
208
|
+
end
|
209
|
+
end
|
210
|
+
end
|
211
|
+
end
|
212
|
+
|
213
|
+
module Resources
|
214
|
+
autoload :Document, 'passwordstate/resources/document'
|
215
|
+
autoload :Folder, 'passwordstate/resources/folder'
|
216
|
+
autoload :Host, 'passwordstate/resources/host'
|
217
|
+
autoload :PasswordList, 'passwordstate/resources/password_list'
|
218
|
+
autoload :Password, 'passwordstate/resources/password'
|
219
|
+
autoload :PasswordHistory, 'passwordstate/resources/password'
|
220
|
+
autoload :Report, 'passwordstate/resources/report'
|
221
|
+
end
|
222
|
+
end
|
@@ -0,0 +1,126 @@
|
|
1
|
+
module Passwordstate
|
2
|
+
class ResourceList < Array
|
3
|
+
Array.public_instance_methods(false).each do |method|
|
4
|
+
next if %i[reject select slice clear inspect].include?(method.to_sym)
|
5
|
+
class_eval <<-EVAL, __FILE__, __LINE__ + 1
|
6
|
+
def #{method}(*args)
|
7
|
+
lazy_load unless @loaded
|
8
|
+
super
|
9
|
+
end
|
10
|
+
EVAL
|
11
|
+
end
|
12
|
+
|
13
|
+
%w[reject select slice].each do |method|
|
14
|
+
class_eval <<-EVAL, __FILE__, __LINE__ + 1
|
15
|
+
def #{method}(*args)
|
16
|
+
lazy_load unless @loaded
|
17
|
+
data = super
|
18
|
+
self.clone.clear.concat(data)
|
19
|
+
end
|
20
|
+
EVAL
|
21
|
+
end
|
22
|
+
|
23
|
+
def inspect
|
24
|
+
lazy_load unless @loaded
|
25
|
+
super
|
26
|
+
end
|
27
|
+
|
28
|
+
attr_reader :client, :resource, :options
|
29
|
+
|
30
|
+
def initialize(client, resource, options = {})
|
31
|
+
@client = client
|
32
|
+
@resource = resource
|
33
|
+
@loaded = false
|
34
|
+
@options = options
|
35
|
+
|
36
|
+
options[:only] = [options[:only]].flatten if options.key? :only
|
37
|
+
options[:except] = [options[:except]].flatten if options.key? :except
|
38
|
+
end
|
39
|
+
|
40
|
+
def clear
|
41
|
+
@loaded = super
|
42
|
+
end
|
43
|
+
|
44
|
+
def reload
|
45
|
+
clear && lazy_load
|
46
|
+
self
|
47
|
+
end
|
48
|
+
|
49
|
+
def load(entries)
|
50
|
+
clear && entries.each { |obj| self << obj }
|
51
|
+
true
|
52
|
+
end
|
53
|
+
|
54
|
+
def operation_supported?(operation)
|
55
|
+
return nil unless %i[search all get post put delete].include?(operation)
|
56
|
+
return false if options.key?(:only) && !options[:only].include?(operation)
|
57
|
+
return false if options.key?(:except) && options[:except].include?(operation)
|
58
|
+
!options.fetch("#{operation}_path".to_sym, '').nil?
|
59
|
+
end
|
60
|
+
|
61
|
+
def new(data)
|
62
|
+
resource.new options.fetch(:object_data, {}).merge(data).merge(_client: client)
|
63
|
+
end
|
64
|
+
|
65
|
+
def create(data)
|
66
|
+
raise 'Operation not supported' unless operation_supported?(:post)
|
67
|
+
obj = resource.new options.fetch(:object_data, {}).merge(data).merge(_client: client)
|
68
|
+
obj.post
|
69
|
+
obj
|
70
|
+
end
|
71
|
+
|
72
|
+
def search(query = {})
|
73
|
+
raise 'Operation not supported' unless operation_supported?(:search)
|
74
|
+
api_path = options.fetch(:search_path, resource.api_path)
|
75
|
+
query = options.fetch(:search_query, {}).merge(query)
|
76
|
+
|
77
|
+
resource.search(client, query.merge(_api_path: api_path))
|
78
|
+
end
|
79
|
+
|
80
|
+
def all(query = {})
|
81
|
+
raise 'Operation not supported' unless operation_supported?(:all)
|
82
|
+
api_path = options.fetch(:all_path, resource.api_path)
|
83
|
+
query = options.fetch(:all_query, {}).merge(query)
|
84
|
+
|
85
|
+
load resource.all(client, query.merge(_api_path: api_path))
|
86
|
+
end
|
87
|
+
|
88
|
+
def get(id, query = {})
|
89
|
+
raise 'Operation not supported' unless operation_supported?(:get)
|
90
|
+
api_path = options.fetch(:get_path, resource.api_path)
|
91
|
+
query = options.fetch(:get_query, {}).merge(query)
|
92
|
+
|
93
|
+
resource.get(client, id, query.merge(_api_path: api_path))
|
94
|
+
end
|
95
|
+
|
96
|
+
def post(data, query = {})
|
97
|
+
raise 'Operation not supported' unless operation_supported?(:post)
|
98
|
+
api_path = options.fetch(:post_path, resource.api_path)
|
99
|
+
query = options.fetch(:post_query, {}).merge(query)
|
100
|
+
|
101
|
+
resource.post(client, data, query.merge(_api_path: api_path))
|
102
|
+
end
|
103
|
+
|
104
|
+
def put(data, query = {})
|
105
|
+
raise 'Operation not supported' unless operation_supported?(:put)
|
106
|
+
api_path = options.fetch(:put_path, resource.api_path)
|
107
|
+
query = options.fetch(:put_query, {}).merge(query)
|
108
|
+
|
109
|
+
resource.put(client, data, query.merge(_api_path: api_path))
|
110
|
+
end
|
111
|
+
|
112
|
+
def delete(id, query = {})
|
113
|
+
raise 'Operation not supported' unless operation_supported?(:delete)
|
114
|
+
api_path = options.fetch(:delete_path, resource.api_path)
|
115
|
+
query = options.fetch(:delete_query, {}).merge(query)
|
116
|
+
|
117
|
+
resource.delete(client, id, query.merge(_api_path: api_path))
|
118
|
+
end
|
119
|
+
|
120
|
+
private
|
121
|
+
|
122
|
+
def lazy_load
|
123
|
+
all
|
124
|
+
end
|
125
|
+
end
|
126
|
+
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
module Passwordstate
|
2
|
+
module Resources
|
3
|
+
class Document < Passwordstate::Resource
|
4
|
+
api_path 'document'
|
5
|
+
|
6
|
+
index_field :document_id
|
7
|
+
|
8
|
+
read_fields :document_id, { name: 'DocumentID' },
|
9
|
+
:document_name
|
10
|
+
|
11
|
+
def self.search(client, store, options = {})
|
12
|
+
client.request :get, "#{api_path}/#{store}/", query: options
|
13
|
+
end
|
14
|
+
|
15
|
+
def self.get(client, store, object)
|
16
|
+
client.request :get, "#{api_path}/#{store}/#{object}"
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
module Passwordstate
|
2
|
+
module Resources
|
3
|
+
class Folder < Passwordstate::Resource
|
4
|
+
api_path 'folders'
|
5
|
+
|
6
|
+
index_field :folder_id
|
7
|
+
|
8
|
+
accessor_fields :folder_name,
|
9
|
+
:description
|
10
|
+
|
11
|
+
read_fields :folder_id, { name: 'FolderID' },
|
12
|
+
:tree_path,
|
13
|
+
:site_id, { name: 'SiteID' },
|
14
|
+
:site_location
|
15
|
+
|
16
|
+
def password_lists
|
17
|
+
Passwordstate::ResourceList.new client, Passwordstate::Resources::PasswordList,
|
18
|
+
search_query: { tree_path: tree_path },
|
19
|
+
all_path: 'searchpasswordlists',
|
20
|
+
all_query: { tree_path: tree_path },
|
21
|
+
object_data: { nest_undef_folder_id: folder_id }
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
@@ -0,0 +1,32 @@
|
|
1
|
+
require 'ipaddr'
|
2
|
+
|
3
|
+
module Passwordstate
|
4
|
+
module Resources
|
5
|
+
class Host < Passwordstate::Resource
|
6
|
+
api_path 'hosts'
|
7
|
+
|
8
|
+
index_field :host_name
|
9
|
+
|
10
|
+
accessor_fields :host_name,
|
11
|
+
:host_type,
|
12
|
+
:operating_system,
|
13
|
+
:database_server_type,
|
14
|
+
:sql_instance_name, { name: 'SQLInstanceName' },
|
15
|
+
:database_port_number,
|
16
|
+
:remote_connection_type,
|
17
|
+
:remote_connection_port_number,
|
18
|
+
:remote_connection_parameters,
|
19
|
+
:tag,
|
20
|
+
:title,
|
21
|
+
:site_id, { name: 'SiteID' },
|
22
|
+
:internal_ip, { name: 'InternalIP', is: IPAddr },
|
23
|
+
:external_ip, { name: 'ExternalIP', is: IPAddr },
|
24
|
+
:mac_address, { name: 'MACAddress' },
|
25
|
+
:virtual_machine,
|
26
|
+
:virtual_machine_type,
|
27
|
+
:notes
|
28
|
+
|
29
|
+
read_fields :host_id, { name: 'HostID' } # rubocop:disable Style/BracesAroundHashParameters
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
@@ -0,0 +1,134 @@
|
|
1
|
+
module Passwordstate
|
2
|
+
module Resources
|
3
|
+
class Password < Passwordstate::Resource
|
4
|
+
api_path 'passwords'
|
5
|
+
|
6
|
+
index_field :password_id
|
7
|
+
|
8
|
+
accessor_fields :title,
|
9
|
+
:domain,
|
10
|
+
:host_name,
|
11
|
+
:user_name,
|
12
|
+
:description,
|
13
|
+
:generic_field_1,
|
14
|
+
:generic_field_2,
|
15
|
+
:generic_field_3,
|
16
|
+
:generic_field_4,
|
17
|
+
:generic_field_5,
|
18
|
+
:generic_field_6,
|
19
|
+
:generic_field_7,
|
20
|
+
:generic_field_8,
|
21
|
+
:generic_field_9,
|
22
|
+
:generic_field_10,
|
23
|
+
:account_type_id, { name: 'AccountTypeID' },
|
24
|
+
:account_type,
|
25
|
+
:notes,
|
26
|
+
:url,
|
27
|
+
:password, { redact: true },
|
28
|
+
:expiry_date, { is: Time },
|
29
|
+
:allow_export,
|
30
|
+
:web_user_id, { name: 'WebUser_ID' },
|
31
|
+
:web_password_id, { name: 'WebPassword_ID' } # rubocop:disable Style/BracesAroundHashParameters
|
32
|
+
|
33
|
+
read_fields :password_id, { name: 'PasswordID' } # rubocop:disable Style/BracesAroundHashParameters
|
34
|
+
|
35
|
+
# Things that can be set in a POST/PUT request
|
36
|
+
# TODO: Do this properly
|
37
|
+
write_fields :generate_password,
|
38
|
+
:generate_gen_field_password,
|
39
|
+
:password_reset_enabled,
|
40
|
+
:enable_password_reset_schedule,
|
41
|
+
:password_reset_schedule,
|
42
|
+
:add_days_to_expiry_date,
|
43
|
+
:script_id, { name: 'ScriptID' },
|
44
|
+
:password_list_id, { name: 'PasswordListID' }, # POST only
|
45
|
+
:privileged_account_id,
|
46
|
+
:heartbeat_enabled,
|
47
|
+
:heartbeat_schedule,
|
48
|
+
:validation_script_id, { name: 'ValidationScriptID' },
|
49
|
+
:host_name,
|
50
|
+
:ad_domain_netbios, { name: 'ADDomainNetBIOS' },
|
51
|
+
:validate_with_priv_account
|
52
|
+
|
53
|
+
def check_in
|
54
|
+
client.request :get, "passwords/#{password_id}", query: passwordstatify_hash(check_in: nil)
|
55
|
+
end
|
56
|
+
|
57
|
+
def history
|
58
|
+
raise 'Password history only available on stored passwords' unless stored?
|
59
|
+
Passwordstate::ResourceList.new client, PasswordHistory,
|
60
|
+
all_path: "passwordhistory/#{password_id}",
|
61
|
+
only: :all
|
62
|
+
end
|
63
|
+
|
64
|
+
def delete(recycle = false, query = {})
|
65
|
+
super query.merge(move_to_recycle_bin: recycle)
|
66
|
+
end
|
67
|
+
|
68
|
+
def add_dependency(data = {})
|
69
|
+
raise 'Password dependency creation only available for stored passwords' unless stored?
|
70
|
+
client.request :post, 'dependencies', body: self.class.passwordstatify_hash(data.merge(password_id: password_id))
|
71
|
+
end
|
72
|
+
|
73
|
+
def self.all(client, query = {})
|
74
|
+
super client, { query_all: true }.merge(query)
|
75
|
+
end
|
76
|
+
|
77
|
+
def self.search(client, query = {})
|
78
|
+
super client, query.merge(_api_path: 'searchpassword')
|
79
|
+
end
|
80
|
+
|
81
|
+
def self.generate(client, options = {})
|
82
|
+
results = client.request(:get, 'generatepassword', query: options).map { |r| r['Password'] }
|
83
|
+
return results.first if results.count == 1
|
84
|
+
results
|
85
|
+
end
|
86
|
+
end
|
87
|
+
|
88
|
+
class PasswordHistory < Passwordstate::Resource
|
89
|
+
read_only
|
90
|
+
|
91
|
+
api_path 'passwordhistory'
|
92
|
+
|
93
|
+
index_field :password_history_id
|
94
|
+
|
95
|
+
read_fields :password_history_id, { name: 'PasswordHistoryID' },
|
96
|
+
:date_changed, { is: Time },
|
97
|
+
:password_list,
|
98
|
+
:user_id, { name: 'UserID' },
|
99
|
+
:first_name,
|
100
|
+
:surname
|
101
|
+
|
102
|
+
# Password object fields
|
103
|
+
read_fields :title,
|
104
|
+
:domain,
|
105
|
+
:host_name,
|
106
|
+
:user_name,
|
107
|
+
:description,
|
108
|
+
:generic_field_1,
|
109
|
+
:generic_field_2,
|
110
|
+
:generic_field_3,
|
111
|
+
:generic_field_4,
|
112
|
+
:generic_field_5,
|
113
|
+
:generic_field_6,
|
114
|
+
:generic_field_7,
|
115
|
+
:generic_field_8,
|
116
|
+
:generic_field_9,
|
117
|
+
:generic_field_10,
|
118
|
+
:account_type_id, { name: 'AccountTypeID' },
|
119
|
+
:account_type,
|
120
|
+
:notes,
|
121
|
+
:url,
|
122
|
+
:password, { redact: true },
|
123
|
+
:password_id, { name: 'PasswordID' },
|
124
|
+
:expiry_date, { is: Time },
|
125
|
+
:allow_export,
|
126
|
+
:web_user_id, { name: 'WebUser_ID' },
|
127
|
+
:web_password_id, { name: 'WebPassword_ID' } # rubocop:disable Style/BracesAroundHashParameters
|
128
|
+
|
129
|
+
def get
|
130
|
+
raise 'Not applicable'
|
131
|
+
end
|
132
|
+
end
|
133
|
+
end
|
134
|
+
end
|
@@ -0,0 +1,54 @@
|
|
1
|
+
module Passwordstate
|
2
|
+
module Resources
|
3
|
+
class PasswordList < Passwordstate::Resource
|
4
|
+
api_path 'passwordlists'
|
5
|
+
|
6
|
+
index_field :password_list_id
|
7
|
+
|
8
|
+
accessor_fields :password_list,
|
9
|
+
:description,
|
10
|
+
:image_file_name,
|
11
|
+
:guide,
|
12
|
+
:allow_export,
|
13
|
+
:private_password_list,
|
14
|
+
:time_based_access_required,
|
15
|
+
:handshake_approval_required,
|
16
|
+
:password_strength_policy_id, { name: 'PasswordStrengthPolicyID' },
|
17
|
+
:password_generator_id, { name: 'PasswordGeneratorID' },
|
18
|
+
:code_page,
|
19
|
+
:prevent_password_reuse,
|
20
|
+
:authentication_type,
|
21
|
+
:authentication_per_session,
|
22
|
+
:prevent_expiry_date_modification,
|
23
|
+
:reset_expiry_date,
|
24
|
+
:prevent_drag_drop,
|
25
|
+
:prevent_bad_password_use,
|
26
|
+
:provide_access_reason,
|
27
|
+
:password_reset_enabled,
|
28
|
+
:force_password_generator,
|
29
|
+
:hide_passwords,
|
30
|
+
:show_guide,
|
31
|
+
:enable_password_reset_schedule,
|
32
|
+
:password_reset_schedule,
|
33
|
+
:add_days_to_expiry_date
|
34
|
+
|
35
|
+
read_fields :password_list_id, { name: 'PasswordListID' },
|
36
|
+
:tree_path,
|
37
|
+
:total_passwords,
|
38
|
+
:generator_name,
|
39
|
+
:policy_name
|
40
|
+
|
41
|
+
def passwords
|
42
|
+
Passwordstate::ResourceList.new client, Passwordstate::Resources::Password,
|
43
|
+
all_path: "passwords/#{password_list_id}",
|
44
|
+
all_query: { query_all: nil },
|
45
|
+
search_path: "searchpasswords/#{password_list_id}",
|
46
|
+
object_data: { password_list_id: password_list_id }
|
47
|
+
end
|
48
|
+
|
49
|
+
def self.search(client, query = {})
|
50
|
+
super client, query.merge(_api_path: 'searchpasswordlists')
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
@@ -0,0 +1,54 @@
|
|
1
|
+
module Passwordstate
|
2
|
+
module Resources
|
3
|
+
class Report < Passwordstate::Resource
|
4
|
+
REPORT_PARAMETERS = {
|
5
|
+
1 => %i[user_id],
|
6
|
+
2 => %i[user_id site_id],
|
7
|
+
3 => %i[user_id duration],
|
8
|
+
4 => %i[user_id site_id duration],
|
9
|
+
5 => %i[duration],
|
10
|
+
6 => %i[],
|
11
|
+
7 => %i[user_id site_id duration],
|
12
|
+
8 => %i[],
|
13
|
+
9 => %i[],
|
14
|
+
10 => %i[duration],
|
15
|
+
11 => %i[duration],
|
16
|
+
12 => %i[site_id],
|
17
|
+
13 => %i[site_id],
|
18
|
+
14 => %i[site_i],
|
19
|
+
15 => %i[site_id],
|
20
|
+
16 => %i[site_id],
|
21
|
+
17 => %i[duration password_list_ids query_expired_passwords],
|
22
|
+
18 => %i[site_id duration],
|
23
|
+
19 => %i[site_id],
|
24
|
+
20 => %i[site_id],
|
25
|
+
21 => %i[site_id],
|
26
|
+
22 => %i[site_id],
|
27
|
+
23 => %i[site_id],
|
28
|
+
24 => %i[site_id user_id],
|
29
|
+
25 => %i[site_id security_group_name],
|
30
|
+
26 => %i[duration],
|
31
|
+
27 => %i[duration],
|
32
|
+
28 => %i[duration],
|
33
|
+
29 => %i[duration],
|
34
|
+
30 => %i[duration],
|
35
|
+
31 => %i[duration],
|
36
|
+
32 => %i[duration],
|
37
|
+
33 => %i[duration],
|
38
|
+
34 => %i[duration]
|
39
|
+
}.freeze
|
40
|
+
|
41
|
+
api_path 'reporting'
|
42
|
+
|
43
|
+
index_field :report_id
|
44
|
+
|
45
|
+
read_fields :report_id, { name: 'ReportID' },
|
46
|
+
:site_id, { name: 'SiteID' },
|
47
|
+
:user_id, { name: 'UserID' },
|
48
|
+
:security_group_name,
|
49
|
+
:duration,
|
50
|
+
:password_list_ids, { name: 'PasswordListIDs' },
|
51
|
+
:query_expired_passwords
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
@@ -0,0 +1,77 @@
|
|
1
|
+
require 'net/http'
|
2
|
+
require 'net/ntlm'
|
3
|
+
|
4
|
+
module Net
|
5
|
+
module HTTPHeader
|
6
|
+
attr_reader :ntlm_auth_information, :ntlm_auth_options
|
7
|
+
|
8
|
+
def ntlm_auth(username, password, domain = nil, workstation = nil)
|
9
|
+
@ntlm_auth_information = {
|
10
|
+
user: username,
|
11
|
+
password: password
|
12
|
+
}
|
13
|
+
@ntlm_auth_information[:domain] = domain unless domain.nil?
|
14
|
+
@ntlm_auth_options = {}
|
15
|
+
@ntlm_auth_options[:workstation] = workstation unless workstation.nil?
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
class String
|
21
|
+
def camel_case
|
22
|
+
split('_').collect(&:capitalize).join
|
23
|
+
end
|
24
|
+
|
25
|
+
def snake_case
|
26
|
+
gsub(/([A-Z]+)([A-Z][a-z])/, '\1_\2')
|
27
|
+
.gsub(/([a-z\d])([A-Z])/, '\1_\2')
|
28
|
+
.tr('-', '_')
|
29
|
+
.downcase
|
30
|
+
end
|
31
|
+
|
32
|
+
def find_line(&_block)
|
33
|
+
raise ArgumentError, 'No block given' unless block_given?
|
34
|
+
each_line do |line|
|
35
|
+
return line if yield line
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
module Passwordstate
|
41
|
+
module NetHTTPExtensions
|
42
|
+
def request(req, body = nil, &block)
|
43
|
+
return super(req, body, &block) if req.ntlm_auth_information.nil?
|
44
|
+
|
45
|
+
unless started?
|
46
|
+
@last_body = req.body
|
47
|
+
req.body = nil
|
48
|
+
start do
|
49
|
+
req.delete('connection')
|
50
|
+
return request(req, body, &block)
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
type1 = Net::NTLM::Message::Type1.new
|
55
|
+
req['authorization'] = 'NTLM ' + type1.encode64
|
56
|
+
res = super(req, body)
|
57
|
+
|
58
|
+
challenge = res['www-authenticate'][/(?:NTLM|Negotiate) (.+)/, 1]
|
59
|
+
|
60
|
+
if challenge && res.code == '401'
|
61
|
+
type2 = Net::NTLM::Message.decode64 challenge
|
62
|
+
type3 = type2.response(req.ntlm_auth_information, req.ntlm_auth_options)
|
63
|
+
|
64
|
+
req['authorization'] = 'NTLM ' + type3.encode64
|
65
|
+
req.body_stream.rewind if req.body_stream
|
66
|
+
req.body = @last_body if @last_body
|
67
|
+
|
68
|
+
super(req, body, &block)
|
69
|
+
else
|
70
|
+
yield res if block_given?
|
71
|
+
res
|
72
|
+
end
|
73
|
+
end
|
74
|
+
end
|
75
|
+
end
|
76
|
+
|
77
|
+
Net::HTTP.send :prepend, Passwordstate::NetHTTPExtensions unless Net::HTTP.ancestors.include? Passwordstate::NetHTTPExtensions
|
@@ -0,0 +1,25 @@
|
|
1
|
+
require File.join File.expand_path('lib', __dir__), 'passwordstate/version'
|
2
|
+
|
3
|
+
Gem::Specification.new do |spec|
|
4
|
+
spec.name = 'passwordstate'
|
5
|
+
spec.version = Passwordstate::VERSION
|
6
|
+
spec.authors = ['Alexander Olofsson']
|
7
|
+
spec.email = ['alexander.olofsson@liu.se']
|
8
|
+
|
9
|
+
spec.summary = 'A ruby API client for interacting with a passwordstate server'
|
10
|
+
spec.description = spec.summary
|
11
|
+
spec.homepage = 'https://github.com/ananace/ruby-passwordstate'
|
12
|
+
spec.license = 'MIT'
|
13
|
+
|
14
|
+
spec.files = `git ls-files -z`.split("\x0")
|
15
|
+
spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
|
16
|
+
spec.require_paths = ['lib']
|
17
|
+
|
18
|
+
spec.add_dependency 'logging', '~> 2.2'
|
19
|
+
spec.add_dependency 'rubyntlm', '~> 0.6'
|
20
|
+
|
21
|
+
spec.add_development_dependency 'bundler'
|
22
|
+
spec.add_development_dependency 'minitest'
|
23
|
+
spec.add_development_dependency 'rake'
|
24
|
+
spec.add_development_dependency 'rubocop'
|
25
|
+
end
|
data/test/test_helper.rb
ADDED
metadata
ADDED
@@ -0,0 +1,153 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: passwordstate
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.0.1
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Alexander Olofsson
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2018-07-31 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: logging
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - "~>"
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '2.2'
|
20
|
+
type: :runtime
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - "~>"
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '2.2'
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: rubyntlm
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - "~>"
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '0.6'
|
34
|
+
type: :runtime
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - "~>"
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '0.6'
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: bundler
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - ">="
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: '0'
|
48
|
+
type: :development
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - ">="
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: '0'
|
55
|
+
- !ruby/object:Gem::Dependency
|
56
|
+
name: minitest
|
57
|
+
requirement: !ruby/object:Gem::Requirement
|
58
|
+
requirements:
|
59
|
+
- - ">="
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: '0'
|
62
|
+
type: :development
|
63
|
+
prerelease: false
|
64
|
+
version_requirements: !ruby/object:Gem::Requirement
|
65
|
+
requirements:
|
66
|
+
- - ">="
|
67
|
+
- !ruby/object:Gem::Version
|
68
|
+
version: '0'
|
69
|
+
- !ruby/object:Gem::Dependency
|
70
|
+
name: rake
|
71
|
+
requirement: !ruby/object:Gem::Requirement
|
72
|
+
requirements:
|
73
|
+
- - ">="
|
74
|
+
- !ruby/object:Gem::Version
|
75
|
+
version: '0'
|
76
|
+
type: :development
|
77
|
+
prerelease: false
|
78
|
+
version_requirements: !ruby/object:Gem::Requirement
|
79
|
+
requirements:
|
80
|
+
- - ">="
|
81
|
+
- !ruby/object:Gem::Version
|
82
|
+
version: '0'
|
83
|
+
- !ruby/object:Gem::Dependency
|
84
|
+
name: rubocop
|
85
|
+
requirement: !ruby/object:Gem::Requirement
|
86
|
+
requirements:
|
87
|
+
- - ">="
|
88
|
+
- !ruby/object:Gem::Version
|
89
|
+
version: '0'
|
90
|
+
type: :development
|
91
|
+
prerelease: false
|
92
|
+
version_requirements: !ruby/object:Gem::Requirement
|
93
|
+
requirements:
|
94
|
+
- - ">="
|
95
|
+
- !ruby/object:Gem::Version
|
96
|
+
version: '0'
|
97
|
+
description: A ruby API client for interacting with a passwordstate server
|
98
|
+
email:
|
99
|
+
- alexander.olofsson@liu.se
|
100
|
+
executables: []
|
101
|
+
extensions: []
|
102
|
+
extra_rdoc_files: []
|
103
|
+
files:
|
104
|
+
- ".gitignore"
|
105
|
+
- ".gitlab-ci.yml"
|
106
|
+
- ".rubocop.yml"
|
107
|
+
- Gemfile
|
108
|
+
- LICENSE.txt
|
109
|
+
- README.md
|
110
|
+
- Rakefile
|
111
|
+
- lib/passwordstate.rb
|
112
|
+
- lib/passwordstate/client.rb
|
113
|
+
- lib/passwordstate/errors.rb
|
114
|
+
- lib/passwordstate/resource.rb
|
115
|
+
- lib/passwordstate/resource_list.rb
|
116
|
+
- lib/passwordstate/resources/document.rb
|
117
|
+
- lib/passwordstate/resources/folder.rb
|
118
|
+
- lib/passwordstate/resources/host.rb
|
119
|
+
- lib/passwordstate/resources/password.rb
|
120
|
+
- lib/passwordstate/resources/password_list.rb
|
121
|
+
- lib/passwordstate/resources/report.rb
|
122
|
+
- lib/passwordstate/util.rb
|
123
|
+
- lib/passwordstate/version.rb
|
124
|
+
- passwordstate.gemspec
|
125
|
+
- test/passwordstate_test.rb
|
126
|
+
- test/test_helper.rb
|
127
|
+
homepage: https://github.com/ananace/ruby-passwordstate
|
128
|
+
licenses:
|
129
|
+
- MIT
|
130
|
+
metadata: {}
|
131
|
+
post_install_message:
|
132
|
+
rdoc_options: []
|
133
|
+
require_paths:
|
134
|
+
- lib
|
135
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
136
|
+
requirements:
|
137
|
+
- - ">="
|
138
|
+
- !ruby/object:Gem::Version
|
139
|
+
version: '0'
|
140
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
141
|
+
requirements:
|
142
|
+
- - ">="
|
143
|
+
- !ruby/object:Gem::Version
|
144
|
+
version: '0'
|
145
|
+
requirements: []
|
146
|
+
rubyforge_project:
|
147
|
+
rubygems_version: 2.7.6
|
148
|
+
signing_key:
|
149
|
+
specification_version: 4
|
150
|
+
summary: A ruby API client for interacting with a passwordstate server
|
151
|
+
test_files:
|
152
|
+
- test/passwordstate_test.rb
|
153
|
+
- test/test_helper.rb
|