cloudfoundry/cloud_controller_ng

View on GitHub
app/controllers/base/base_controller.rb

Summary

Maintainability
B
5 hrs
Test Coverage
require 'cloud_controller/rest_controller/common_params'
require 'cloud_controller/rest_controller/messages'
require 'cloud_controller/rest_controller/routes'
require 'cloud_controller/security/access_context'
require 'cloud_controller/basic_auth/basic_auth_authenticator'
require 'vcap/json_message'
require 'vcap/rest_api'

module VCAP::CloudController::RestController
  # The base class for all api endpoints.
  class BaseController
    V2_ROUTE_PREFIX ||= '/v2'.freeze

    include VCAP::CloudController
    include CloudController::Errors
    include VCAP::RestAPI
    include Messages
    include Routes
    extend Forwardable

    def_delegators :@sinatra, :redirect, :request

    # Create a new rest api endpoint.
    #
    # @param [Hash] config CC configuration
    #
    # @param [Steno::Logger] logger The logger to use during the request.
    #
    # @param [Hash] env The http environment for the request.
    #
    # @param [Hash] params The http query parms and/or the parameters in a
    # multipart post.
    #
    # @param [IO] body The request body.
    #
    # @param [Sinatra::Base] sinatra The sinatra object associated with the
    # request.
    #
    # We had been trying to keep everything relatively framework
    # agnostic in the base api and everthing build on it, but, the need to call
    # send_file changed that.
    #
    def initialize(config, logger, env, params, body, sinatra=nil, dependencies={})
      @config  = config
      @logger  = logger
      @env     = env
      @params  = params
      @body    = body
      common_params = CommonParams.new(logger)
      query_string = sinatra.request.query_string if sinatra
      @opts = common_params.parse(params, query_string)
      @sinatra = sinatra

      @queryer = VCAP::CloudController::Permissions.new(VCAP::CloudController::SecurityContext.current_user)

      @access_context = Security::AccessContext.new(queryer)

      inject_dependencies(dependencies)
    end

    # Override this to set dependencies
    #
    def inject_dependencies(dependencies={}); end

    # Main entry point for the rest routes.  Acts as the final location
    # for catching any unhandled sequel and db exceptions.  By calling
    # translate_and_log_exception, they will get logged so that we can
    # address them and will get converted to a generic invalid request
    # so that they can be investigated and have more accurate error
    # reporting added.
    #
    # @param [Symbol] operation The method to dispatch to.
    #
    # @param [Array] args The arguments to the method being dispatched to.
    #
    # @return [Object] Returns an array of [http response code, Header hash,
    # body string], or just a body string.
    def dispatch(operation, *args)
      logger.debug 'cc.dispatch', endpoint: operation, args: args
      check_authentication(operation)
      check_arguments_encoding(args)
      send(operation, *args)
    rescue Sequel::ValidationFailed => e
      raise self.class.translate_validation_exception(e, request_attrs)
    rescue Sequel::HookFailed => e
      raise CloudController::Errors::ApiError.new_from_details('InvalidRequest', e.message)
    rescue Sequel::DatabaseError => e
      raise self.class.translate_and_log_exception(logger, e)
    rescue CloudController::Blobstore::BlobstoreError => e
      raise CloudController::Errors::ApiError.new_from_details('BlobstoreError', e.message)
    rescue JsonMessage::Error => e
      logger.debug("Rescued JsonMessage::Error at #{__FILE__}:#{__LINE__}\n#{e.inspect}\n#{e.backtrace.join("\n")}")
      raise CloudController::Errors::ApiError.new_from_details('MessageParseError', e)
    rescue CloudController::Errors::InvalidRelation => e
      raise CloudController::Errors::ApiError.new_from_details('InvalidRelation', e)
    end

    # Fetch the current active user.  May be nil
    #
    # @return [User] User object for the currently active user
    def user
      @access_context.user
    end

    # Fetch the current roles in a Roles object.
    #
    # @return [Roles] Roles object that can be queried for roles
    def roles
      @access_context.roles
    end

    # see Sinatra::Base#send_file
    def send_file(path, opts={})
      @sinatra.send_file(path, opts)
    end

    def set_header(name, value)
      @sinatra.headers[name] = value
    end

    def add_warning(warning)
      escaped_warning = CGI.escape(warning)
      existing_warning = @sinatra.headers['X-Cf-Warnings']

      new_warning = existing_warning.nil? ? escaped_warning : "#{existing_warning},#{escaped_warning}"

      set_header('X-Cf-Warnings', new_warning)
    end

    def check_authentication(operation)
      # The logic here is a bit oddly ordered, but it supports the
      # legacy calls setting a user, but not providing a token.
      return if self.class.allow_unauthenticated_access?(operation)
      return if VCAP::CloudController::SecurityContext.current_user

      if VCAP::CloudController::SecurityContext.missing_token?
        raise CloudController::Errors::NotAuthenticated
      elsif VCAP::CloudController::SecurityContext.invalid_token?
        raise CloudController::Errors::InvalidAuthToken
      else
        logger.error 'Unexpected condition: valid token with no user/client id ' \
                     "or admin scope. Token hash: #{VCAP::CloudController::SecurityContext.token}"
        raise CloudController::Errors::InvalidAuthToken
      end
    end

    def check_arguments_encoding(args)
      args.each do |arg|
        if arg.respond_to?(:valid_encoding?) && !arg.valid_encoding?
          raise CloudController::Errors::ApiError.new_from_details('InvalidRequest',
                                                                   "Invalid encoding for parameter: #{arg}")
        end
      end
    end

    def recursive_delete?
      params['recursive'] == 'true'
    end

    def async?
      params['async'] == 'true'
    end

    def convert_flag_to_bool(flag)
      raise CloudController::Errors::ApiError.new_from_details('InvalidRequest') unless ['true', 'false', nil].include? flag

      flag == 'true'
    end

    # hook called before +create+
    def before_create; end

    # hook called after +create+
    def after_create(obj); end

    # hook called before +update+, +add_related+ or +remove_related+
    def before_update(obj); end

    # hook called after +update+, +add_related+ or +remove_related+
    def after_update(obj); end

    def check_write_permissions!
      return if SecurityContext.roles.admin? || SecurityContext.scopes.include?('cloud_controller.write')

      raise CloudController::Errors::ApiError.new_from_details('NotAuthorized')
    end

    def check_read_permissions!
      return if SecurityContext.roles.admin? ||
                SecurityContext.roles.admin_read_only? ||
                SecurityContext.roles.global_auditor? ||
                SecurityContext.scopes.include?('cloud_controller.read')

      raise CloudController::Errors::ApiError.new_from_details('NotAuthorized')
    end

    def current_user
      SecurityContext.current_user
    end

    def current_user_email
      SecurityContext.current_user_email
    end

    def parse_and_validate_json(body)
      begin
        parsed = Oj.load(body) unless body.nil?
      rescue StandardError => e
        bad_request!(e.message)
      end

      bad_request!('invalid request body') unless parsed.is_a?(Hash)

      parsed
    end

    def bad_request!(message)
      raise CloudController::Errors::ApiError.new_from_details('MessageParseError', message)
    end

    def overwrite_request_attr(key, value)
      @request_attrs = @request_attrs.deep_dup
      @request_attrs[key] = value
      @request_attrs.freeze
    end

    attr_reader :config, :logger, :env, :params, :body, :request_attrs, :queryer

    class << self
      include VCAP::CloudController

      # basename of the class
      #
      # @return [String] basename of the class
      def class_basename
        name.split('::').last
      end

      # path
      #
      # @return [String] The path/route to the collection associated with
      # the class.
      def path
        "#{V2_ROUTE_PREFIX}/#{path_base}"
      end

      # Get and set the base of the path for the api endpoint.
      #
      # @param [String] base path to the api endpoint, e.g. the apps part of
      # /v2/apps/...
      #
      # @return [String] base path to the api endpoint
      def path_base(base=nil)
        @path_base = base if base
        @path_base ||= class_basename.underscore.sub(/_controller$/, '')
      end

      # Get and set the allowed query parameters (sent via the q http
      # query parameter) for this rest/api endpoint.
      #
      # @param [Array] args One or more attributes that can be used
      # as query parameters.
      #
      # @return [Set] If called with no arguments, returns the list
      # of query parameters.
      def query_parameters(*args)
        @query_parameters ||= Set.new
        @query_parameters |= Set.new(args.map(&:to_s)) unless args.empty?
        @query_parameters
      end

      # Query params that will be preserved in next and prev urls while doing enum
      #
      # @param [Array] args One or more param keys that will be preserved in next
      # and prev urls
      #
      # @return [Set] If called with no arguments, returns the list
      # of preserve query parameters.
      def preserve_query_parameters(*args)
        @preserved_query_params ||= Set.new
        @preserved_query_params |= args.map(&:to_s) unless args.empty?
        @preserved_query_params
      end

      def deprecated_endpoint(path, message='Endpoint deprecated')
        controller.after "#{path}*" do
          headers['X-Cf-Warnings'] ||= CGI.escape(message)
        end
      end

      def allow_unauthenticated_access(options={})
        if options[:only]
          @allow_unauthenticated_access_ops = Array(options[:only])
        else
          @allow_unauthenticated_access_to_all_ops = true
        end
      end

      def authenticate_basic_auth(path)
        controller.before path do
          credentials = yield

          raise CloudController::Errors::NotAuthenticated unless CloudController::BasicAuth::BasicAuthAuthenticator.valid?(env, credentials)
        end
      end

      def allow_unauthenticated_access?(operation)
        if @allow_unauthenticated_access_to_all_ops
          @allow_unauthenticated_access_to_all_ops
        elsif @allow_unauthenticated_access_ops
          @allow_unauthenticated_access_ops.include?(operation)
        end
      end

      def translate_and_log_exception(logger, e)
        msg = ["exception not translated: #{e.class} - #{e.message}"]
        msg[0] = msg[0] + ':'
        msg.concat(e.backtrace).join('\\n')
        logger.warn(msg.join('\\n'))
        CloudController::Errors::ApiError.new_from_details('DatabaseError')
      end
    end
  end
end