lib/rcs-db/rest.rb
#
# The REST interface for all the rest Objects
#
# relatives
require_relative 'audit'
require_relative 'rest_response'
# from RCS::Common
require 'rcs-common/trace'
# system
require 'json'
require 'base64'
require 'rcs-common/rest'
module RCS
module DB
class NotAuthorized < StandardError
def initialize(actual, required)
@message = "required priv is #{required} you have #{actual}"
super @message
end
end
class BasicAuthRequired < StandardError
def initialize
@message = "basic auth required for this uri"
super @message
end
end
class RESTController
include RCS::Tracer
include RCS::Common::Rest
# the parameters passed on the REST request
attr_reader :session, :request
@controllers = {}
def ok(*args)
RESTResponse.new STATUS_OK, *args
end
#def generic(*args)
# return RESTResponse.new *args
#end
def p404
"<html>\r\n" +
"<head><title>404 Not Found</title></head>\r\n" +
"<body bgcolor=\"white\">\r\n" +
"<center><h1>404 Not Found</h1></center>\r\n" +
"<hr><center>nginx</center>\r\n" +
"</body>\r\n" +
"</html>\r\n"
end
def not_found(message='', opts={}, callback=nil)
RESTResponse.new(STATUS_NOT_FOUND, message, opts, callback)
end
def redirect(message='', opts={}, callback=nil)
opts[:content_type] = 'text/html'
RESTResponse.new(STATUS_REDIRECT, message, opts, callback)
end
def p403
"<html>\r\n" +
"<head><title>403 Forbidden</title></head>\r\n" +
"<body bgcolor=\"white\">\r\n" +
"<center><h1>403 Forbidden</h1></center>\r\n" +
"<hr><center>nginx</center>\r\n" +
"</body>\r\n" +
"</html>\r\n"
end
def not_authorized(message='', opts={}, callback=nil)
opts[:content_type] = 'text/html'
RESTResponse.new(STATUS_NOT_AUTHORIZED, message, opts, callback)
end
def auth_required(message='', opts={}, callback=nil)
opts[:content_type] = 'text/html'
RESTResponse.new(STATUS_AUTH_REQUIRED, message, opts, callback)
end
def conflict(message='', callback=nil)
RESTResponse.new(STATUS_CONFLICT, message, {}, callback)
end
def bad_request(message='', callback=nil)
RESTResponse.new(STATUS_BAD_REQUEST, message, {}, callback)
end
def server_error(message='', callback=nil)
RESTResponse.new(STATUS_SERVER_ERROR, message, {}, callback)
end
def stream_file(filename, callback=nil)
begin
RESTFileStream.new(filename, callback)
rescue Exception => e
not_found(e.message)
end
end
def stream_grid(id, collection=nil, callback=proc{})
begin
RESTGridStream.new(id, collection, callback)
rescue Exception => e
not_found(e.message)
end
end
def self.register(klass)
@controllers[klass.to_s] = RCS::DB.const_get(klass) if klass.to_s.end_with? "Controller"
end
def self.sessionmanager
@session_manager || SessionManager.instance
end
def self.get(request)
name = request[:controller]
begin
controller = @controllers["#{name}"].new
rescue Exception => e
controller = InvalidController.new
end
controller.request = request
controller
end
def self.bypass_auth(methods)
self.send(:define_method, :bypass_auth_methods) do
methods
end
end
def self.require_license(*args)
options = args.pop
license = options[:license] || raise("Missing license option")
self.send(:define_method, :require_license_methods) do
{methods: args, license: license}
end
end
def request=(request)
@request = request
identify_action
end
def valid_session?
@session = RESTController.sessionmanager.get(@request[:cookie])
# methods without authentication
# class XXXXController < RESTController
#
# bypass_auth [:method]
#
# def method
# ...
return true if self.respond_to?(:bypass_auth_methods) and bypass_auth_methods.include? @request[:action]
# without a valid session you cannot operate
return false if @session.nil?
return true
end
def identify_action
action = @request[:uri_params].first
if not action.nil? and respond_to?(action)
# use the default http method as action
@request[:action] = @request[:uri_params].shift.to_sym
else
@request[:action] = map_method_to_action(@request[:method], @request[:uri_params].empty?)
end
end
def valid_license?
if respond_to?(:require_license_methods) and require_license_methods[:methods].include?(@request[:action])
LicenseManager.instance.check(require_license_methods[:license])
else
true
end
end
def act!
begin
# check we have a valid session and an action
return not_authorized('INVALID_LICENSE') unless valid_license?
return not_authorized() unless valid_session?
return server_error('NULL_ACTION') if @request[:action].nil?
# make a copy of the params, handy for access and mongoid queries
# consolidate URI parameters
@params = @request[:params].clone unless @request[:params].nil?
@params ||= {}
unless @params.has_key? '_id'
@params['_id'] = @request[:uri_params].first unless @request[:uri_params].first.nil?
end
return not_authorized("INVALID_ACTION") if private_methods.include?(@request[:action])
# Execute the action
response = __send__(@request[:action])
return server_error('CONTROLLER_ERROR') if response.nil?
return response
rescue NotAuthorized => e
trace :error, "[#{@request[:peer]}] Request not authorized: #{e.message}"
return not_authorized(e.message)
rescue BasicAuthRequired => e
trace :error, "[#{@request[:peer]}] Request not authorized: #{e.message}"
return auth_required(e.message)
rescue Exception => e
trace :error, "Server error: #{e.message}"
trace :fatal, "Backtrace : #{e.backtrace}"
return server_error(e.message)
end
end
def cleanup
# hook method if you need to perform some cleanup operation
end
def map_method_to_action(method, no_params)
case method
when 'GET'
return (no_params ? :index : :show)
when 'POST'
return :create
when 'PUT'
return :update
when 'DELETE'
return :destroy
end
end
# macro for auth level check
def require_auth_level(*levels)
if (levels & @session[:level]).empty?
trace :warn, "Trying to access #{@request[:uri]} with #{@session[:level]}"
raise NotAuthorized.new(@session[:level], levels)
end
end
def require_basic_auth
# check if the headers contains the authentication
auth = @request[:headers][:authorization]
# no header
raise BasicAuthRequired.new if auth.nil?
type, encoded = auth.split(' ')
# check for bad requests
raise BasicAuthRequired.new if type.downcase != 'basic'
# parse the auth record
username, password = Base64.decode64(encoded).split(':')
user = User.where(name: username).first
raise BasicAuthRequired.new if user.nil? or not user.enabled
raise BasicAuthRequired.new unless user.has_password?(password)
trace :info, "Auth granted for user #{username} to #{@request[:uri]}"
end
def admin?
return @session[:level].include? :admin
end
def system?
return @session[:level].include? :sys
end
def tech?
return @session[:level].include? :tech
end
def view?
return @session[:level].include? :view
end
def server?
return @session[:level].include? :server
end
def mongoid_query(&block)
begin
start = Time.now
ret = yield
@request[:time][:moingoid] = Time.now - start
return ret
rescue Moped::Errors::ConnectionFailure => e
trace :error, "Connection to database lost, retrying in 5 seconds..."
sleep 5
retry if attempt ||= 0 and attempt += 1 and attempt < 2
rescue Mongoid::Errors::DocumentNotFound => e
trace :error, "Document not found => #{e.message}"
return not_found(e.message)
rescue Mongoid::Errors::InvalidOptions => e
trace :error, "Invalid parameter => #{e.message}"
return bad_request(e.message)
rescue Moped::Errors::InvalidObjectId => e
trace :error, "Bad request #{e.class} => #{e.message}"
return bad_request(e.message)
rescue BlacklistError => e
trace :error, "Blacklist: #{e.message}"
return conflict(e.message)
rescue Exception => e
trace :error, e.message
trace :fatal, "EXCEPTION(#{e.class}): " + e.backtrace.join("\n")
return server_error(e.message)
end
end
end # RESTController
class InvalidController < RESTController
def act!
# default 404 for GET / to simulate a web server (nginx)
if @request[:controller].nil?
trace :error, "[#{@request[:peer]}] Invalid URI requested /#{@request[:action]}. Replied 404."
return not_found(p404, {content_type: 'text/html'})
end
# default reply (403 Forbidden) for invalid controllers
trace :error, "[#{@request[:peer]}] Invalid controller invoked: #{@request[:controller]}/#{@request[:action]}. Replied 403."
return not_authorized(p403)
end
end
# require all the controllers
Dir[File.dirname(__FILE__) + '/rest/*.rb'].each do |file|
require file
end
# register all controllers into RESTController
RCS::DB.constants.keep_if{|x| x.to_s.end_with? 'Controller'}.each do |klass|
RCS::DB::RESTController.register klass
end
end #DB::
end #RCS::