lib/jss/api_connection.rb in ruby-jss-1.4.1 vs lib/jss/api_connection.rb in ruby-jss-1.5.1

- old
+ new

@@ -23,22 +23,10 @@ ### ### module JSS - # Constants - ##################################### - - # Module Variables - ##################################### - - # Module Methods - ##################################### - - # Classes - ##################################### - # Instances of this class represent a REST connection to a JSS API. # # For most cases, a single connection to a single JSS is all you need, and # this is ruby-jss's default behavior. # @@ -269,11 +257,11 @@ # is all you'll need. However to access API resources that aren't yet # implemented in other parts of ruby-jss, you can use the methods # {#get_rsrc}, {#put_rsrc}, {#post_rsrc}, & {#delete_rsrc} # documented below. # - # For even lower-level work, you can access the underlying RestClient::Resource + # For even lower-level work, you can access the underlying Faraday::Connection # inside the APIConnection via the connection's {#cnx} attribute. # # APIConnection instances also have a {#server} attribute which contains an # instance of {JSS::Server} q.v., representing the JSS to which it's connected. # @@ -328,18 +316,24 @@ EXTENDABLE_CLASSES = [JSS::Computer, JSS::MobileDevice, JSS::User].freeze # values for the format param of get_rsrc GET_FORMATS = %i[json xml].freeze + HTTP_ACCEPT_HEADER = 'Accept'.freeze + HTTP_CONTENT_TYPE_HEADER = 'Content-Type'.freeze + + MIME_JSON = 'application/json'.freeze + MIME_XML = 'application/xml'.freeze + # Attributes ##################################### # @return [String] the username who's connected to the JSS API attr_reader :user alias jss_user user - # @return [RestClient::Resource] the underlying connection resource + # @return [Faraday::Connection] the underlying connection resource attr_reader :cnx # @return [Boolean] are we connected right now? attr_reader :connected alias connected? connected @@ -357,11 +351,11 @@ attr_reader :port # @return [String] the protocol being used: http or https attr_reader :protocol - # @return [RestClient::Response] The response from the most recent API call + # @return [Faraday::Response] The response from the most recent API call attr_reader :last_http_response # @return [String] The base URL to to the current REST API attr_reader :rest_url @@ -451,12 +445,10 @@ # @option args :port[Integer] the port number to connect with, defaults to 8443 # # @option args :use_ssl[Boolean] should the connection be made over SSL? Defaults to true. # # @option args :verify_cert[Boolean] should HTTPS SSL certificates be verified. Defaults to true. - # If your connection raises RestClient::SSLCertificateNotVerified, and you don't care about the - # validity of the SSL cert. just set this explicitly to false. # # @option args :user[String] a JSS user who has API privs, required if not defined in JSS::CONFIG # # @option args :pw[String,Symbol] Required, the password for that user, or :prompt, or :stdin # If :prompt, the user is promted on the commandline to enter the password for the :user. @@ -492,11 +484,11 @@ # figure out :password from :pw args[:password] = acquire_password args # heres our connection - @cnx = RestClient::Resource.new(@rest_url.to_s, args) + @cnx = create_connection args[:password] verify_server_version @name = "#{@user}@#{@server_host}:#{@port}" if @name.nil? || @name == :disconnected @connected ? hostname : nil @@ -541,12 +533,11 @@ @server_host = nil @cnx = nil @connected = false end # disconnect - # Get an arbitrary JSS resource - # + # Get a JSS resource # The first argument is the resource to get (the part of the API url # after the 'JSSResource/' ) The resource must be properly URL escaped # beforehand. Note: URL.encode is deprecated, use CGI.escape # # By default we get the data in JSON, and parse it into a ruby Hash @@ -570,18 +561,23 @@ # def get_rsrc(rsrc, format = :json, raw_json: false) validate_connected raise JSS::InvalidDataError, 'format must be :json or :xml' unless GET_FORMATS.include? format - begin - @last_http_response = @cnx[rsrc].get(accept: format) - return JSON.parse(@last_http_response.body, symbolize_names: true) if format == :json && !raw_json + @last_http_response = + @cnx.get(rsrc) do |req| + req.headers[HTTP_ACCEPT_HEADER] = format == :json ? MIME_JSON : MIME_XML + end - @last_http_response.body - rescue RestClient::ExceptionWithResponse => e - handle_http_error e + unless @last_http_response.success? + handle_http_error + return end + + return JSON.parse(@last_http_response.body, symbolize_names: true) if format == :json && !raw_json + + @last_http_response.body end # Update an existing JSS resource # # @param rsrc[String] the API resource being changed, the URL part after 'JSSResource/' @@ -595,55 +591,75 @@ # convert CRs & to &#13; xml.gsub!(/\r/, '&#13;') # send the data - @last_http_response = @cnx[rsrc].put(xml, content_type: 'text/xml') + @last_http_response = + @cnx.put(rsrc) do |req| + req.headers[HTTP_CONTENT_TYPE_HEADER] = MIME_XML + req.headers[HTTP_ACCEPT_HEADER] = MIME_XML + req.body = xml + end + unless @last_http_response.success? + handle_http_error + return + end + @last_http_response.body - rescue RestClient::ExceptionWithResponse => e - handle_http_error e end # Create a new JSS resource # # @param rsrc[String] the API resource being created, the URL part after 'JSSResource/' # # @param xml[String] the xml specifying the new object. # # @return [String] the xml response from the server. # - def post_rsrc(rsrc, xml = '') + def post_rsrc(rsrc, xml) validate_connected # convert CRs & to &#13; - xml.gsub!(/\r/, '&#13;') if xml + xml&.gsub!(/\r/, '&#13;') # send the data - @last_http_response = @cnx[rsrc].post(xml, content_type: 'text/xml', accept: :json) + @last_http_response = + @cnx.post(rsrc) do |req| + req.headers[HTTP_CONTENT_TYPE_HEADER] = MIME_XML + req.headers[HTTP_ACCEPT_HEADER] = MIME_XML + req.body = xml + end + unless @last_http_response.success? + handle_http_error + return + end @last_http_response.body - rescue RestClient::ExceptionWithResponse => e - handle_http_error e end # post_rsrc # Delete a resource from the JSS # # @param rsrc[String] the resource to create, the URL part after 'JSSResource/' # # @return [String] the xml response from the server. # - def delete_rsrc(rsrc, xml = nil) + def delete_rsrc(rsrc) validate_connected raise MissingDataError, 'Missing :rsrc' if rsrc.nil? - # payload? - return delete_with_payload rsrc, xml if xml - # delete the resource - @last_http_response = @cnx[rsrc].delete + @last_http_response = + @cnx.delete(rsrc) do |req| + req.headers[HTTP_CONTENT_TYPE_HEADER] = MIME_XML + req.headers[HTTP_ACCEPT_HEADER] = MIME_XML + end + + unless @last_http_response.success? + handle_http_error + return + end + @last_http_response.body - rescue RestClient::ExceptionWithResponse => e - handle_http_error e end # delete_rsrc # Test that a given hostname & port is a JSS API server # # @param server[String] The hostname to test, @@ -655,300 +671,29 @@ def valid_server?(server, port = SSL_PORT) # cheating by shelling out to curl, because getting open-uri, or even net/http to use # ssl_options like :OP_NO_SSLv2 and :OP_NO_SSLv3 will take time to figure out.. return true if `/usr/bin/curl -s 'https://#{server}:#{port}/#{TEST_PATH}'`.include? TEST_CONTENT return true if `/usr/bin/curl -s 'http://#{server}:#{port}/#{TEST_PATH}'`.include? TEST_CONTENT - false - # # try ssl first - # # NOTE: doesn't work if we can't disallow SSLv3 or force TLSv1 - # # See cheat above. - # begin - # return true if open("https://#{server}:#{port}/#{TEST_PATH}", ssl_verify_mode: OpenSSL::SSL::VERIFY_NONE).read.include? TEST_CONTENT - # - # rescue - # # then regular http - # begin - # return true if open("http://#{server}:#{port}/#{TEST_PATH}").read.include? TEST_CONTENT - # rescue - # # any errors = no API - # return false - # end # begin - # end # begin - # # if we're here, no API - # false + false end # The server to which we are connected, or will # try connecting to if none is specified with the # call to #connect # # @return [String] the hostname of the server # def hostname return @server_host if @server_host + srvr = JSS::CONFIG.api_server_name srvr ||= JSS::Client.jss_server srvr end alias host hostname - ################# - - # Call one of the 'all*' methods on a JSS::APIObject subclass - # using this APIConnection. - # - # - # @deprecated please use the .all class method of the desired class - # - # @param class_name[String,Symbol] The name of a JSS::APIObject subclass - # see {JSS.api_object_class} - # - # @param refresh[Boolean] Should the data be re-read from the API? - # - # @param only[String,Symbol] Limit the output to subset or data. All - # APIObject subclasses can take :ids or :names, which calls the .all_ids - # and .all_names methods. Some subclasses can take other options, e.g. - # MobileDevice can take :udids - # - # @return [Array] The list of items for the class - # - def all(class_name, refresh = false, only: nil) - the_class = JSS.api_object_class(class_name) - list_method = only ? :"all_#{only}" : :all - - raise ArgumentError, "Unknown identifier: #{only} for #{the_class}" unless - the_class.respond_to? list_method - - the_class.send list_method, refresh, api: self - end - - # Call the 'map_all_ids_to' method on a JSS::APIObject subclass - # using this APIConnection. - # - # @deprecated please use the .map_all_ids_to class method of the desired class - # - # - # @param class_name[String,Symbol] The name of a JSS::APIObject subclass - # see {JSS.api_object_class} - # - # @param refresh[Boolean] Should the data be re-read from the API? - # - # @param to[String,Symbol] the value to which the ids should be mapped - # - # @return [Hash] The ids for the class keyed to the requested identifier - # - def map_all_ids(class_name, refresh = false, to: nil) - raise "'to:' value must be provided for mapping ids." unless to - the_class = JSS.api_object_class(class_name) - the_class.map_all_ids_to to, refresh, api: self - end - - # Call the 'valid_id' method on a JSS::APIObject subclass - # using this APIConnection. See {JSS::APIObject.valid_id} - # - # @deprecated please use the .valid_id class method of the desired class - # - # - # @param class_name[String,Symbol] The name of a JSS::APIObject subclass, - # see {JSS.api_object_class} - # - # @param identifier[String,Symbol] the value to which the ids should be mapped - # - # @param refresh[Boolean] Should the data be re-read from the API? - # - # @return [Integer, nil] the id of the matching object of the class, - # or nil if there isn't one - # - def valid_id(class_name, identifier, refresh = true) - the_class = JSS.api_object_class(class_name) - the_class.valid_id identifier, refresh, api: self - end - - # Call the 'exist?' method on a JSS::APIObject subclass - # using this APIConnection. See {JSS::APIObject.exist?} - # - # @deprecated please use the .exist class method of the desired class - # - # @param class_name[String,Symbol] The name of a JSS::APIObject subclass - # see {JSS.api_object_class} - # - # @param identifier[String,Symbol] the value to which the ids should be mapped - # - # @param refresh[Boolean] Should the data be re-read from the API? - # - # @return [Boolean] Is there an object of this class in the JSS matching - # this indentifier? - # - def exist?(class_name, identifier, refresh = false) - !valid_id(class_name, identifier, refresh).nil? - end - - # Call {Matchable.match} for the given class. - # - # See {Matchable.match} - # - # @deprecated Please use the .match class method of the desired class - # - # @param class_name[String,Symbol] The name of a JSS::APIObject subclass - # see {JSS.api_object_class} - # - # @return (see Matchable.match) - # - def match(class_name, term) - the_class = JSS.api_object_class(class_name) - raise JSS::UnsupportedError, "Class #{the_class} is not matchable" unless the_class.respond_to? :match - the_class.match term, api: self - end - - # Retrieve an object of a given class from the API - # See {APIObject.fetch} - # - # @deprecated Please use the .fetch class method of the desired class - # - # - # @param class_name[String,Symbol] The name of a JSS::APIObject subclass - # see {JSS.api_object_class} - # - # @return [APIObject] The ruby-instance of the object. - # - def fetch(class_name, arg) - the_class = JSS.api_object_class(class_name) - the_class.fetch arg, api: self - end - - # Make a ruby instance of a not-yet-existing APIObject - # of the given class - # See {APIObject.make} - # - # @deprecated Please use the .make class method of the desired class - # - # @param class_name[String,Symbol] The name of a JSS::APIObject subclass - # see {JSS.api_object_class} - # - # @return [APIObject] The un-created ruby-instance of the object. - # - def make(class_name, **args) - the_class = JSS.api_object_class(class_name) - args[:api] = self - the_class.make args - end - - # Call {JSS::Computer.checkin_settings} q.v., passing this API - # connection - # @deprecated Please use JSS::Computer.checkin_settings - # - def computer_checkin_settings - JSS::Computer.checkin_settings api: self - end - - # Call {JSS::Computer.inventory_collection_settings} q.v., passing this API - # connection - # @deprecated Please use JSS::Computer.inventory_collection_settings - # - def computer_inventory_collection_settings - JSS::Computer.inventory_collection_settings api: self - end - - # Call {JSS::Computer.application_usage} q.v., passing this API - # connection - # @deprecated Please use JSS::Computer.application_usage - # - def computer_application_usage(ident, start_date, end_date = nil) - JSS::Computer.application_usage ident, start_date, end_date, api: self - end - - # Call {JSS::Computer.management_data} q.v., passing this API - # connection - # - # @deprecated Please use JSS::Computer.management_data - # - def computer_management_data(ident, subset: nil, only: nil) - JSS::Computer.management_data ident, subset: subset, only: only, api: self - end - - # Call {JSS::Computer.history} q.v., passing this API - # connection - # - # @deprecated Please use JSS::Computer.management_history or its - # convenience methods. @see JSS::ManagementHistory - # - def computer_history(ident, subset: nil) - JSS::Computer.history ident, subset, api: self - end - - # Call {JSS::Computer.send_mdm_command} q.v., passing this API - # connection - # - # @deprecated Please use JSS::Computer.send_mdm_command or its - # convenience methods. @see JSS::MDM - # - def send_computer_mdm_command(targets, command, passcode = nil) - opts = passcode ? { passcode: passcode } : {} - JSS::Computer.send_mdm_command targets, command, opts: opts, api: self - end - - # Get the DistributionPoint instance for the master - # distribution point in the JSS. If there's only one - # in the JSS, return it even if not marked as master. - # - # @param refresh[Boolean] re-read from the API? - # - # @return [JSS::DistributionPoint] - # - def master_distribution_point(refresh = false) - JSS::DistributionPoint.master_distribution_point refresh, api: self - end - - # Get the DistributionPoint instance for the machine running - # this code, based on its IP address. If none is defined for this IP address, - # use the result of master_distribution_point - # - # @param refresh[Boolean] should the distribution point be re-queried? - # - # @return [JSS::DistributionPoint] - # - def my_distribution_point(refresh = false) - JSS::DistributionPoint.my_distribution_point refresh, api: self - end - - # @deprecated - # - # @see {JSS::NetworkSegment.network_ranges} - # - def network_ranges(refresh = false) - JSS::NetworkSegment.network_ranges refresh, api: self - end # def network_segments - - # @deprecated - # - # @see {JSS::NetworkSegment.network_segments_for_ip} - # - def network_segments_for_ip(ip, refresh = false) - JSS::NetworkSegment.network_segments_for_ip ip, refresh, api: self - end - - # @deprecated - # - # @see {JSS::NetworkSegment.my_network_segments} - # - def my_network_segments - network_segments_for_ip JSS::Client.my_ip_address - end - - # Send an MDM command to one or more mobile devices managed by - # this JSS - # - # see {JSS::MobileDevice.send_mdm_command} - # - # @deprecated Please use JSS::MobileDevice.send_mdm_command or its - # convenience methods. @see JSS::MDM - # - def send_mobiledevice_mdm_command(targets, command, data = {}) - JSS::MobileDevice.send_mdm_command(targets, command, opts: data, api: self) - end - # Empty all cached lists from this connection # then run garbage collection to clear any available memory # # If an APIObject Subclass's RSRC_LIST_KEY is specified, only the caches # for that class are flushed (e.g. :computers, :comptuer_groups) @@ -1050,10 +795,11 @@ # # @return [Hash] The args with defaults applied # def apply_defaults_from_client(args) return unless JSS::Client.installed? + # these settings can come from the jamf binary config, if this machine is a JSS client. args[:server] ||= JSS::Client.jss_server args[:port] ||= JSS::Client.jss_port.to_i args[:use_ssl] ||= JSS::Client.jss_protocol.to_s.end_with? 's' args @@ -1103,15 +849,17 @@ # regardless of their permissions. # However, it's marked as 'deprecated'. Hopefully jamf will # keep this basic level of info available for basic authentication # and JSS version checking. begin - @server = JSS::Server.new get_rsrc('jssuser')[:user], self - rescue RestClient::Unauthorized + data = get_rsrc('jssuser') + rescue JSS::AuthorizationError raise JSS::AuthenticationError, "Incorrect JSS username or password for '#{@user}@#{@server_host}:#{@port}'." end + @server = JSS::Server.new data[:user], self + min_vers = JSS.parse_jss_version(JSS::MINIMUM_SERVER_VERSION)[:version] return if @server.version >= min_vers # we're good... err_msg = "JSS version #{@server.raw_version} to low. Must be >= #{min_vers}" @connected = false @@ -1176,81 +924,72 @@ def verify_ssl(args) # use SSL for SSL ports unless specifically told not to if SSL_PORTS.include? args[:port] args[:use_ssl] = true unless args[:use_ssl] == false end + return unless args[:use_ssl] + # if verify_cert is anything but false, we will verify - args[:verify_ssl] = args[:verify_cert] == false ? OpenSSL::SSL::VERIFY_NONE : OpenSSL::SSL::VERIFY_PEER + args[:verify_ssl] = args[:verify_cert] != false + + # ssl version if not specified + args[:ssl_version] ||= DFT_SSL_VERSION + + @ssl_options = { + verify: args[:verify_ssl], + version: args[:ssl_version] + } end - # Parses the HTTP body of a RestClient::ExceptionWithResponse - # (the parent of all HTTP error responses) and its subclasses - # and re-raises a JSS::APIError with a more - # useful error message. + # Parses the @last_http_response + # and raises a JSS::APIError with a useful error message. # - # @param exception[RestClient::ExceptionWithResponse] the exception to parse - # # @return [void] # - def handle_http_error(exception) - @last_http_response = exception.response - case exception - when RestClient::ResourceNotFound - # other methods catch this and report more details - raise exception - when RestClient::Conflict + def handle_http_error + return if @last_http_response.success? + + case @last_http_response.status + when 404 + err = JSS::NoSuchItemError + msg = 'Not Found' + when 409 err = JSS::ConflictError - msg_matcher = /<p>Error:(.*?)(<|$)/m - when RestClient::BadRequest + @last_http_response.body =~ /<p>(The server has not .*?)(<|$)/m + msg = Regexp.last_match(1) + when 400 err = JSS::BadRequestError - msg_matcher = %r{>Bad Request</p>\n<p>(.*?)</p>\n<p>You can get technical detail}m - when RestClient::Unauthorized - raise + @last_http_response.body =~ %r{>Bad Request</p>\n<p>(.*?)</p>\n<p>You can get technical detail}m + msg = Regexp.last_match(1) + when 401 + err = JSS::AuthorizationError + msg = 'You are not authorized to do that.' + when (500..599) + err = JSS::APIRequestError + msg = 'There was an internal server error' else err = JSS::APIRequestError - msg_matcher = %r{<body.*?>(.*?)</body>}m + msg = "There was a error processing your request, status: #{@last_http_response.status}" end - exception.http_body =~ msg_matcher - msg = Regexp.last_match(1) - msg ||= exception.http_body raise err, msg end - # RestClient::Resource#delete doesn't take an HTTP payload, - # but some JSS API resources require it (notably, logflush). - # - # This method uses RestClient::Request#execute - # to do the same thing that RestClient::Resource#delete does, but - # adding the payload. - # - # @param rsrc[String] the sub-resource we're DELETEing - # - # @param payload[String] The XML to be passed with the DELETE - # - # @param additional_headers[Type] See RestClient::Request#execute - # - # @param &block[Type] See RestClient::Request#execute - # - # @return [String] the XML response from the server. - # - def delete_with_payload(rsrc, payload, additional_headers = {}, &block) - headers = (@cnx.options[:headers] || {}).merge(additional_headers) - @last_http_response = RestClient::Request.execute( - @cnx.options.merge( - method: :delete, - url: @cnx[rsrc].url, - payload: payload, - headers: headers - ), - &(block || @block) - ) - rescue RestClient::ExceptionWithResponse => e - handle_http_error e - end # delete_with_payload + # create the faraday connection object + def create_connection(pw) + Faraday.new(@rest_url, ssl: @ssl_options) do |cnx| + cnx.basic_auth @user, pw + cnx.options[:timeout] = @timeout + cnx.options[:open_timeout] = @open_timeout + cnx.adapter Faraday::Adapter::NetHttp + end + end end # class APIConnection + # JSS MODULE METHODS + ###################### + # Create a new APIConnection object and use it for all # future API calls. If connection options are provided, # they are passed to the connect method immediately, otherwise # JSS.api.connect must be called before attemting to use the # connection. @@ -1274,9 +1013,10 @@ # # @return [APIConnection] The connection now being used. # def self.use_api_connection(connection) raise 'API connections must be instances of JSS::APIConnection' unless connection.is_a? JSS::APIConnection + @api = connection end # Make the default connection (Stored in JSS::API) active #