diff --git a/Gemfile.lock b/Gemfile.lock new file mode 100644 index 0000000..7bbc2bb --- /dev/null +++ b/Gemfile.lock @@ -0,0 +1,97 @@ +GEM + remote: http://rubygems.org/ + specs: + SystemTimer (1.2.3) + ZenTest (4.6.2) + archive-tar-minitar (0.5.2) + autotest (4.4.6) + ZenTest (>= 4.4.1) + bcrypt-ruby (3.0.1) + builder (3.0.0) + ci_reporter (1.6.5) + builder (>= 2.1.2) + columnize (0.3.4) + daemons (1.1.4) + diff-lcs (1.1.3) + eventmachine (0.12.10) + httpclient (2.2.3) + linecache (0.46) + rbx-require-relative (> 0.0.4) + linecache19 (0.5.12) + ruby_core_source (>= 0.1.4) + machinist (1.0.6) + minitar (0.5.3) + multi_json (1.0.3) + pg (0.11.0) + rack (1.3.5) + rack-protection (1.1.4) + rack + rack-test (0.6.1) + rack (>= 1.0) + rake (0.9.2.2) + rbx-require-relative (0.0.5) + rspec (2.7.0) + rspec-core (~> 2.7.0) + rspec-expectations (~> 2.7.0) + rspec-mocks (~> 2.7.0) + rspec-core (2.7.1) + rspec-expectations (2.7.0) + diff-lcs (~> 1.1.2) + rspec-mocks (2.7.0) + ruby-debug (0.10.4) + columnize (>= 0.1) + ruby-debug-base (~> 0.10.4.0) + ruby-debug-base (0.10.4) + linecache (>= 0.3) + ruby-debug-base19 (0.11.25) + columnize (>= 0.3.1) + linecache19 (>= 0.5.11) + ruby_core_source (>= 0.1.4) + ruby-debug19 (0.11.6) + columnize (>= 0.3.1) + linecache19 (>= 0.5.11) + ruby-debug-base19 (>= 0.11.19) + ruby_core_source (0.1.5) + archive-tar-minitar (>= 0.5.2) + sequel (3.29.0) + simplecov (0.5.4) + multi_json (~> 1.0.3) + simplecov-html (~> 0.5.3) + simplecov-html (0.5.3) + sinatra (1.3.1) + rack (~> 1.3, >= 1.3.4) + rack-protection (~> 1.1, >= 1.1.2) + tilt (~> 1.3, >= 1.3.3) + sqlite3 (1.3.4) + thin (1.3.1) + daemons (>= 1.0.9) + eventmachine (>= 0.12.6) + rack (>= 1.0.0) + tilt (1.3.3) + uuidtools (2.1.2) + yajl-ruby (1.1.0) + +PLATFORMS + ruby + +DEPENDENCIES + SystemTimer + autotest + bcrypt-ruby + ci_reporter + httpclient + machinist + minitar + pg + rack-test + rake + rspec + ruby-debug + ruby-debug19 + sequel + simplecov + sinatra + sqlite3 + thin + uuidtools + yajl-ruby diff --git a/bin/acm b/bin/acm old mode 100644 new mode 100755 index 9dcfa3d..85b94ef --- a/bin/acm +++ b/bin/acm @@ -25,14 +25,14 @@ opts.parse!(ARGV.dup) config_file ||= ::File.expand_path("../../config/acm.yml", __FILE__) config = YAML.load_file(config_file) -CollabSpaces::Config.configure(config) +ACM::Config.configure(config) require "acm_controller" thin_server = Thin::Server.new("0.0.0.0", config["port"], :signals => false) do use Rack::CommonLogger map "/" do - run CollabSpaces::Controller::RackController.new + run ACM::Controller::RackController.new end end diff --git a/lib/acm/api_controller.rb b/lib/acm/api_controller.rb new file mode 100644 index 0000000..c0088fe --- /dev/null +++ b/lib/acm/api_controller.rb @@ -0,0 +1,435 @@ +require 'acm/errors' +require 'acm_controller' +require 'sinatra/base' +require 'json' +require 'net/http' + +module ACM + + module Controller + + class ApiController < Sinatra::Base + + def initialize + super + @logger = Config.logger + @logger.debug("ApiController is up") + end + + use Rack::Auth::Basic, "Restricted Area" do |username, password| + [username, password] == [Config.basic_auth[:user], Config.basic_auth[:password]] + end + + ################### /token_info ################### + + def uri_to(path) + URI::HTTP.build({:host => Config.uaa[:host], + :port => Config.uaa[:port], + :path => "#{Config.uaa[:context]}#{path}" + }) + end + + post '/token_info' do + content_type 'application/json', :charset => 'utf-8' + + token = params[:token] + @logger.debug("token_info call for token #{token}") + + begin + uri = uri_to("/check_token") + @logger.error("URI for this operation is #{uri.inspect}") + http = Net::HTTP.new(uri.host, uri.port) + req = Net::HTTP::Post.new(uri.request_uri) + req.basic_auth(Config.uaa[:user], Config.uaa[:password]) + if(token.include? "Bearer") + stripped_token = token.gsub("Bearer ", "") + req.set_form_data({"token" => stripped_token}) + else + req.set_form_data({"token" => token}) + end + + res = http.request(req) + + @logger.debug("Response is #{res.body.inspect}") + rescue StandardError => e + @logger.error("Failed to fetch the authentication url #{e.inspect}") + raise e + end + + res.body + end + + ################### /token_info ################### + + + ################### authorizer /org/project/authorized ################### + + post '/:org_context/:project_context/authorized' do + content_type 'application/json', :charset => 'utf-8' + + + end + + ################### authorizer /org/project/authorized ################### + + + ################### resource creation /org/project/resource_type ################### + + #TODO: Request response logging + post '/:org_context/:project_context/:resource_type' do + content_type 'application/json', :charset => 'utf-8' + response = nil + + #TODO: Schema/input validation + case params[:resource_type].to_sym + when :org + response = createOrg(params, request) + else + response = createResource(params, request, params[:resource_type]) + end + + @logger.debug("Response is #{response.inspect}") + response + + end + + def createOrg(params, request) + org_context, project_context = get_context(params) + user = get_user(request) + + @logger.debug("Received request from #{user}") + request_json = nil + begin + request_json = Yajl::Parser.new.parse(request.body) + rescue => e + @logger.error("Invalid request #{e.message}") + raise ACM::InvalidRequest.new(e.message) + end + @logger.debug("decoded value is #{request_json.inspect}") + + input_hash = process_org_input(request_json) + + org_service = ACM::Services::OrganizationService.new(:authenticated_user => user, + :org => org_context, + :project => project_context) + org = org_service.create(:name => input_hash[:name], + :description => input_hash[:description], + :authentication_endpoint => input_hash[:authentication_endpoint]) + + org.to_json() + end + + def process_org_input(request) + return_hash = {} + + return_hash[:name] = request[:name.to_s] + return_hash[:description] = request[:description.to_s] + return_hash[:authentication_endpoint] = request[:authenticationEndpoint.to_s] + return_hash[:schema] = "urn:ACM:schemas:1.0" + + return_hash + end + + def createResource(params, request, resource_type) + org_context, project_context = get_context(params) + user = get_user(request) + + @logger.debug("Received request #{request.body.inspect} from #{user}") + request_json = Yajl::Parser.new.parse(request.body) + @logger.debug("decoded value is #{request_json.inspect}") + + organization_service = ACM::Services::OrganizationService.new(:authenticated_user => user, + :org => org_context, + :project => project_context) + + org_entity = organization_service.find_organization() + + project_service = ACM::Services::ProjectService.new(:authenticated_user => user, + :org => org_context, + :project => project_context) + project_entity = project_service.find_project() + + resource_service = ACM::Services::ResourceService.new(:authenticated_user => user, + :org => org_context, + :project => project_context) + + if(!request_json[:resource_metadata.to_s].nil?) + begin + resource_metadata = Hash.try_convert(request_json[:resource_metadata.to_s]) + rescue => e + @logger.error("Failed to parse resource_metadata for \ + resource #{request_json[:name.to_s]} type #{resource_type} \ + metadata is #{request_json[:resource_metadata.to_s].inspect}") + end + end + + @logger.debug("Resource metadata is #{resource_metadata.inspect}") + resource = resource_service.create(resource_type, + request_json[:name.to_s], + request_json[:description.to_s], + resource_metadata.to_json) + + resource.to_json() + end + + ################### resource creation /org/project/resource_type ################### + + + ################### resource deletion /org/project/resource_type ################### + + delete '/:org_context/:project_context/:resource_type' do + content_type 'application/json', :charset => 'utf-8' + response = nil + + #TODO: Schema/input validation + resource_type = params[:resource_type] + case params[:resource_type].to_sym + when :org + response = deleteOrg(params, request) + else + response = deleteResource(params, request, params[:resource_type]) + end + + + @logger.debug("Response is #{response.inspect}") + response + + end + + def deleteOrg(params, request) + org_context, project_context = get_context(params) + user = get_user(request) + + @logger.debug("Received request from #{user}") + request_json = nil + begin + request_json = Yajl::Parser.new.parse(request.body) + rescue => e + @logger.error("Invalid request #{e.message}") + raise ACM::InvalidRequest.new(e.message) + end + @logger.debug("decoded value is #{request_json.inspect}") + + org_name = request[:name.to_s] + + org_service = ACM::Services::OrganizationService.new(:authenticated_user => user, + :org => org_context, + :project => project_context) + org_service.delete(org_name) + + operation_success_info + end + + def operation_success_info + { + :code => 0, + :description => :Success + }.to_json() + end + + def deleteResource(params, request, resource_type) + + end + + ################### resource deletion /org/project/resource_type ################### + + + ################### resource fetch /org/project/resource_type ################### + + get '/:org_context/:project_context/:resource_type/:resource_name' do + content_type 'application/json', :charset => 'utf-8' + response = nil + + #TODO: Schema/input validation + case params[:resource_type].to_sym + when :org + response = getOrg(params, request) + else + response = getResource(params, request, params[:resource_type]) + end + + if(response.nil?) + raise ACM::ResourceNotFound.new("Name: #{params[:resource_name]} Type #{params[:resource_type]}") + end + + @logger.debug("Response is #{response.inspect}") + response + end + + def getOrg(params, request) + org_context, project_context = get_context(params) + user = get_user(request) + org_name = params[:resource_name] + + @logger.debug("Received request from #{user}") + request_json = Yajl::Parser.new.parse(request.body) + @logger.debug("decoded value is #{request_json.inspect}") + + org_service = ACM::Services::OrganizationService.new(:authenticated_user => user, + :org => org_context, + :project => project_context) + org = org_service.find_organization(org_name) + + if(!org.nil?) + org.to_json() + else + raise ACM::ResourceNotFound.new("Name: #{params[:resource_name]} Type #{params[:resource_type]}") + end + + end + + + def getResource(params, request, resource_type) + org_context, project_context = get_context(params) + user = get_user(request) + resource_name = params[:resource_name] + resource_type = params[:resource_type] + + @logger.debug("Received request from #{user}") + request_json = Yajl::Parser.new.parse(request.body) + @logger.debug("decoded value is #{request_json.inspect}") + + resource_service = ACM::Services::ResourceService.new(:authenticated_user => user, + :org => org_context, + :project => project_context) + resource = resource_service.find_resource() + + if(!resource.nil?) + resource.to_json(resource_name, resource_type) + else + nil + end + + end + + def get_context(params) + @logger.debug("Post params #{params}") + return params[:org_context], params[:project_context] + end + + def get_user(request) + @logger.debug("authz headers #{request.env[:AUTHORIZATION.to_s].inspect}") + user_token = request.env[:AUTHORIZATION.to_s] + + if(user_token.nil?) + return nil + end + + @logger.debug("token_info call for token #{user_token}") + + begin + uri = uri_to("/check_token") + @logger.error("URI for this operation is #{uri.inspect}") + http = Net::HTTP.new(uri.host, uri.port) + req = Net::HTTP::Post.new(uri.request_uri) + req.basic_auth(Config.uaa[:user], Config.uaa[:password]) + if(user_token.include? "Bearer") + stripped_token = token.gsub("Bearer ", "") + req.set_form_data({"token" => stripped_token}) + else + req.set_form_data({"token" => token}) + end + + res = http.request(req) + + @logger.debug("Response is #{res.body.inspect}") + rescue StandardError => e + @logger.error("Failed to fetch the authentication url #{e.inspect}") + raise e + end + + if(res.code == "200") + token_as_hash = Yajl::Parser.new.parse(res.body) + + token_as_hash[:user_id.to_s] + else + @logger.debug("Problem with check_token request to the uaa: #{res.inspect}") + raise ACM::SystemInternalError.new() + end + + end + + ################### resource fetch /org/project/resource_type ################### + + ################### resource update /org/project/resource_type ################### + + put '/:org_context/:project_context/:resource_type/:resource_name' do + content_type 'application/json', :charset => 'utf-8' + response = nil + + #TODO: Schema/input validation + case params[:resource_type].to_sym + when :org + response = updateOrg(params, request) + when :project + response = updateProject(params, request) + else + response = updateResource(params, request, params[:resource_type]) + end + + response + end + + def updateOrg(params, request) + #TODO: Not implemented + nil + end + + def updateProject(params, request) + org_context, project_context = get_context(params) + user = get_user(request) + + project_service = ACM::Services::ProjectService + end + + def updateResource(params, request, resource_type) + #TODO: Not implemented + nil + end + + ################### resource update /org/project/resource_type ################### + + configure do + set(:show_exceptions, false) + set(:raise_errors, false) + set(:dump_errors, true) + end + + error do + content_type 'application/json', :charset => 'utf-8' + + @logger.debug("Reached error handler") + exception = request.env["sinatra.error"] + if exception.kind_of?(ACMError) + @logger.debug("Request failed with response code: #{exception.response_code} error code: " + + "#{exception.error_code} error: #{exception.message}") + status(exception.response_code) + error_payload = Hash.new + error_payload['code'] = exception.error_code + error_payload['description'] = exception.message + #TODO: Handle meta and uri. Exception class to contain to_json + Yajl::Encoder.encode(error_payload) + else + msg = ["#{exception.class} - #{exception.message}"] + @logger.warn(msg.join("\n")) + status(500) + end + end + + not_found do + content_type 'application/json', :charset => 'utf-8' + + @logger.debug("Reached not_found handler") + status(404) + error_payload = Hash.new + error_payload['code'] = ACM::ResourceNotFound.new("").error_code + error_payload['description'] = "The resource was not found" + #TODO: Handle meta and uri + Yajl::Encoder.encode(error_payload) + end + + end + + end + +end diff --git a/lib/acm/config.rb b/lib/acm/config.rb index 90df5cf..2161189 100644 --- a/lib/acm/config.rb +++ b/lib/acm/config.rb @@ -2,10 +2,10 @@ require "logger" require "securerandom" -require "collab_spaces/thread_formatter" +require "acm/thread_formatter" -module CollabSpaces +module ACM class Config @@ -17,7 +17,6 @@ class << self :db, :name, :revision, - :uaa, :basic_auth ] @@ -58,18 +57,10 @@ def configure(config) @db.sql_log_level = :debug Sequel::Model.plugin :validation_helpers - create_default_org_and_project() - @lock = Monitor.new puts "Configuration complete" - @logger.debug("Collab Spaces running") - - #@show_exceptions = config["sinatra"]["show_exceptions"] - #@raise_errors = config["sinatra"]["raise_errors"] - #@dump_errors = config["sinatra"]["dump_errors"] - - @uaa = { :host => "localhost", :port => 8080, :context => "/cloudfoundry-identity-uaa", :user => "app", :password => "appclientsecret" } + @logger.debug("ACM running") @basic_auth = { :user => config["basic_auth"]["user"], :password => config["basic_auth"]["password"]} @@ -89,7 +80,7 @@ def connect(server) opts[:database] = ':memory:' if blank_object?(opts[:database]) db = ::SQLite3::Database.new(opts[:database]) db.busy_handler do |retries| - CollabSpaces::Config.logger.debug "SQLITE BUSY, retry ##{retries}" + ACM::Config.logger.debug "SQLITE BUSY, retry ##{retries}" sleep(0.1) retries < 20 end @@ -106,43 +97,6 @@ class << db end end - def create_default_org_and_project() - - @logger.debug("Is default org available?") - ds = @db[:resources] - all_org = ds.filter(:name => :all.to_s, :type => :organization.to_s).all() - if(all_org.nil? || all_org.size() == 0) - @logger.debug("Creating default org") - @db[:resources].insert(:id => -1, - :name => "all", - :owner_id => -1, - :type => "organization", - :description => "Root cloudfoundry org", - :immutable_id => SecureRandom.uuid, - :created_at => Time.now, - :last_updated_at => Time.now) - @db[:resources].insert(:id => -2, - :name => "all", - :owner_id => -1, - :type => "project", - :description => "Project for the root cloudfoundry org", - :immutable_id => SecureRandom.uuid, - :created_at => Time.now, - :last_updated_at => Time.now) - @db[:resources].insert(:id => -3, - :name => "organization", - :owner_id => -1, - :type => "resource_type", - :description => "Organization resource type for the root cloudfoundry org", - :immutable_id => SecureRandom.uuid, - :created_at => Time.now, - :last_updated_at => Time.now) - else - @logger.debug("Yes") - end - - end - end end end diff --git a/lib/acm/errors.rb b/lib/acm/errors.rb new file mode 100644 index 0000000..c90cd35 --- /dev/null +++ b/lib/acm/errors.rb @@ -0,0 +1,46 @@ + +module CollabSpaces + + OK = 200 + CREATED = 201 + NO_CONTENT = 204 + + BAD_REQUEST = 400 + UNAUTHORIZED = 401 + FORBIDDEN = 403 + NOT_FOUND = 404 + + INTERNAL_SERVER_ERROR = 500 + + class CollabSpacesError < StandardError + attr_reader :response_code + attr_reader :error_code + + def initialize(response_code, error_code, format, *args) + @response_code = response_code + @error_code = error_code + msg = sprintf(format, *args) + super(msg) + end + end + + [ + ["ResourceNotFound", NOT_FOUND, 1000, "Resource %s not found"], + ["InvalidRequest", BAD_REQUEST, 1001, "Invalid request: \"%s\""], + ["Unauthorized", UNAUTHORIZED, 1002, "Unauthorized"], + + ["SystemInternalError", INTERNAL_SERVER_ERROR, 2000, "An unknown error occurred" ] + + ].each do |e| + class_name, response_code, error_code, format = e + + klass = Class.new CollabSpacesError do + define_method :initialize do |*args| + super(response_code, error_code, format, *args) + end + end + + CollabSpaces.const_set(class_name, klass) + end + +end diff --git a/lib/acm/thread_formatter.rb b/lib/acm/thread_formatter.rb new file mode 100644 index 0000000..9cb62fa --- /dev/null +++ b/lib/acm/thread_formatter.rb @@ -0,0 +1,49 @@ +class ThreadFormatter + FORMAT = "%s, [%s#%d] [%s] %5s -- %s: %s\n" + + attr_accessor :datetime_format + + def initialize + @datetime_format = nil + end + + def call(severity, time, progname, msg) + thread_name = Thread.current[:name] || "0x#{Thread.current.object_id.to_s(16)}" + FORMAT % [severity[0..0], format_datetime(time), $$, thread_name, severity, progname, + msg2str(msg)] + end + + private + + def format_datetime(time) + if @datetime_format.nil? + time.strftime("%Y-%m-%dT%H:%M:%S.") << "%06d " % time.usec + else + time.strftime(@datetime_format) + end + end + + def msg2str(msg) + case msg + when ::String + msg + when ::Exception + "#{ msg.message } (#{ msg.class })\n" << + (msg.backtrace || []).join("\n") + else + msg.inspect + end + end +end + +module Kernel + + def with_thread_name(name) + old_name = Thread.current[:name] + Thread.current[:name] = name + yield + ensure + Thread.current[:name] = old_name + end + +end \ No newline at end of file diff --git a/lib/acm_controller.rb b/lib/acm_controller.rb index e69de29..bfa3a20 100644 --- a/lib/acm_controller.rb +++ b/lib/acm_controller.rb @@ -0,0 +1,56 @@ +module ACM; module Controller; end; end + +require "acm/config" +require "acm/api_controller" + +require "sequel" +require "yajl" + +module ACM + + module Controller + + class RackController + PUBLIC_URLS = ["/info"] + + def initialize + super + @logger = Config.logger + end + + def call(env) + + @logger.debug("Call with parameters #{env.inspect}") + + api_controller = ApiController.new + + @logger.debug("Created ApiController") + + #if perform_auth?(env) + # app = Rack::Auth::Basic.new(api_controller) do |user, password| + # api_controller.authenticate(user, password) + # end + # + # app.realm = "Collab Spaces" + #else + app = api_controller + #end + + status, headers, body = app.call(env) + headers["Date"] = Time.now.rfc822 # As thin doesn't inject date + + [ status, headers, body ] + end + + + #TODO: Need to modify this for the UAA + def perform_auth?(env) + auth_needed = !PUBLIC_URLS.include?(env["PATH_INFO"]) + auth_provided = %w(HTTP_AUTHORIZATION X-HTTP_AUTHORIZATION X_HTTP_AUTHORIZATION).detect{ |key| env.has_key?(key) } + auth_needed || auth_provided + end + end + + end + +end