visor-auth 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -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