# # Author:: Adam Jacob (<adam@opscode.com>) # Author:: Christopher Brown (<cb@opscode.com>) # Copyright:: Copyright (c) 2008 Opscode, Inc. # License:: Apache License, Version 2.0 # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. # require "chef" / "mixin" / "checksum" require "chef" / "cookbook_loader" require "mixlib/authentication/signatureverification" class ChefServerApi::Application < Merb::Controller include Chef::Mixin::Checksum controller_for_slice # Generate the absolute url for a slice - takes the slice's :path_prefix into account. # # @param slice_name<Symbol> # The name of the slice - in identifier_sym format (underscored). # @param *args<Array[Symbol,Hash]> # There are several possibilities regarding arguments: # - when passing a Hash only, the :default route of the current # slice will be used # - when a Symbol is passed, it's used as the route name # - a Hash with additional params can optionally be passed # # @return <String> A uri based on the requested slice. # # @example absolute_slice_url(:awesome, :format => 'html') # @example absolute_slice_url(:forum, :posts, :format => 'xml') def absolute_slice_url(slice_name, *args) options = {} if args.length == 1 && args[0].respond_to?(:keys) options = args[0] else options = extract_options_from_args!(args) || {} end protocol = options.delete(:protocol) || request.protocol host = options.delete(:host) || request.host protocol + "://" + host + slice_url(slice_name, *args) end def authenticate_every authenticator = Mixlib::Authentication::SignatureVerification.new auth = begin headers = request.env.inject({ }) { |memo, kv| memo[$2.downcase.gsub(/\-/,"_").to_sym] = kv[1] if kv[0] =~ /^(HTTP_)(.*)/; memo } Chef::Log.debug("Headers in authenticate_every: #{headers.inspect}") username = headers[:x_ops_userid].chomp Chef::Log.info("Authenticating client #{username}") user = Chef::ApiClient.cdb_load(username) Chef::Log.debug("Found API Client: #{user.inspect}") user_key = OpenSSL::PKey::RSA.new(user.public_key) Chef::Log.debug "Authenticating:\n #{user.inspect}\n" # Store this for later.. @auth_user = user authenticator.authenticate_user_request(request, user_key) rescue StandardError => se Chef::Log.debug "Authentication failed: #{se}, #{se.backtrace.join("\n")}" nil end raise Unauthorized, "Failed to authenticate!" unless auth auth end def is_admin if @auth_user.admin true else raise Unauthorized, "You are not allowed to take this action." end end def is_correct_node if @auth_user.admin || @auth_user.name == params[:id] true else raise Unauthorized, "You are not the correct node (auth_user name: #{@auth_user.name}, params[:id]: #{params[:id]}), or are not an API administrator (admin: #{@auth_user.admin})." end end # Store the URI of the current request in the session. # # We can return to this location by calling #redirect_back_or_default. def store_location session[:return_to] = request.uri end # Redirect to the URI stored by the most recent store_location call or # to the passed default. def redirect_back_or_default(default) loc = session[:return_to] || default session[:return_to] = nil redirect loc end def access_denied case content_type when :html store_location redirect slice_url(:openid_consumer), :message => { :error => "You don't have access to that, please login."} else raise Unauthorized, "You must authenticate first!" end end # Load a cookbook and return a hash with a list of all the files of a # given segment (attributes, recipes, definitions, libraries) # # === Parameters # cookbook_id<String>:: The cookbook to load # segment<Symbol>:: :attributes, :recipes, :definitions, :libraries # # === Returns # <Hash>:: A hash consisting of the short name of the file in :name, and the full path # to the file in :file. def load_cookbook_segment(cookbook, segment) files_list = segment_files(segment, cookbook) files = Hash.new files_list.each do |f| full = File.expand_path(f) name = File.basename(full) files[name] = { :name => name, :file => full, } end files end def segment_files(segment, cookbook) files_list = nil case segment when :attributes files_list = cookbook.attribute_files when :recipes files_list = cookbook.recipe_files when :definitions files_list = cookbook.definition_files when :libraries files_list = cookbook.lib_files when :providers files_list = cookbook.provider_files when :resources files_list = cookbook.resource_files when :files files_list = cookbook.remote_files when :templates files_list = cookbook.template_files else raise ArgumentError, "segment must be one of :attributes, :recipes, :definitions, :remote_files, :template_files, :resources, :providers or :libraries" end Chef::Log.error(files_list.inspect) files_list end def specific_cookbooks(node_name, cl) valid_cookbooks = Hash.new begin node = Chef::Node.cdb_load(node_name) recipes, default_attrs, override_attrs = node.run_list.expand('couchdb') rescue Net::HTTPServerException recipes = [] end recipes.each do |recipe| valid_cookbooks = expand_cookbook_deps(valid_cookbooks, cl, recipe) end valid_cookbooks end def expand_cookbook_deps(valid_cookbooks, cl, recipe) cookbook = recipe if recipe =~ /^(.+)::/ cookbook = $1 end Chef::Log.debug("Node requires #{cookbook}") valid_cookbooks[cookbook] = true cl.metadata[cookbook.to_sym].dependencies.each do |dep, versions| expand_cookbook_deps(valid_cookbooks, cl, dep) unless valid_cookbooks[dep] end valid_cookbooks end def load_cookbook_files(cookbook) response = { :recipes => Array.new, :definitions => Array.new, :libraries => Array.new, :attributes => Array.new, :files => Array.new, :templates => Array.new, :resources => Array.new, :providers => Array.new } [ :resources, :providers, :recipes, :definitions, :libraries, :attributes, :files, :templates ].each do |segment| segment_files(segment, cookbook).each do |sf| next if File.directory?(sf) file_name = nil file_url = nil file_specificity = nil if segment == :templates || segment == :files mo = sf.match("cookbooks/#{cookbook.name}/#{segment}/(.+?)/(.+)") unless mo Chef::Log.debug("Skipping file #{sf}, as it doesn't have a proper segment.") next end specificity = mo[1] file_name = mo[2] url_options = { :cookbook_id => cookbook.name.to_s, :segment => segment, :id => file_name } case specificity when "default" when /^host-(.+)$/ url_options[:fqdn] = $1 when /^(.+)-(.+)$/ url_options[:platform] = $1 url_options[:version] = $2 when /^(.+)$/ url_options[:platform] = $1 end file_specificity = specificity file_url = absolute_slice_url(:cookbook_segment, url_options) else mo = sf.match("cookbooks/#{cookbook.name}/#{segment}/(.+)") file_name = mo[1] url_options = { :cookbook_id => cookbook.name.to_s, :segment => segment, :id => file_name } file_url = absolute_slice_url(:cookbook_segment, url_options) end rs = { :name => file_name, :uri => file_url, :checksum => checksum(sf) } rs[:specificity] = file_specificity if file_specificity response[segment] << rs end end response end def load_all_files(node_name=nil) cl = Chef::CookbookLoader.new valid_cookbooks = node_name ? specific_cookbooks(node_name, cl) : {} cookbook_list = Hash.new cl.each do |cookbook| if node_name next unless valid_cookbooks[cookbook.name.to_s] end cookbook_list[cookbook.name.to_s] = load_cookbook_files(cookbook) end cookbook_list end def get_available_recipes cl = Chef::CookbookLoader.new available_recipes = cl.sort{ |a,b| a.name.to_s <=> b.name.to_s }.inject([]) do |result, element| element.recipes.sort.each do |r| if r =~ /^(.+)::default$/ result << $1 else result << r end end result end available_recipes end end