passwordstate 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -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
@@ -0,0 +1,10 @@
1
+ /.bundle/
2
+ /.yardoc
3
+ /_yardoc/
4
+ /coverage/
5
+ /doc/
6
+ /pkg/
7
+ /spec/reports/
8
+ /tmp/
9
+ /vendor/
10
+ Gemfile.lock
@@ -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
@@ -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
@@ -0,0 +1,6 @@
1
+ source 'https://rubygems.org'
2
+
3
+ git_source(:github) { |repo_name| "https://github.com/#{repo_name}" }
4
+
5
+ # Specify your gem's dependencies in passwordstate.gemspec
6
+ gemspec
@@ -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.
@@ -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).
@@ -0,0 +1,10 @@
1
+ require "bundler/gem_tasks"
2
+ require "rake/testtask"
3
+
4
+ Rake::TestTask.new(:test) do |t|
5
+ t.libs << "test"
6
+ t.libs << "lib"
7
+ t.test_files = FileList["test/**/*_test.rb"]
8
+ end
9
+
10
+ task :default => :test
@@ -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,3 @@
1
+ module Passwordstate
2
+ VERSION = '0.0.1'.freeze
3
+ end
@@ -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
@@ -0,0 +1,11 @@
1
+ require 'test_helper'
2
+
3
+ class PasswordstateTest < Minitest::Test
4
+ def test_that_it_has_a_version_number
5
+ refute_nil ::Passwordstate::VERSION
6
+ end
7
+
8
+ def test_it_does_something_useful
9
+ assert false
10
+ end
11
+ end
@@ -0,0 +1,4 @@
1
+ $LOAD_PATH.unshift File.expand_path('../lib', __dir__)
2
+ require 'passwordstate'
3
+
4
+ require 'minitest/autorun'
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