visor-auth 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.
@@ -0,0 +1,186 @@
1
+ require 'mysql2'
2
+ require 'json'
3
+ require 'uri'
4
+
5
+ module Visor
6
+ module Auth
7
+ module Backends
8
+
9
+ # The MySQL Backend for the VISoR Auth.
10
+ #
11
+ class MySQL < Base
12
+ include Visor::Common::Exception
13
+
14
+ # Connection constants
15
+ #
16
+ # Default MySQL database
17
+ DEFAULT_DB = 'visor'
18
+ # Default MySQL host address
19
+ DEFAULT_HOST = '127.0.0.1'
20
+ # Default MySQL host port
21
+ DEFAULT_PORT = 3306
22
+ # Default MySQL user
23
+ DEFAULT_USER = 'visor'
24
+ # Default MySQL password
25
+ DEFAULT_PASSWORD = 'passwd'
26
+
27
+ #CREATE DATABASE visor;
28
+ #CREATE USER 'visor'@'localhost' IDENTIFIED BY 'visor';
29
+ #SET PASSWORD FOR 'visor'@'localhost' = PASSWORD('passwd');
30
+ #GRANT ALL PRIVILEGES ON visor.* TO 'visor'@'localhost';
31
+
32
+ # Initializes a MongoDB Backend instance.
33
+ #
34
+ # @option [Hash] opts Any of the available options can be passed.
35
+ #
36
+ # @option opts [String] :uri The connection uri, if provided, no other option needs to be setted.
37
+ # @option opts [String] :db (DEFAULT_DB) The wanted database.
38
+ # @option opts [String] :host (DEFAULT_HOST) The host address.
39
+ # @option opts [Integer] :port (DEFAULT_PORT) The port to be used.
40
+ # @option opts [String] :user (DEFAULT_USER) The user to be used.
41
+ # @option opts [String] :password (DEFAULT_PASSWORD) The password to be used.
42
+ # @option opts [Object] :conn The connection pool to access database.
43
+ #
44
+ def self.connect(opts = {})
45
+ opts[:uri] ||= ''
46
+ uri = URI.parse(opts[:uri])
47
+ opts[:db] = uri.path ? uri.path.gsub('/', '') : DEFAULT_DB
48
+ opts[:host] = uri.host || DEFAULT_HOST
49
+ opts[:port] = uri.port || DEFAULT_PORT
50
+ opts[:user] = uri.user || DEFAULT_USER
51
+ opts[:password] = uri.password || DEFAULT_PASSWORD
52
+
53
+ self.new opts
54
+ end
55
+
56
+ def initialize(opts)
57
+ super opts
58
+ @conn = connection
59
+ @conn.query %[
60
+ CREATE TABLE IF NOT EXISTS `#{opts[:db]}`.`users` (
61
+ `_id` VARCHAR(45) NOT NULL ,
62
+ `access_key` VARCHAR(45) NOT NULL ,
63
+ `secret_key` VARCHAR(45) NOT NULL ,
64
+ `email` VARCHAR(45) NOT NULL ,
65
+ `created_at` DATETIME NULL ,
66
+ `updated_at` DATETIME NULL ,
67
+ PRIMARY KEY (`_id`) )
68
+ ENGINE = InnoDB;
69
+ ]
70
+ end
71
+
72
+ # Establishes and returns a MySQL database connection and
73
+ # creates Images table if it does not exists.
74
+ #
75
+ # @return [Mysql2::Client] It returns a database client object.
76
+ #
77
+ def connection
78
+ Mysql2::Client.new(host: @host, port: @port, database: @db,
79
+ username: @user, password: @password)
80
+ end
81
+
82
+
83
+ # Returns an array with the registered users.
84
+ #
85
+ # @option [Hash] filters Users attributes for filtering the returned results.
86
+ # Besides common attributes filters, the following options can be passed to.
87
+ #
88
+ # @return [Array] The users information.
89
+ #
90
+ # @raise [NotFound] If there are no registered users.
91
+ #
92
+ def get_users(filters = {})
93
+ validate_query_filters filters unless filters.empty?
94
+ filter = filters.empty? ? 1 : to_sql_where(filters)
95
+ users = @conn.query("SELECT * FROM users WHERE #{filter}", symbolize_keys: true).to_a
96
+ raise NotFound, "No users found." if users.empty? && filters.empty?
97
+ raise NotFound, "No users found with given parameters." if users.empty?
98
+ users
99
+ end
100
+
101
+ # Returns an user information.
102
+ #
103
+ # @param [String] access_key The user access_key.
104
+ #
105
+ # @return [Hash] The requested user information.
106
+ #
107
+ # @raise [NotFound] If user was not found.
108
+ #
109
+ def get_user(access_key)
110
+ user = @conn.query("SELECT * FROM users WHERE access_key='#{access_key}'", symbolize_keys: true).first
111
+ raise NotFound, "No user found with access_key '#{access_key}'." unless user
112
+ user
113
+ end
114
+
115
+ # Delete a registered user.
116
+ #
117
+ # @param [String] access_key The user access_key.
118
+ #
119
+ # @return [hash] The deleted image metadata.
120
+ #
121
+ # @raise [NotFound] If user was not found.
122
+ #
123
+ def delete_user(access_key)
124
+ user = @conn.query("SELECT * FROM users WHERE access_key='#{access_key}'", symbolize_keys: true).first
125
+ raise NotFound, "No user found with access_key '#{access_key}'." unless user
126
+ @conn.query "DELETE FROM users WHERE access_key='#{access_key}'"
127
+ user
128
+ end
129
+
130
+ # Delete all images records.
131
+ #
132
+ def delete_all!
133
+ @conn.query "DELETE FROM users"
134
+ end
135
+
136
+ # Create a new user record for the given information.
137
+ #
138
+ # @param [Hash] user The user information.
139
+ #
140
+ # @return [Hash] The already added user information.
141
+ #
142
+ # @raise [Invalid] If user information validation fails.
143
+ # @raise [ConflictError] If an access_key was already taken.
144
+ #
145
+ def post_user(user)
146
+ validate_data_post user
147
+ exists = @conn.query("SELECT * FROM users WHERE access_key='#{user[:access_key]}'", symbolize_keys: true).first
148
+ raise ConflictError, "The access_key '#{user[:access_key]}' was already taken." if exists
149
+
150
+ set_protected_post user
151
+ keys_values = to_sql_insert(user)
152
+ @conn.query "INSERT INTO users #{keys_values[0]} VALUES #{keys_values[1]}"
153
+ self.get_user(user[:access_key])
154
+ end
155
+
156
+ # Update an user information.
157
+ #
158
+ # @param [String] access_key The user access_key.
159
+ # @param [Hash] update The user information update.
160
+ #
161
+ # @return [BSON::OrderedHash] The updated user information.
162
+ #
163
+ # @raise [Invalid] If user information validation fails.
164
+ # @raise [ConflictError] If an access_key was already taken.
165
+ # @raise [NotFound] If user was not found.
166
+ #
167
+ def put_user(access_key, update)
168
+ validate_data_put update
169
+ user = @conn.query("SELECT * FROM users WHERE access_key='#{access_key}'", symbolize_keys: true).first
170
+ raise NotFound, "No user found with access_key '#{access_key}'." unless user
171
+
172
+ if update[:access_key]
173
+ exists = @conn.query("SELECT * FROM users WHERE access_key='#{update[:access_key]}'", symbolize_keys: true).first
174
+ raise ConflictError, "The access_key '#{update[:access_key]}' was already taken." if exists
175
+ end
176
+
177
+ set_protected_put update
178
+ @conn.query "UPDATE users SET #{to_sql_update(update)} WHERE access_key='#{access_key}'"
179
+ self.get_user(update[:access_key] || access_key)
180
+ end
181
+
182
+ end
183
+ end
184
+ end
185
+ end
186
+
data/lib/auth/cli.rb ADDED
@@ -0,0 +1,333 @@
1
+ require 'open-uri'
2
+ require 'logger'
3
+ require 'optparse'
4
+ require 'fileutils'
5
+ require 'rack'
6
+
7
+ module Visor
8
+ module Auth
9
+ class CLI
10
+
11
+ attr_reader :app, :cli_name, :argv, :options,
12
+ :port, :host, :env, :command, :parser
13
+
14
+ # Available commands
15
+ COMMANDS = %w[start stop restart status clean]
16
+ # Commands that wont load options from the config file
17
+ NO_CONF_COMMANDS = %w[stop status]
18
+ # Default config files directories to look at
19
+ DEFAULT_DIR = File.expand_path('~/.visor')
20
+ # Default host address
21
+ DEFAULT_HOST = '0.0.0.0'
22
+ # Default port
23
+ DEFAULT_PORT = 4566
24
+ # Default application environment
25
+ DEFAULT_ENV = :production
26
+
27
+ # Initialize a CLI
28
+ #
29
+ def initialize(app, cli_name, argv=ARGV)
30
+ @app = app
31
+ @cli_name = cli_name
32
+ @argv = argv
33
+ @options = default_opts
34
+ @parser = parser
35
+ @command = parse!
36
+ end
37
+
38
+ def default_opts
39
+ {debug: false,
40
+ foreground: false,
41
+ no_proxy: false,
42
+ environment: DEFAULT_ENV}
43
+ end
44
+
45
+ # OptionParser parser
46
+ #
47
+ def parser
48
+ OptionParser.new do |opts|
49
+ opts.banner = "Usage: #{cli_name} [OPTIONS] COMMAND"
50
+
51
+ opts.separator ""
52
+ opts.separator "Commands:"
53
+ opts.separator " start start the server"
54
+ opts.separator " stop stop the server"
55
+ opts.separator " restart restart the server"
56
+ opts.separator " status current server status"
57
+
58
+ opts.separator ""
59
+ opts.separator "Options:"
60
+
61
+ opts.on("-c", "--config FILE", "Load a custom configuration file") do |file|
62
+ options[:config] = File.expand_path(file)
63
+ end
64
+ opts.on("-o", "--host HOST", "listen on HOST (default: #{DEFAULT_HOST})") do |host|
65
+ options[:host] = host.to_s
66
+ end
67
+ opts.on("-p", "--port PORT", "use PORT (default: #{DEFAULT_PORT})") do |port|
68
+ options[:port] = port.to_i
69
+ end
70
+ opts.on("-x", "--no-proxy", "ignore proxy settings if any") do
71
+ options[:no_proxy] = true
72
+ end
73
+ opts.on("-e", "--env ENVIRONMENT", "use ENVIRONMENT for defaults (default: #{DEFAULT_ENV})") do |env|
74
+ options[:environment] = env.to_sym
75
+ end
76
+ opts.on("-F", "--foreground", "don't daemonize, run in the foreground") do
77
+ options[:foreground] = true
78
+ end
79
+
80
+ opts.separator ""
81
+ opts.separator "Common options:"
82
+
83
+ opts.on_tail("-d", "--debug", "Set debugging on (with foreground only)") do
84
+ options[:debug] = true
85
+ end
86
+ opts.on_tail("-h", "--help", "Show this message") do
87
+ puts opts
88
+ exit
89
+ end
90
+ opts.on_tail('-v', '--version', "Show version") do
91
+ puts "VISoR Auth Server v#{Visor::Auth::VERSION}"
92
+ exit
93
+ end
94
+ end
95
+ end
96
+
97
+ # Parse the current shell arguments and run the command.
98
+ # Exits on error.
99
+ #
100
+ def run!
101
+ if command.nil?
102
+ abort @parser.to_s
103
+ elsif COMMANDS.include?(command)
104
+ run_command
105
+ else
106
+ abort "Unknown command: #{command}. Available commands: #{COMMANDS.join(', ')}"
107
+ end
108
+ end
109
+
110
+ # Execute the command
111
+ #
112
+ def run_command
113
+ unless NO_CONF_COMMANDS.include?(command)
114
+ @conf = load_conf_file
115
+ @host = options[:host] || @conf[:bind_host] || DEFAULT_HOST
116
+ @port = options[:port] || @conf[:bind_port] || DEFAULT_PORT
117
+ @env = options[:environment]
118
+ end
119
+
120
+ case command
121
+ when 'start' then start
122
+ when 'stop' then stop
123
+ when 'restart' then restart
124
+ when 'status' then status
125
+ else clean
126
+ end
127
+ exit 0
128
+ end
129
+
130
+ # Remove all files created by the daemon.
131
+ #
132
+ def clean
133
+ begin
134
+ FileUtils.rm(pid_file) rescue Errno::ENOENT
135
+ end
136
+ begin
137
+ FileUtils.rm(url_file) rescue Errno::ENOENT
138
+ end
139
+ put_and_log :warn, "Removed all files created by server start"
140
+ end
141
+
142
+ # Restart server
143
+ #
144
+ def restart
145
+ @restart = true
146
+ stop
147
+ sleep 0.1 while running?
148
+ start
149
+ end
150
+
151
+ # Display current server status
152
+ #
153
+ def status
154
+ if running?
155
+ STDERR.puts "#{cli_name} is running PID: #{fetch_pid} URL: #{fetch_url}"
156
+ else
157
+ STDERR.puts "#{cli_name} is not running."
158
+ end
159
+ end
160
+
161
+ # Stop the server
162
+ #
163
+ def stop
164
+ begin
165
+ pid = File.read(pid_file)
166
+ put_and_log :warn, "Stopping #{cli_name} with PID: #{pid.to_i} Signal: INT"
167
+ Process.kill(:INT, pid.to_i)
168
+ File.delete(url_file)
169
+ rescue
170
+ put_and_log :warn, "Cannot stop #{cli_name}, is it running?"
171
+ exit! 1
172
+ end
173
+ end
174
+
175
+ # Start the server
176
+ #
177
+ def start
178
+ FileUtils.mkpath(DEFAULT_DIR)
179
+ begin
180
+ is_it_running?
181
+ can_use_port?
182
+ write_url
183
+ launch!
184
+ rescue => e
185
+ put_and_log :warn, "ERROR starting #{cli_name}: #{e}"
186
+ exit! 1
187
+ end
188
+ end
189
+
190
+ # Launch the server
191
+ #
192
+ def launch!
193
+ put_and_log :info, "Starting #{cli_name} at #{host}:#{port}"
194
+ debug_settings
195
+
196
+ Rack::Server.start(app: app,
197
+ Host: host,
198
+ Port: port,
199
+ environment: get_env,
200
+ daemonize: daemonize?,
201
+ pid: pid_file)
202
+ end
203
+
204
+ protected
205
+
206
+ def is_it_running?
207
+ if files_exist?(pid_file, url_file)
208
+ if running?
209
+ put_and_log :warn, "'#{cli_name}' is already running at #{fetch_url}"
210
+ exit! 1
211
+ else
212
+ clean
213
+ end
214
+ end
215
+ end
216
+
217
+ def running?
218
+ begin
219
+ Process.kill 0, fetch_pid
220
+ true
221
+ rescue Errno::ESRCH
222
+ false
223
+ rescue Errno::EPERM
224
+ true
225
+ rescue
226
+ false
227
+ end
228
+ end
229
+
230
+ def can_use_port?
231
+ unless port_open?
232
+ put_and_log :warn, "Port #{port} already in use. Please try other."
233
+ exit! 1
234
+ end
235
+ end
236
+
237
+ def port_open?
238
+ begin
239
+ options[:no_proxy] ? open(url, proxy: nil) : open(url)
240
+ false
241
+ rescue OpenURI::HTTPError #TODO: quick-fix, try solve this
242
+ false
243
+ rescue Errno::ECONNREFUSED
244
+ true
245
+ end
246
+ end
247
+
248
+ def daemonize?
249
+ !options[:foreground]
250
+ end
251
+
252
+ def get_env
253
+ env == 'development' ? env : 'deployment'
254
+ end
255
+
256
+ def logger
257
+ @conf ||= load_conf_file
258
+ @logger ||=
259
+ begin
260
+ log = options[:foreground] ? Logger.new(STDERR) : Visor::Common::Config.build_logger(:visor_auth)
261
+ conf_level = @conf[:log_level] == 'INFO' ? 1 : 0
262
+ log.level = options[:debug] ? 0 : conf_level
263
+ log.formatter = Proc.new {|s, t, n, msg| "[#{t.strftime("%Y-%m-%d %H:%M:%S")}] #{s} - #{msg}\n"}
264
+ log
265
+ end
266
+ end
267
+
268
+ def put_and_log(level, msg)
269
+ STDERR.puts msg
270
+ logger.send level, msg
271
+ end
272
+
273
+ def parse!
274
+ parser.parse! argv
275
+ argv.shift
276
+ end
277
+
278
+ def debug_settings
279
+ logger.debug "Configurations loaded from #{@conf[:file]}:"
280
+ logger.debug "***************************************************"
281
+ @conf.each { |k, v| logger.debug "#{k}: #{v}" }
282
+ logger.debug "***************************************************"
283
+
284
+ logger.debug "Configurations passed from #{cli_name} CLI:"
285
+ logger.debug "***************************************************"
286
+ options.each { |k, v| logger.debug "#{k}: #{v}" if options[k] != default_opts[k] }
287
+ logger.debug "***************************************************"
288
+ end
289
+
290
+ def files_exist?(*files)
291
+ files.each { |file| return false unless File.exists?(File.expand_path(file)) }
292
+ true
293
+ end
294
+
295
+ def write_url
296
+ File.open(url_file, 'w') { |f| f << url }
297
+ end
298
+
299
+ def load_conf_file
300
+ Visor::Common::Config.load_config(:visor_auth, options[:config])
301
+ end
302
+
303
+ def safe_cli_name
304
+ cli_name.gsub('-', '_')
305
+ end
306
+
307
+ def fetch_pid
308
+ IO.read(pid_file).to_i
309
+ rescue
310
+ nil
311
+ end
312
+
313
+ def fetch_url
314
+ IO.read(url_file).split('//').last
315
+ rescue
316
+ nil
317
+ end
318
+
319
+ def pid_file
320
+ File.join(DEFAULT_DIR, "#{safe_cli_name}.pid")
321
+ end
322
+
323
+ def url_file
324
+ File.join(DEFAULT_DIR, "#{safe_cli_name}.url")
325
+ end
326
+
327
+ def url
328
+ "http://#{host}:#{port}"
329
+ end
330
+
331
+ end
332
+ end
333
+ end
@@ -0,0 +1,149 @@
1
+ require 'net/http'
2
+ require 'net/https'
3
+ require 'uri'
4
+ require 'json'
5
+
6
+ module Visor
7
+ module Auth
8
+
9
+ # The Client API for the VISoR Auth.
10
+ #
11
+ # After Instantiate a Client object its possible to directly interact with the auth server and its
12
+ # database backend.
13
+ #
14
+ class Client
15
+
16
+ include Visor::Common::Exception
17
+
18
+ configs = Common::Config.load_config :visor_auth
19
+
20
+ DEFAULT_HOST = configs[:bind_host] || '0.0.0.0'
21
+ DEFAULT_PORT = configs[:bind_port] || 4566
22
+
23
+ attr_reader :host, :port, :ssl
24
+
25
+ def initialize(opts = {})
26
+ @host = opts[:host] || DEFAULT_HOST
27
+ @port = opts[:port] || DEFAULT_PORT
28
+ @ssl = opts[:ssl] || false
29
+ end
30
+
31
+ def get_users(query={})
32
+ str = build_query(query)
33
+ request = Net::HTTP::Get.new("/users#{str}")
34
+ do_request(request)
35
+ end
36
+
37
+ def get_user(access_key)
38
+ request = Net::HTTP::Get.new("/users/#{access_key}")
39
+ do_request(request)
40
+ end
41
+
42
+ def post_user(info)
43
+ request = Net::HTTP::Post.new('/users')
44
+ request.body = prepare_body(info)
45
+ do_request(request)
46
+ end
47
+
48
+ def put_user(access_key, info)
49
+ request = Net::HTTP::Put.new("/users/#{access_key}")
50
+ request.body = prepare_body(info)
51
+ do_request(request)
52
+ end
53
+
54
+ def delete_user(access_key)
55
+ request = Net::HTTP::Delete.new("/users/#{access_key}")
56
+ do_request(request)
57
+ end
58
+
59
+ private
60
+
61
+ # Parses a response body with the JSON parser and extracts and returns a single
62
+ # key value from it if defined, otherwise returns all the body.
63
+ #
64
+ # @param key (nil) [Symbol] The hash key to extract the wanted value.
65
+ # @param response [Net::HTTPResponse] The response which contains the body to parse.
66
+ #
67
+ # @return [String, Hash] If key is provided and exists on the response body, them return
68
+ # its value, otherwise return all the body hash.
69
+ #
70
+ def parse(key=nil, response)
71
+ parsed = JSON.parse(response.body, symbolize_names: true)
72
+ key ? parsed[key] : parsed
73
+ end
74
+
75
+ # Generate a valid URI query string from key/value pairs of the given hash.
76
+ #
77
+ # @param opts [Hash] The hash with the key/value pairs to generate query from.
78
+ #
79
+ # @return [String] The generated query in the form of "?k=v&k1=v1".
80
+ #
81
+ def build_query(h)
82
+ (h.nil? or h.empty?) ? '' : '?' + URI.encode_www_form(h)
83
+ end
84
+
85
+ # Fill common header keys before each request. This sets the 'User-Agent' and 'Accept'
86
+ # headers for every request and additionally sets the 'content-type' header
87
+ # for POST and PUT requests.
88
+ #
89
+ # @param request [Net::HTTPResponse] The request which will be modified in its headers.
90
+ #
91
+ def prepare_headers(request)
92
+ request['User-Agent'] = 'VISoR image server'
93
+ request['Accept'] = 'application/json'
94
+ request['content-type'] = 'application/json' if ['POST', 'PUT'].include?(request.method)
95
+ end
96
+
97
+ # Generate a valid JSON request body for POST and PUT requests.
98
+ # It generates a JSON object encapsulated inside a :image key and then returns it.
99
+ #
100
+ # @param hash [Hash] The hash with the key/value pairs to generate a JSON object from.
101
+ #
102
+ # @return [Hash] If an :image key is already present in the hash, it just returns the plain
103
+ # JSON object, otherwise, encapsulate the hash inside a :image key and returns it.
104
+ #
105
+ def prepare_body(hash)
106
+ hash.has_key?(:user) ? hash.to_json : {user: hash}.to_json
107
+ end
108
+
109
+ # Process requests by preparing its headers, launch them and assert or raise their response.
110
+ #
111
+ # @param request [Net::HTTPResponse] The request which will be launched.
112
+ #
113
+ # @return [String, Hash] If an error is raised, then it parses and returns its message,
114
+ # otherwise it properly parse and return the response body.
115
+ #
116
+ # @raise [NotFound] If required image was not found (on a GET, PUT or DELETE request).
117
+ # @raise [Invalid] If image meta validation fails (on a POST or PUT request).
118
+ #
119
+ def do_request(request)
120
+ prepare_headers(request)
121
+ response = http_or_https.request(request)
122
+ case response
123
+ when Net::HTTPNotFound then
124
+ raise NotFound, parse(:message, response)
125
+ when Net::HTTPBadRequest then
126
+ raise Invalid, parse(:message, response)
127
+ when Net::HTTPConflict then
128
+ raise ConflictError, parse(:message, response)
129
+ else
130
+ parse(:user, response) or parse(:users, response)
131
+ end
132
+ end
133
+
134
+ # Generate a new HTTP or HTTPS connection based on initialization parameters.
135
+ #
136
+ # @return [Net::HTTP] A HTTP or HTTPS (not done yet) connection ready to use.
137
+ #
138
+ def http_or_https
139
+ if @ssl
140
+ #TODO: ssl connection
141
+ #https://github.com/augustl/net-http-cheat-sheet/blob/master/ssl_and_https.rb
142
+ else
143
+ Net::HTTP.new(@host, @port)
144
+ end
145
+ end
146
+
147
+ end
148
+ end
149
+ end