From cbf48145cd56020c055b9ebdc47ebb806f93319c Mon Sep 17 00:00:00 2001 From: amartynov Date: Fri, 12 Dec 2014 17:00:06 +0300 Subject: [PATCH] user module --- devops-service/devops-service.rb | 99 ++++++- devops-service/helpers/request.rb | 47 ++++ devops-service/helpers/version_2.rb | 131 +++++++++ devops-service/routes/v2.0/base_routes.rb | 115 -------- devops-service/routes/v2.0/provider.rb | 54 ++-- devops-service/routes/v2.0/user.rb | 309 +++++++++++----------- 6 files changed, 461 insertions(+), 294 deletions(-) create mode 100644 devops-service/helpers/request.rb create mode 100644 devops-service/helpers/version_2.rb diff --git a/devops-service/devops-service.rb b/devops-service/devops-service.rb index 7176086..d1396f5 100644 --- a/devops-service/devops-service.rb +++ b/devops-service/devops-service.rb @@ -13,13 +13,22 @@ require "db/mongo/mongo_connector" require "providers/provider_factory" require "routes/v2.0" +require "helpers/version_2" + +require "routes/v2.0/provider" +require "routes/v2.0/user" class DevopsService < Sinatra::Base helpers Sinatra::Streaming + helpers Sinatra::Version2_0::Helpers + + register Sinatra::Version2_0::Core::ProviderRoutes + register Sinatra::Version2_0::Core::UserRoutes def initialize config super() + self.class.set :config, config @@config = config root = File.dirname(__FILE__) @@config[:keys_dir] = File.join(root, "../.devops_files/keys") @@ -29,9 +38,10 @@ class DevopsService < Sinatra::Base end [:keys_dir, :scripts_dir].each {|key| d = @@config[key]; FileUtils.mkdir_p(d) unless File.exists?(d) } mongo = DevopsService.mongo + self.class.set :mongo, mongo mongo.create_root_user ::Provider::ProviderFactory.init(config) - set_up_providers_keys!(::Provider::ProviderFactory.all, mongo) + #set_up_providers_keys!(::Provider::ProviderFactory.all, mongo) end @@mongo @@ -54,7 +64,92 @@ class DevopsService < Sinatra::Base end end - use ::Version2_0::V2_0 + include Sinatra::JSON + + configure :production do + disable :dump_errors + disable :show_exceptions + set :logging, Logger::INFO + end + + configure :development do + set :logging, Logger::DEBUG + disable :raise_errors +# disable :dump_errors + set :show_exceptions, :after_handler + end + + not_found do + "Not found" + end + + error RecordNotFound do + e = env["sinatra.error"] + logger.warn e.message + halt_response(e.message, 404) + end + + error InvalidRecord do + e = env["sinatra.error"] + logger.warn e.message + logger.warn "Request body: #{request.body.read}" + halt_response(e.message, 400) + end + + error InvalidCommand do + e = env["sinatra.error"] + logger.warn e.message + halt_response(e.message, 400) + end + + error DependencyError do + e = env["sinatra.error"] + logger.warn e.message + halt_response(e.message, 400) + end + + error InvalidPrivileges do + e = env["sinatra.error"] + logger.warn e.message + halt_response(e.message, 401) + end + + error Excon::Errors::Unauthorized do + e = env["sinatra.error"] + resp = e.response + ct = resp.headers["Content-Type"] + msg = unless ct.nil? + if ct.include?("application/json") + json = ::Chef::JSONCompat.from_json(resp.body) + m = "ERROR: Unauthorized (#{json['error']['code']}): #{json['error']['message']}" + logger.error(m) + else + end + m + else + "Unauthorized: #{e.inspect}" + end + halt_response(msg, 500) + end + + error Fog::Compute::AWS::Error do + e = env["sinatra.error"] + logger.error e.message + halt_response(e.message, 500) + end + + error do + e = env["sinatra.error"] + logger.error e.message + halt_response(e.message, 500) + end + +# def self.mongo +# DevopsService.mongo +# end + + +# use ::Version2_0::V2_0 private diff --git a/devops-service/helpers/request.rb b/devops-service/helpers/request.rb new file mode 100644 index 0000000..fa967b8 --- /dev/null +++ b/devops-service/helpers/request.rb @@ -0,0 +1,47 @@ +require 'sinatra/base' +require "sinatra/json" + +require "providers/provider_factory" + +module Sinatra + module Version2_0 + module Request + module Helpers + + # Check request headers + def check_headers *headers + ha = (headers.empty? ? [:accept, :content_type] : headers) + ha.each do |h| + case h + when :accept, "accept" + accept_json + when :content_type, "content_type" + request_json + end + end + end + + # Check Accept header + # + # Can client works with JSON? + def accept_json + logger.debug(request.accept) + unless request.accept? 'application/json' + response.headers['Accept'] = 'application/json' + halt_response("Accept header should contains 'application/json' type", 406) + end + rescue NoMethodError => e + #error in sinatra 1.4.4 (https://github.com/sinatra/sinatra/issues/844, https://github.com/sinatra/sinatra/pull/805) + response.headers['Accept'] = 'application/json' + halt_response("Accept header should contains 'application/json' type", 406) + end + + # Check Content-Type header + def request_json + halt_response("Content-Type should be 'application/json'", 415) if request.media_type.nil? or request.media_type != 'application/json' + end + + end + end + end +end diff --git a/devops-service/helpers/version_2.rb b/devops-service/helpers/version_2.rb new file mode 100644 index 0000000..0559b13 --- /dev/null +++ b/devops-service/helpers/version_2.rb @@ -0,0 +1,131 @@ +require "json" +require 'sinatra/base' +require "sinatra/json" + + +require "providers/provider_factory" + +module Sinatra + module Version2_0 + module Helpers + + def create_response msg, obj=nil, rstatus=200 + logger.info(msg) + status rstatus + obj = {} if obj.nil? + obj[:message] = msg + json(obj) + end + + def halt_response msg, rstatus=400 + obj = {:message => msg} + halt(rstatus, json(obj)) + end + + def check_privileges cmd, p + #BaseRoutes.mongo.check_user_privileges(request.env['REMOTE_USER'], cmd, p) + true + end + + # Check request headers + def check_headers *headers + ha = (headers.empty? ? [:accept, :content_type] : headers) + ha.each do |h| + case h + when :accept, "accept" + accept_json + when :content_type, "content_type" + request_json + end + end + end + + # Check Accept header + # + # Can client works with JSON? + def accept_json + logger.debug(request.accept) + unless request.accept? 'application/json' + response.headers['Accept'] = 'application/json' + halt_response("Accept header should contains 'application/json' type", 406) + end + rescue NoMethodError => e + #error in sinatra 1.4.4 (https://github.com/sinatra/sinatra/issues/844, https://github.com/sinatra/sinatra/pull/805) + response.headers['Accept'] = 'application/json' + halt_response("Accept header should contains 'application/json' type", 406) + end + + # Check Content-Type header + def request_json + halt_response("Content-Type should be 'application/json'", 415) if request.media_type.nil? or request.media_type != 'application/json' + end + + def check_provider provider + list = ::Provider::ProviderFactory.providers + halt_response("Invalid provider '#{provider}', available providers: '#{list.join("', '")}'", 404) unless list.include?(provider) + end + + def create_object_from_json_body type=Hash, empty_body=false + json = request.body.read.strip + return nil if json.empty? and empty_body + @body_json = begin + ::JSON.parse(json) + rescue ::JSON::ParserError => e + logger.error e.message + halt_response("Invalid JSON: #{e.message}") + end + halt_response("Invalid JSON, it should be an #{type == Array ? "array" : "object"}") unless @body_json.is_a?(type) + @body_json + end + + def check_string val, msg, _nil=false, empty=false + check_param val, String, msg, _nil, empty + end + + def check_array val, msg, vals_type=String, _nil=false, empty=false + check_param val, Array, msg, _nil, empty + val.each {|v| halt_response(msg) unless v.is_a?(vals_type)} unless val.nil? + val + end + + def check_filename file_name, not_string_msg, json_resp=true + check_string file_name, not_string_msg + r = Regexp.new("^[\\w _\\-.]{1,255}$", Regexp::IGNORECASE) + if r.match(file_name).nil? + msg = "Invalid file name '#{file_name}'. Expected name with 'a'-'z', '0'-'9', ' ', '_', '-', '.' symbols with length greate then 0 and less then 256 " + if json_resp + halt_response(msg) + else + halt(400, msg) + end + end + file_name + end + + def check_param val, type, msg, _nil=false, empty=false + if val.nil? + if _nil + return val + else + halt_response(msg) + end + end + if val.is_a?(type) + halt_response(msg) if val.empty? and !empty + val + else + halt_response(msg) + end + end + + # Save information about requests with methods POST, PUT, DELETE + def statistic msg=nil + unless request.get? + settings.mongo.statistic request.env['REMOTE_USER'], request.path, request.request_method, @body_json, response.status + end + end + + end + end +end + diff --git a/devops-service/routes/v2.0/base_routes.rb b/devops-service/routes/v2.0/base_routes.rb index ebb74d1..18f15fa 100644 --- a/devops-service/routes/v2.0/base_routes.rb +++ b/devops-service/routes/v2.0/base_routes.rb @@ -31,38 +31,6 @@ module Version2_0 BaseRoutes.mongo.check_user_privileges(request.env['REMOTE_USER'], cmd, p) end - # Check request headers - def check_headers *headers - ha = (headers.empty? ? [:accept, :content_type] : headers) - ha.each do |h| - case h - when :accept, "accept" - accept_json - when :content_type, "content_type" - request_json - end - end - end - - # Check Accept header - # - # Can client works with JSON? - def accept_json - logger.debug(request.accept) - unless request.accept? 'application/json' - response.headers['Accept'] = 'application/json' - halt_response("Accept header should contains 'application/json' type", 406) - end - rescue NoMethodError => e - #error in sinatra 1.4.4 (https://github.com/sinatra/sinatra/issues/844, https://github.com/sinatra/sinatra/pull/805) - response.headers['Accept'] = 'application/json' - halt_response("Accept header should contains 'application/json' type", 406) - end - - # Check Content-Type header - def request_json - halt_response("Content-Type should be 'application/json'", 415) if request.media_type.nil? or request.media_type != 'application/json' - end def check_provider provider list = ::Provider::ProviderFactory.providers @@ -132,89 +100,6 @@ module Version2_0 end - include Sinatra::JSON - - configure :production do - disable :dump_errors - disable :show_exceptions - set :logging, Logger::INFO - end - - configure :development do - set :logging, Logger::DEBUG - disable :raise_errors -# disable :dump_errors - set :show_exceptions, :after_handler - end - - not_found do - "Not found" - end - - error RecordNotFound do - e = env["sinatra.error"] - logger.warn e.message - halt_response(e.message, 404) - end - - error InvalidRecord do - e = env["sinatra.error"] - logger.warn e.message - logger.warn "Request body: #{request.body.read}" - halt_response(e.message, 400) - end - - error InvalidCommand do - e = env["sinatra.error"] - logger.warn e.message - halt_response(e.message, 400) - end - - error DependencyError do - e = env["sinatra.error"] - logger.warn e.message - halt_response(e.message, 400) - end - - error InvalidPrivileges do - e = env["sinatra.error"] - logger.warn e.message - halt_response(e.message, 401) - end - - error Excon::Errors::Unauthorized do - e = env["sinatra.error"] - resp = e.response - ct = resp.headers["Content-Type"] - msg = unless ct.nil? - if ct.include?("application/json") - json = ::Chef::JSONCompat.from_json(resp.body) - m = "ERROR: Unauthorized (#{json['error']['code']}): #{json['error']['message']}" - logger.error(m) - else - end - m - else - "Unauthorized: #{e.inspect}" - end - halt_response(msg, 500) - end - - error Fog::Compute::AWS::Error do - e = env["sinatra.error"] - logger.error e.message - halt_response(e.message, 500) - end - - error do - e = env["sinatra.error"] - logger.error e.message - halt_response(e.message, 500) - end - - def self.mongo - DevopsService.mongo - end end end diff --git a/devops-service/routes/v2.0/provider.rb b/devops-service/routes/v2.0/provider.rb index de340f0..344a195 100644 --- a/devops-service/routes/v2.0/provider.rb +++ b/devops-service/routes/v2.0/provider.rb @@ -1,33 +1,39 @@ # encoding: UTF-8 + +require 'sinatra/base' + require "json" require "routes/v2.0/base_routes" require "providers/provider_factory" -module Version2_0 - class ProviderRoutes < BaseRoutes +module Sinatra + module Version2_0 + module Core + module ProviderRoutes - def initialize wrapper - super wrapper - puts "Provider routes initialized" + def self.registered(app) + puts "Provider routes initialized" + + # Get devops providers + # + # * *Request* + # - method : GET + # - headers : + # - Accept: application/json + # + # * *Returns* : + # [ + # "ec2", + # "openstack" + # ] + app.get "/providers" do + check_headers :accept + check_privileges("provider", "r") + json ::Provider::ProviderFactory.providers + end + end + end end - - # Get devops providers - # - # * *Request* - # - method : GET - # - headers : - # - Accept: application/json - # - # * *Returns* : - # [ - # "ec2", - # "openstack" - # ] - get "/providers" do - check_headers :accept - check_privileges("provider", "r") - json ::Provider::ProviderFactory.providers - end - end + #register Version2_0::Core::ProviderRoutes end diff --git a/devops-service/routes/v2.0/user.rb b/devops-service/routes/v2.0/user.rb index 9a3d82b..1756743 100644 --- a/devops-service/routes/v2.0/user.rb +++ b/devops-service/routes/v2.0/user.rb @@ -1,167 +1,170 @@ -require "json" require "db/exceptions/invalid_record" require "db/mongo/models/user" -module Version2_0 - class UserRoutes < BaseRoutes +module Sinatra + module Version2_0 + module Core + module UserRoutes - def initialize wrapper - super wrapper - puts "User routes initialized" - end + def self.registered(app) + puts "User routes initialized" - after %r{\A/user(/[\w]+(/password)?)?\z} do - statistic - end + app.after %r{\A/user(/[\w]+(/password)?)?\z} do + statistic + end - # Get users list - # - # * *Request* - # - method : GET - # - headers : - # - Accept: application/json - # - # * *Returns* : - # [ - # { - # "email": "test@test.test", - # "privileges": { - # "flavor": "r", - # "group": "r", - # "image": "r", - # "project": "r", - # "server": "r", - # "key": "r", - # "user": "", - # "filter": "r", - # "network": "r", - # "provider": "r", - # "script": "r", - # "templates": "r" - # }, - # "id": "test" - # } - # ] - get "/users" do - check_headers :accept - check_privileges("user", "r") - users = BaseRoutes.mongo.users.map {|i| i.to_hash} - users.each {|u| u.delete("password")} - json users - end + # Get users list + # + # * *Request* + # - method : GET + # - headers : + # - Accept: application/json + # + # * *Returns* : + # [ + # { + # "email": "test@test.test", + # "privileges": { + # "flavor": "r", + # "group": "r", + # "image": "r", + # "project": "r", + # "server": "r", + # "key": "r", + # "user": "", + # "filter": "r", + # "network": "r", + # "provider": "r", + # "script": "r", + # "templates": "r" + # }, + # "id": "test" + # } + # ] + app.get "/users" do + check_headers :accept + check_privileges("user", "r") + users = settings.mongo.users.map {|i| i.to_hash} + users.each {|u| u.delete("password")} + json users + end - # Create user - # - # * *Request* - # - method : POST - # - headers : - # - Accept: application/json - # - Content-Type: application/json - # - body : - # { - # "username": "user name", - # "email": "user email", - # "password": "user password" - # } - # - # * *Returns* : - # 201 - Created - post "/user" do - check_headers :accept, :content_type - check_privileges("user", "w") - user = create_object_from_json_body - ["username", "password", "email"].each do |p| - check_string(user[p], "Parameter '#{p}' must be a not empty string") - end - BaseRoutes.mongo.user_insert User.new(user) - create_response("Created", nil, 201) - end + # Create user + # + # * *Request* + # - method : POST + # - headers : + # - Accept: application/json + # - Content-Type: application/json + # - body : + # { + # "username": "user name", + # "email": "user email", + # "password": "user password" + # } + # + # * *Returns* : + # 201 - Created + app.post "/user" do + check_headers :accept, :content_type + check_privileges("user", "w") + user = create_object_from_json_body + ["username", "password", "email"].each do |p| + check_string(user[p], "Parameter '#{p}' must be a not empty string") + end + #BaseRoutes.mongo.user_insert User.new(user) + settings.mongo.user_insert User.new(user) + create_response("Created", nil, 201) + end - # Delete user - # - # * *Request* - # - method : DELETE - # - headers : - # - Accept: application/json - # - # * *Returns* : - # 200 - Deleted - delete "/user/:user" do - check_headers :accept - check_privileges("user", "w") - projects = BaseRoutes.mongo.projects_by_user params[:user] - if !projects.empty? - str = "" - projects.each do |p| - p.deploy_envs.each do |e| - str+="#{p.id}.#{e.identifier} " if e.users.include? params[:user] + # Delete user + # + # * *Request* + # - method : DELETE + # - headers : + # - Accept: application/json + # + # * *Returns* : + # 200 - Deleted + app.delete "/user/:user" do + check_headers :accept + check_privileges("user", "w") + projects = settings.mongo.projects_by_user params[:user] + if !projects.empty? + str = "" + projects.each do |p| + p.deploy_envs.each do |e| + str+="#{p.id}.#{e.identifier} " if e.users.include? params[:user] + end + end + logger.info projects + raise DependencyError.new "Deleting is forbidden: User is included in #{str}" + #return [400, "Deleting is forbidden: User is included in #{str}"] + end + + r = settings.mongo.user_delete params[:user] + create_response("User '#{params[:user]}' removed") + end + + # Change user privileges + # + # * *Request* + # - method : PUT + # - headers : + # - Accept: application/json + # - Content-Type: application/json + # - body : + # { + # "cmd": "command or all", -> if empty, set default privileges + # "privileges": "priv" -> 'rwx' or '' + # } + # + # * *Returns* : + # 200 - Updated + app.put "/user/:user" do + check_headers :accept, :content_type + check_privileges("user", "w") + data = create_object_from_json_body + user = settings.mongo.user params[:user] + cmd = check_string(data["cmd"], "Parameter 'cmd' should be a not empty string", true) || "" + privileges = check_string(data["privileges"], "Parameter 'privileges' should be a not empty string", true) || "" + user.grant(cmd, privileges) + settings.mongo.user_update user + create_response("Updated") + end + + # Change user email/password + # + # * *Request* + # - method : PUT + # - headers : + # - Accept: application/json + # - Content-Type: application/json + # - body : + # { + # "email/password": "new user email/password", + # } + # + # * *Returns* : + # 200 - Updated + app.put %r{\A/user/[\w]+/(email|password)\z} do + check_headers :accept, :content_type + action = File.basename(request.path) + u = File.basename(File.dirname(request.path)) + raise InvalidPrivileges.new("Access denied for '#{request.env['REMOTE_USER']}'") if u == User::ROOT_USER_NAME and request.env['REMOTE_USER'] != User::ROOT_USER_NAME + + check_privileges("user", "w") unless request.env['REMOTE_USER'] == u + + body = create_object_from_json_body + p = check_string(body[action], "Parameter '#{action}' must be a not empty string") + user = settings.mongo.user u + user.send("#{action}=", p) + settings.mongo.user_update user + create_response("Updated") end end - logger.info projects - raise DependencyError.new "Deleting is forbidden: User is included in #{str}" - #return [400, "Deleting is forbidden: User is included in #{str}"] + end - - r = BaseRoutes.mongo.user_delete params[:user] - create_response("User '#{params[:user]}' removed") end - - # Change user privileges - # - # * *Request* - # - method : PUT - # - headers : - # - Accept: application/json - # - Content-Type: application/json - # - body : - # { - # "cmd": "command or all", -> if empty, set default privileges - # "privileges": "priv" -> 'rwx' or '' - # } - # - # * *Returns* : - # 200 - Updated - put "/user/:user" do - check_headers :accept, :content_type - check_privileges("user", "w") - data = create_object_from_json_body - user = BaseRoutes.mongo.user params[:user] - cmd = check_string(data["cmd"], "Parameter 'cmd' should be a not empty string", true) || "" - privileges = check_string(data["privileges"], "Parameter 'privileges' should be a not empty string", true) || "" - user.grant(cmd, privileges) - BaseRoutes.mongo.user_update user - create_response("Updated") - end - - # Change user email/password - # - # * *Request* - # - method : PUT - # - headers : - # - Accept: application/json - # - Content-Type: application/json - # - body : - # { - # "email/password": "new user email/password", - # } - # - # * *Returns* : - # 200 - Updated - put %r{\A/user/[\w]+/(email|password)\z} do - check_headers :accept, :content_type - action = File.basename(request.path) - u = File.basename(File.dirname(request.path)) - raise InvalidPrivileges.new("Access denied for '#{request.env['REMOTE_USER']}'") if u == User::ROOT_USER_NAME and request.env['REMOTE_USER'] != User::ROOT_USER_NAME - - check_privileges("user", "w") unless request.env['REMOTE_USER'] == u - - body = create_object_from_json_body - p = check_string(body[action], "Parameter '#{action}' must be a not empty string") - user = BaseRoutes.mongo.user u - user.send("#{action}=", p) - BaseRoutes.mongo.user_update user - create_response("Updated") - end - end end