spark_api 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- data/History.txt +139 -0
- data/LICENSE +14 -0
- data/README.md +153 -0
- data/Rakefile +18 -0
- data/VERSION +1 -0
- data/bin/spark_api +8 -0
- data/bin/spark_api~ +8 -0
- data/lib/spark_api.rb +46 -0
- data/lib/spark_api/authentication.rb +55 -0
- data/lib/spark_api/authentication/api_auth.rb +104 -0
- data/lib/spark_api/authentication/api_auth.rb~ +104 -0
- data/lib/spark_api/authentication/base_auth.rb +47 -0
- data/lib/spark_api/authentication/base_auth.rb~ +47 -0
- data/lib/spark_api/authentication/oauth2.rb +198 -0
- data/lib/spark_api/authentication/oauth2.rb~ +199 -0
- data/lib/spark_api/authentication/oauth2_impl/grant_type_base.rb +87 -0
- data/lib/spark_api/authentication/oauth2_impl/grant_type_base.rb~ +87 -0
- data/lib/spark_api/authentication/oauth2_impl/grant_type_code.rb +48 -0
- data/lib/spark_api/authentication/oauth2_impl/grant_type_code.rb~ +49 -0
- data/lib/spark_api/authentication/oauth2_impl/grant_type_password.rb +44 -0
- data/lib/spark_api/authentication/oauth2_impl/grant_type_password.rb~ +45 -0
- data/lib/spark_api/authentication/oauth2_impl/grant_type_refresh.rb +35 -0
- data/lib/spark_api/authentication/oauth2_impl/grant_type_refresh.rb~ +36 -0
- data/lib/spark_api/authentication/oauth2_impl/middleware.rb +38 -0
- data/lib/spark_api/authentication/oauth2_impl/middleware.rb~ +39 -0
- data/lib/spark_api/authentication/oauth2_impl/password_provider.rb +24 -0
- data/lib/spark_api/authentication/oauth2_impl/password_provider.rb~ +25 -0
- data/lib/spark_api/cli.rb +158 -0
- data/lib/spark_api/cli.rb~ +158 -0
- data/lib/spark_api/cli/api_auth.rb +8 -0
- data/lib/spark_api/cli/api_auth.rb~ +8 -0
- data/lib/spark_api/cli/oauth2.rb +14 -0
- data/lib/spark_api/cli/oauth2.rb~ +14 -0
- data/lib/spark_api/cli/setup.rb +47 -0
- data/lib/spark_api/cli/setup.rb~ +47 -0
- data/lib/spark_api/client.rb +27 -0
- data/lib/spark_api/configuration.rb +54 -0
- data/lib/spark_api/configuration.rb~ +54 -0
- data/lib/spark_api/configuration/yaml.rb +101 -0
- data/lib/spark_api/configuration/yaml.rb~ +101 -0
- data/lib/spark_api/connection.rb +42 -0
- data/lib/spark_api/faraday.rb +64 -0
- data/lib/spark_api/faraday.rb~ +64 -0
- data/lib/spark_api/models.rb +33 -0
- data/lib/spark_api/models.rb~ +33 -0
- data/lib/spark_api/models/account.rb +115 -0
- data/lib/spark_api/models/account.rb~ +115 -0
- data/lib/spark_api/models/base.rb +118 -0
- data/lib/spark_api/models/base.rb~ +118 -0
- data/lib/spark_api/models/connect_prefs.rb +10 -0
- data/lib/spark_api/models/connect_prefs.rb~ +10 -0
- data/lib/spark_api/models/constraint.rb +16 -0
- data/lib/spark_api/models/constraint.rb~ +16 -0
- data/lib/spark_api/models/contact.rb +49 -0
- data/lib/spark_api/models/contact.rb~ +49 -0
- data/lib/spark_api/models/custom_fields.rb +12 -0
- data/lib/spark_api/models/custom_fields.rb~ +12 -0
- data/lib/spark_api/models/document.rb +11 -0
- data/lib/spark_api/models/document.rb~ +11 -0
- data/lib/spark_api/models/finders.rb +45 -0
- data/lib/spark_api/models/finders.rb~ +45 -0
- data/lib/spark_api/models/idx_link.rb +47 -0
- data/lib/spark_api/models/idx_link.rb~ +47 -0
- data/lib/spark_api/models/listing.rb +197 -0
- data/lib/spark_api/models/listing.rb~ +197 -0
- data/lib/spark_api/models/listing_cart.rb +72 -0
- data/lib/spark_api/models/listing_cart.rb~ +72 -0
- data/lib/spark_api/models/market_statistics.rb +33 -0
- data/lib/spark_api/models/market_statistics.rb~ +33 -0
- data/lib/spark_api/models/message.rb +21 -0
- data/lib/spark_api/models/message.rb~ +21 -0
- data/lib/spark_api/models/note.rb +41 -0
- data/lib/spark_api/models/note.rb~ +41 -0
- data/lib/spark_api/models/notification.rb +42 -0
- data/lib/spark_api/models/notification.rb~ +42 -0
- data/lib/spark_api/models/open_house.rb +24 -0
- data/lib/spark_api/models/open_house.rb~ +24 -0
- data/lib/spark_api/models/photo.rb +70 -0
- data/lib/spark_api/models/photo.rb~ +70 -0
- data/lib/spark_api/models/property_types.rb +7 -0
- data/lib/spark_api/models/property_types.rb~ +7 -0
- data/lib/spark_api/models/saved_search.rb +16 -0
- data/lib/spark_api/models/saved_search.rb~ +16 -0
- data/lib/spark_api/models/shared_listing.rb +35 -0
- data/lib/spark_api/models/shared_listing.rb~ +35 -0
- data/lib/spark_api/models/standard_fields.rb +50 -0
- data/lib/spark_api/models/standard_fields.rb~ +50 -0
- data/lib/spark_api/models/subresource.rb +19 -0
- data/lib/spark_api/models/subresource.rb~ +19 -0
- data/lib/spark_api/models/system_info.rb +14 -0
- data/lib/spark_api/models/system_info.rb~ +14 -0
- data/lib/spark_api/models/tour_of_home.rb +24 -0
- data/lib/spark_api/models/tour_of_home.rb~ +24 -0
- data/lib/spark_api/models/video.rb +16 -0
- data/lib/spark_api/models/video.rb~ +16 -0
- data/lib/spark_api/models/virtual_tour.rb +18 -0
- data/lib/spark_api/models/virtual_tour.rb~ +18 -0
- data/lib/spark_api/multi_client.rb +59 -0
- data/lib/spark_api/multi_client.rb~ +59 -0
- data/lib/spark_api/paginate.rb +109 -0
- data/lib/spark_api/paginate.rb~ +109 -0
- data/lib/spark_api/primary_array.rb +29 -0
- data/lib/spark_api/primary_array.rb~ +29 -0
- data/lib/spark_api/request.rb +96 -0
- data/lib/spark_api/request.rb~ +96 -0
- data/lib/spark_api/response.rb +70 -0
- data/lib/spark_api/response.rb~ +70 -0
- data/lib/spark_api/version.rb +4 -0
- data/lib/spark_api/version.rb~ +4 -0
- data/script/console +6 -0
- data/script/console~ +6 -0
- data/script/example.rb +27 -0
- data/script/example.rb~ +27 -0
- data/spec/fixtures/accounts/all.json +160 -0
- data/spec/fixtures/accounts/my.json +74 -0
- data/spec/fixtures/accounts/my_portal.json +20 -0
- data/spec/fixtures/accounts/my_put.json +5 -0
- data/spec/fixtures/accounts/my_save.json +5 -0
- data/spec/fixtures/accounts/office.json +142 -0
- data/spec/fixtures/accounts/password_save.json +6 -0
- data/spec/fixtures/authentication_failure.json +7 -0
- data/spec/fixtures/base.json +13 -0
- data/spec/fixtures/contacts/contacts.json +28 -0
- data/spec/fixtures/contacts/my.json +19 -0
- data/spec/fixtures/contacts/new.json +11 -0
- data/spec/fixtures/contacts/new_empty.json +8 -0
- data/spec/fixtures/contacts/new_notify.json +11 -0
- data/spec/fixtures/contacts/post.json +10 -0
- data/spec/fixtures/contacts/tags.json +11 -0
- data/spec/fixtures/count.json +10 -0
- data/spec/fixtures/empty.json +3 -0
- data/spec/fixtures/errors/expired.json +7 -0
- data/spec/fixtures/errors/failure.json +5 -0
- data/spec/fixtures/errors/failure_with_constraint.json +17 -0
- data/spec/fixtures/errors/failure_with_msg.json +7 -0
- data/spec/fixtures/generic_delete.json +1 -0
- data/spec/fixtures/generic_failure.json +5 -0
- data/spec/fixtures/listing_carts/add_listing.json +13 -0
- data/spec/fixtures/listing_carts/add_listing_post.json +5 -0
- data/spec/fixtures/listing_carts/empty.json +5 -0
- data/spec/fixtures/listing_carts/listing_cart.json +19 -0
- data/spec/fixtures/listing_carts/new.json +12 -0
- data/spec/fixtures/listing_carts/post.json +10 -0
- data/spec/fixtures/listing_carts/remove_listing.json +13 -0
- data/spec/fixtures/listings/constraints.json +18 -0
- data/spec/fixtures/listings/constraints_with_pagination.json +24 -0
- data/spec/fixtures/listings/document_index.json +19 -0
- data/spec/fixtures/listings/multiple.json +69 -0
- data/spec/fixtures/listings/no_subresources.json +38 -0
- data/spec/fixtures/listings/open_houses.json +21 -0
- data/spec/fixtures/listings/photos/index.json +469 -0
- data/spec/fixtures/listings/photos/new.json +12 -0
- data/spec/fixtures/listings/photos/post.json +20 -0
- data/spec/fixtures/listings/put.json +5 -0
- data/spec/fixtures/listings/put_expiration_date.json +5 -0
- data/spec/fixtures/listings/saved_search.json +17 -0
- data/spec/fixtures/listings/shared_listing_get.json +14 -0
- data/spec/fixtures/listings/shared_listing_new.json +9 -0
- data/spec/fixtures/listings/shared_listing_post.json +10 -0
- data/spec/fixtures/listings/tour_of_homes.json +23 -0
- data/spec/fixtures/listings/videos_index.json +18 -0
- data/spec/fixtures/listings/virtual_tours_index.json +42 -0
- data/spec/fixtures/listings/with_documents.json +52 -0
- data/spec/fixtures/listings/with_permissions.json +44 -0
- data/spec/fixtures/listings/with_photos.json +110 -0
- data/spec/fixtures/listings/with_supplement.json +39 -0
- data/spec/fixtures/listings/with_videos.json +54 -0
- data/spec/fixtures/listings/with_vtour.json +48 -0
- data/spec/fixtures/logo_fbs.png +0 -0
- data/spec/fixtures/messages/new.json +14 -0
- data/spec/fixtures/messages/new_empty.json +7 -0
- data/spec/fixtures/messages/new_with_recipients.json +15 -0
- data/spec/fixtures/messages/post.json +5 -0
- data/spec/fixtures/notes/add.json +11 -0
- data/spec/fixtures/notes/agent_shared.json +11 -0
- data/spec/fixtures/notes/agent_shared_empty.json +7 -0
- data/spec/fixtures/notes/new.json +5 -0
- data/spec/fixtures/notifications/mark_read.json +1 -0
- data/spec/fixtures/notifications/new.json +8 -0
- data/spec/fixtures/notifications/new_empty.json +7 -0
- data/spec/fixtures/notifications/notifications.json +43 -0
- data/spec/fixtures/notifications/post.json +10 -0
- data/spec/fixtures/notifications/unread.json +10 -0
- data/spec/fixtures/oauth2/access.json +3 -0
- data/spec/fixtures/oauth2/access_with_old_refresh.json +5 -0
- data/spec/fixtures/oauth2/access_with_refresh.json +5 -0
- data/spec/fixtures/oauth2/authorization_code_body.json +7 -0
- data/spec/fixtures/oauth2/error.json +3 -0
- data/spec/fixtures/oauth2/password_body.json +7 -0
- data/spec/fixtures/oauth2/refresh_body.json +7 -0
- data/spec/fixtures/oauth2_error.json +3 -0
- data/spec/fixtures/property_types/property_types.json +31 -0
- data/spec/fixtures/session.json +10 -0
- data/spec/fixtures/standardfields/city.json +1031 -0
- data/spec/fixtures/standardfields/nearby.json +53 -0
- data/spec/fixtures/standardfields/standardfields.json +188 -0
- data/spec/fixtures/standardfields/stateorprovince.json +36 -0
- data/spec/fixtures/success.json +5 -0
- data/spec/json_helper.rb +76 -0
- data/spec/mock_helper.rb +124 -0
- data/spec/oauth2_helper.rb +68 -0
- data/spec/spec_helper.rb +48 -0
- data/spec/unit/flexmls_api_spec.rb~ +23 -0
- data/spec/unit/spark_api/authentication/api_auth_spec.rb +169 -0
- data/spec/unit/spark_api/authentication/api_auth_spec.rb~ +169 -0
- data/spec/unit/spark_api/authentication/base_auth_spec.rb +10 -0
- data/spec/unit/spark_api/authentication/base_auth_spec.rb~ +10 -0
- data/spec/unit/spark_api/authentication/oauth2_impl/grant_type_base_spec.rb +10 -0
- data/spec/unit/spark_api/authentication/oauth2_impl/grant_type_base_spec.rb~ +10 -0
- data/spec/unit/spark_api/authentication/oauth2_spec.rb +205 -0
- data/spec/unit/spark_api/authentication/oauth2_spec.rb~ +205 -0
- data/spec/unit/spark_api/authentication_spec.rb +38 -0
- data/spec/unit/spark_api/authentication_spec.rb~ +38 -0
- data/spec/unit/spark_api/configuration/yaml_spec.rb +72 -0
- data/spec/unit/spark_api/configuration/yaml_spec.rb~ +72 -0
- data/spec/unit/spark_api/configuration_spec.rb +122 -0
- data/spec/unit/spark_api/configuration_spec.rb~ +122 -0
- data/spec/unit/spark_api/faraday_spec.rb +90 -0
- data/spec/unit/spark_api/faraday_spec.rb~ +90 -0
- data/spec/unit/spark_api/models/account_spec.rb +176 -0
- data/spec/unit/spark_api/models/base_spec.rb +106 -0
- data/spec/unit/spark_api/models/connect_prefs_spec.rb +9 -0
- data/spec/unit/spark_api/models/constraint_spec.rb +19 -0
- data/spec/unit/spark_api/models/contact_spec.rb +108 -0
- data/spec/unit/spark_api/models/contact_spec.rb~ +108 -0
- data/spec/unit/spark_api/models/document_spec.rb +32 -0
- data/spec/unit/spark_api/models/listing_cart_spec.rb +127 -0
- data/spec/unit/spark_api/models/listing_cart_spec.rb~ +127 -0
- data/spec/unit/spark_api/models/listing_spec.rb +320 -0
- data/spec/unit/spark_api/models/listing_spec.rb~ +320 -0
- data/spec/unit/spark_api/models/message_spec.rb +47 -0
- data/spec/unit/spark_api/models/message_spec.rb~ +47 -0
- data/spec/unit/spark_api/models/note_spec.rb +63 -0
- data/spec/unit/spark_api/models/note_spec.rb~ +63 -0
- data/spec/unit/spark_api/models/notification_spec.rb +62 -0
- data/spec/unit/spark_api/models/notification_spec.rb~ +62 -0
- data/spec/unit/spark_api/models/open_house_spec.rb +39 -0
- data/spec/unit/spark_api/models/photo_spec.rb +92 -0
- data/spec/unit/spark_api/models/property_types_spec.rb +33 -0
- data/spec/unit/spark_api/models/saved_search_spec.rb +40 -0
- data/spec/unit/spark_api/models/shared_listing_spec.rb +45 -0
- data/spec/unit/spark_api/models/shared_listing_spec.rb~ +45 -0
- data/spec/unit/spark_api/models/standard_fields_spec.rb +60 -0
- data/spec/unit/spark_api/models/system_info_spec.rb +83 -0
- data/spec/unit/spark_api/models/tour_of_home_spec.rb +44 -0
- data/spec/unit/spark_api/models/video_spec.rb +36 -0
- data/spec/unit/spark_api/models/virtual_tour_spec.rb +44 -0
- data/spec/unit/spark_api/multi_client_spec.rb +56 -0
- data/spec/unit/spark_api/multi_client_spec.rb~ +56 -0
- data/spec/unit/spark_api/paginate_spec.rb +224 -0
- data/spec/unit/spark_api/paginate_spec.rb~ +224 -0
- data/spec/unit/spark_api/primary_array_spec.rb +41 -0
- data/spec/unit/spark_api/primary_array_spec.rb~ +41 -0
- data/spec/unit/spark_api/request_spec.rb +344 -0
- data/spec/unit/spark_api/request_spec.rb~ +344 -0
- data/spec/unit/spark_api_spec.rb +23 -0
- metadata +725 -0
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
module SparkApi
|
|
2
|
+
|
|
3
|
+
module Authentication
|
|
4
|
+
|
|
5
|
+
#=API Authentication
|
|
6
|
+
# Auth implementation for the API's original hash based authentication design. This is the
|
|
7
|
+
# default authentication strategy used by the client. API Auth rely's on the user's API key
|
|
8
|
+
# and secret and the active user is tied to the key owner.
|
|
9
|
+
|
|
10
|
+
#==ApiAuth
|
|
11
|
+
# Implementation the BaseAuth interface for API style authentication
|
|
12
|
+
class ApiAuth < BaseAuth
|
|
13
|
+
|
|
14
|
+
def initialize(client)
|
|
15
|
+
super(client)
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def authenticate
|
|
19
|
+
sig = sign("#{@client.api_secret}ApiKey#{@client.api_key}")
|
|
20
|
+
SparkApi.logger.debug("Authenticating to #{@client.endpoint}")
|
|
21
|
+
start_time = Time.now
|
|
22
|
+
request_path = "/#{@client.version}/session?ApiKey=#{@client.api_key}&ApiSig=#{sig}"
|
|
23
|
+
resp = @client.connection(true).post request_path, ""
|
|
24
|
+
request_time = Time.now - start_time
|
|
25
|
+
SparkApi.logger.info("[#{(request_time * 1000).to_i}ms] Api: POST #{request_path}")
|
|
26
|
+
SparkApi.logger.debug("Authentication Response: #{resp.inspect}")
|
|
27
|
+
@session = Session.new(resp.body.results.first)
|
|
28
|
+
SparkApi.logger.debug("Authentication: #{@session.inspect}")
|
|
29
|
+
@session
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def logout
|
|
33
|
+
@client.delete("/session/#{@session.auth_token}") unless @session.nil?
|
|
34
|
+
@session = nil
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
# Builds an ordered list of key value pairs and concatenates it all as one big string. Used
|
|
38
|
+
# specifically for signing a request.
|
|
39
|
+
def build_param_string(param_hash)
|
|
40
|
+
return "" if param_hash.nil?
|
|
41
|
+
sorted = param_hash.sort do |a,b|
|
|
42
|
+
a.to_s <=> b.to_s
|
|
43
|
+
end
|
|
44
|
+
params = ""
|
|
45
|
+
sorted.each do |key,val|
|
|
46
|
+
params += key.to_s + val.to_s
|
|
47
|
+
end
|
|
48
|
+
params
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
# Sign a request
|
|
52
|
+
def sign(sig)
|
|
53
|
+
Digest::MD5.hexdigest(sig)
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
# Sign a request with request data.
|
|
57
|
+
def sign_token(path, params = {}, post_data="")
|
|
58
|
+
token_string = "#{@client.api_secret}ApiKey#{@client.api_key}ServicePath#{path}#{build_param_string(params)}#{post_data}"
|
|
59
|
+
signed = sign(token_string)
|
|
60
|
+
signed
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
# Perform an HTTP request (no data)
|
|
64
|
+
def request(method, path, body, options)
|
|
65
|
+
escaped_path = URI.escape(path)
|
|
66
|
+
request_opts = {
|
|
67
|
+
:AuthToken => @session.auth_token
|
|
68
|
+
}
|
|
69
|
+
unless @client.api_user.nil?
|
|
70
|
+
request_opts.merge!(:ApiUser => "#{@client.api_user}")
|
|
71
|
+
end
|
|
72
|
+
request_opts.merge!(options)
|
|
73
|
+
sig = sign_token(escaped_path, request_opts, body)
|
|
74
|
+
request_path = "#{escaped_path}?#{build_url_parameters({"ApiSig"=>sig}.merge(request_opts))}"
|
|
75
|
+
SparkApi.logger.debug("Request: #{request_path}")
|
|
76
|
+
if body.nil?
|
|
77
|
+
response = @client.connection.send(method, request_path)
|
|
78
|
+
else
|
|
79
|
+
SparkApi.logger.debug("Data: #{body}")
|
|
80
|
+
response = @client.connection.send(method, request_path, body)
|
|
81
|
+
end
|
|
82
|
+
response
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
# ==Session class
|
|
88
|
+
# Handle on the api user session information as return by the api session service, including
|
|
89
|
+
# roles, tokens and expiration
|
|
90
|
+
class Session
|
|
91
|
+
attr_accessor :auth_token, :expires, :roles
|
|
92
|
+
def initialize(options={})
|
|
93
|
+
@auth_token = options["AuthToken"]
|
|
94
|
+
@expires = DateTime.parse options["Expires"]
|
|
95
|
+
@roles = options["Roles"]
|
|
96
|
+
end
|
|
97
|
+
# Is the user session token expired?
|
|
98
|
+
def expired?
|
|
99
|
+
DateTime.now > @expires
|
|
100
|
+
end
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
end
|
|
104
|
+
end
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
module FlexmlsApi
|
|
2
|
+
|
|
3
|
+
module Authentication
|
|
4
|
+
|
|
5
|
+
#=API Authentication
|
|
6
|
+
# Auth implementation for the API's original hash based authentication design. This is the
|
|
7
|
+
# default authentication strategy used by the client. API Auth rely's on the user's API key
|
|
8
|
+
# and secret and the active user is tied to the key owner.
|
|
9
|
+
|
|
10
|
+
#==ApiAuth
|
|
11
|
+
# Implementation the BaseAuth interface for API style authentication
|
|
12
|
+
class ApiAuth < BaseAuth
|
|
13
|
+
|
|
14
|
+
def initialize(client)
|
|
15
|
+
super(client)
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def authenticate
|
|
19
|
+
sig = sign("#{@client.api_secret}ApiKey#{@client.api_key}")
|
|
20
|
+
FlexmlsApi.logger.debug("Authenticating to #{@client.endpoint}")
|
|
21
|
+
start_time = Time.now
|
|
22
|
+
request_path = "/#{@client.version}/session?ApiKey=#{@client.api_key}&ApiSig=#{sig}"
|
|
23
|
+
resp = @client.connection(true).post request_path, ""
|
|
24
|
+
request_time = Time.now - start_time
|
|
25
|
+
FlexmlsApi.logger.info("[#{(request_time * 1000).to_i}ms] Api: POST #{request_path}")
|
|
26
|
+
FlexmlsApi.logger.debug("Authentication Response: #{resp.inspect}")
|
|
27
|
+
@session = Session.new(resp.body.results.first)
|
|
28
|
+
FlexmlsApi.logger.debug("Authentication: #{@session.inspect}")
|
|
29
|
+
@session
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def logout
|
|
33
|
+
@client.delete("/session/#{@session.auth_token}") unless @session.nil?
|
|
34
|
+
@session = nil
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
# Builds an ordered list of key value pairs and concatenates it all as one big string. Used
|
|
38
|
+
# specifically for signing a request.
|
|
39
|
+
def build_param_string(param_hash)
|
|
40
|
+
return "" if param_hash.nil?
|
|
41
|
+
sorted = param_hash.sort do |a,b|
|
|
42
|
+
a.to_s <=> b.to_s
|
|
43
|
+
end
|
|
44
|
+
params = ""
|
|
45
|
+
sorted.each do |key,val|
|
|
46
|
+
params += key.to_s + val.to_s
|
|
47
|
+
end
|
|
48
|
+
params
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
# Sign a request
|
|
52
|
+
def sign(sig)
|
|
53
|
+
Digest::MD5.hexdigest(sig)
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
# Sign a request with request data.
|
|
57
|
+
def sign_token(path, params = {}, post_data="")
|
|
58
|
+
token_string = "#{@client.api_secret}ApiKey#{@client.api_key}ServicePath#{path}#{build_param_string(params)}#{post_data}"
|
|
59
|
+
signed = sign(token_string)
|
|
60
|
+
signed
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
# Perform an HTTP request (no data)
|
|
64
|
+
def request(method, path, body, options)
|
|
65
|
+
escaped_path = URI.escape(path)
|
|
66
|
+
request_opts = {
|
|
67
|
+
:AuthToken => @session.auth_token
|
|
68
|
+
}
|
|
69
|
+
unless @client.api_user.nil?
|
|
70
|
+
request_opts.merge!(:ApiUser => "#{@client.api_user}")
|
|
71
|
+
end
|
|
72
|
+
request_opts.merge!(options)
|
|
73
|
+
sig = sign_token(escaped_path, request_opts, body)
|
|
74
|
+
request_path = "#{escaped_path}?#{build_url_parameters({"ApiSig"=>sig}.merge(request_opts))}"
|
|
75
|
+
FlexmlsApi.logger.debug("Request: #{request_path}")
|
|
76
|
+
if body.nil?
|
|
77
|
+
response = @client.connection.send(method, request_path)
|
|
78
|
+
else
|
|
79
|
+
FlexmlsApi.logger.debug("Data: #{body}")
|
|
80
|
+
response = @client.connection.send(method, request_path, body)
|
|
81
|
+
end
|
|
82
|
+
response
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
# ==Session class
|
|
88
|
+
# Handle on the api user session information as return by the api session service, including
|
|
89
|
+
# roles, tokens and expiration
|
|
90
|
+
class Session
|
|
91
|
+
attr_accessor :auth_token, :expires, :roles
|
|
92
|
+
def initialize(options={})
|
|
93
|
+
@auth_token = options["AuthToken"]
|
|
94
|
+
@expires = DateTime.parse options["Expires"]
|
|
95
|
+
@roles = options["Roles"]
|
|
96
|
+
end
|
|
97
|
+
# Is the user session token expired?
|
|
98
|
+
def expired?
|
|
99
|
+
DateTime.now > @expires
|
|
100
|
+
end
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
end
|
|
104
|
+
end
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
module SparkApi
|
|
2
|
+
|
|
3
|
+
module Authentication
|
|
4
|
+
#=Authentication Base
|
|
5
|
+
# This base class defines the basic interface supported by all client authentication
|
|
6
|
+
# implementations.
|
|
7
|
+
class BaseAuth
|
|
8
|
+
attr_accessor :session
|
|
9
|
+
# All ihheriting classes should accept the spark_api client as a part of initialization
|
|
10
|
+
def initialize(client)
|
|
11
|
+
@client = client
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
# Perform requests to authenticate the client with the API
|
|
15
|
+
def authenticate
|
|
16
|
+
raise "Implement me!"
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
# Called prior to running authenticate (except in case of api authentication errors)
|
|
20
|
+
def authenticated?
|
|
21
|
+
!(session.nil? || session.expired?)
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
# Terminate the active session
|
|
25
|
+
def logout
|
|
26
|
+
raise "Implement me!"
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
# Perform an HTTP request (no data)
|
|
30
|
+
def request(method, path, body, options)
|
|
31
|
+
raise "Implement me!"
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
# Format a hash as request parameters
|
|
35
|
+
#
|
|
36
|
+
# :returns:
|
|
37
|
+
# Stringized form of the parameters as needed for an HTTP request
|
|
38
|
+
def build_url_parameters(parameters={})
|
|
39
|
+
array = parameters.map do |key,value|
|
|
40
|
+
escaped_value = CGI.escape("#{value}")
|
|
41
|
+
"#{key}=#{escaped_value}"
|
|
42
|
+
end
|
|
43
|
+
array.join "&"
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
end
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
module FlexmlsApi
|
|
2
|
+
|
|
3
|
+
module Authentication
|
|
4
|
+
#=Authentication Base
|
|
5
|
+
# This base class defines the basic interface supported by all client authentication
|
|
6
|
+
# implementations.
|
|
7
|
+
class BaseAuth
|
|
8
|
+
attr_accessor :session
|
|
9
|
+
# All ihheriting classes should accept the flexmls_api client as a part of initialization
|
|
10
|
+
def initialize(client)
|
|
11
|
+
@client = client
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
# Perform requests to authenticate the client with the API
|
|
15
|
+
def authenticate
|
|
16
|
+
raise "Implement me!"
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
# Called prior to running authenticate (except in case of api authentication errors)
|
|
20
|
+
def authenticated?
|
|
21
|
+
!(session.nil? || session.expired?)
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
# Terminate the active session
|
|
25
|
+
def logout
|
|
26
|
+
raise "Implement me!"
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
# Perform an HTTP request (no data)
|
|
30
|
+
def request(method, path, body, options)
|
|
31
|
+
raise "Implement me!"
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
# Format a hash as request parameters
|
|
35
|
+
#
|
|
36
|
+
# :returns:
|
|
37
|
+
# Stringized form of the parameters as needed for an HTTP request
|
|
38
|
+
def build_url_parameters(parameters={})
|
|
39
|
+
array = parameters.map do |key,value|
|
|
40
|
+
escaped_value = CGI.escape("#{value}")
|
|
41
|
+
"#{key}=#{escaped_value}"
|
|
42
|
+
end
|
|
43
|
+
array.join "&"
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
end
|
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
require 'uri'
|
|
2
|
+
|
|
3
|
+
module SparkApi
|
|
4
|
+
|
|
5
|
+
module Authentication
|
|
6
|
+
|
|
7
|
+
#=OAuth2 Authentication
|
|
8
|
+
# Auth implementation to the API using the OAuth2 service endpoint. Current adheres to the 10
|
|
9
|
+
# draft of the OAuth2 specification. With OAuth2, the application supplies credentials for the
|
|
10
|
+
# application, and a separate a user authentication flow dictactes the active user for
|
|
11
|
+
# requests.
|
|
12
|
+
#
|
|
13
|
+
#===Setup
|
|
14
|
+
# When using this authentication method, there is a bit more setup involved to make the client
|
|
15
|
+
# work. All applications need to extend the BaseOAuth2Provider class to supply the application
|
|
16
|
+
# specific configuration. Also depending on the application type (command line, native, or web
|
|
17
|
+
# based), the user authentication step will be handled differently.
|
|
18
|
+
|
|
19
|
+
#==OAuth2
|
|
20
|
+
# Implementation the BaseAuth interface for API style authentication
|
|
21
|
+
class OAuth2 < BaseAuth
|
|
22
|
+
|
|
23
|
+
def initialize(client)
|
|
24
|
+
super(client)
|
|
25
|
+
@provider = client.oauth2_provider
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def session
|
|
29
|
+
@provider.load_session()
|
|
30
|
+
end
|
|
31
|
+
def session=(s)
|
|
32
|
+
@provider.save_session(s)
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def authenticate
|
|
36
|
+
granter = OAuth2Impl::GrantTypeBase.create(@client, @provider, session)
|
|
37
|
+
self.session = granter.authenticate
|
|
38
|
+
session
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
# Perform an HTTP request (no data)
|
|
42
|
+
def request(method, path, body, options={})
|
|
43
|
+
escaped_path = URI.escape(path)
|
|
44
|
+
connection = @client.connection(true) # SSL Only!
|
|
45
|
+
connection.headers.merge!(self.auth_header)
|
|
46
|
+
parameter_string = options.size > 0 ? "?#{build_url_parameters(options)}" : ""
|
|
47
|
+
request_path = "#{escaped_path}#{parameter_string}"
|
|
48
|
+
SparkApi.logger.debug("Request: #{request_path}")
|
|
49
|
+
if body.nil?
|
|
50
|
+
response = connection.send(method, request_path)
|
|
51
|
+
else
|
|
52
|
+
SparkApi.logger.debug("Data: #{body}")
|
|
53
|
+
response = connection.send(method, request_path, body)
|
|
54
|
+
end
|
|
55
|
+
response
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def logout
|
|
59
|
+
@provider.save_session(nil)
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def authorization_url()
|
|
63
|
+
params = {
|
|
64
|
+
"client_id" => @provider.client_id,
|
|
65
|
+
"response_type" => "code",
|
|
66
|
+
"redirect_uri" => @provider.redirect_uri
|
|
67
|
+
}
|
|
68
|
+
"#{@provider.authorization_uri}?#{build_url_parameters(params)}"
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
protected
|
|
73
|
+
|
|
74
|
+
def auth_header
|
|
75
|
+
{"Authorization"=> "OAuth #{session.access_token}"}
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def provider
|
|
79
|
+
@provider
|
|
80
|
+
end
|
|
81
|
+
def client
|
|
82
|
+
@client
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
# Representation of a session with the api using oauth2
|
|
88
|
+
class OAuthSession
|
|
89
|
+
SESSION_ATTRIBUTES = [:access_token, :expires_in, :scope, :refresh_token, :refresh_timeout, :start_time]
|
|
90
|
+
attr_accessor *SESSION_ATTRIBUTES
|
|
91
|
+
def initialize(options={})
|
|
92
|
+
@access_token = options["access_token"]
|
|
93
|
+
@expires_in = options["expires_in"]
|
|
94
|
+
@scope = options["scope"]
|
|
95
|
+
@refresh_token = options["refresh_token"]
|
|
96
|
+
@start_time = options.fetch("start_time", DateTime.now)
|
|
97
|
+
@refresh_timeout = options.fetch("refresh_timeout",3600)
|
|
98
|
+
if @start_time.is_a? String
|
|
99
|
+
@start_time = DateTime.parse(@start_time)
|
|
100
|
+
end
|
|
101
|
+
end
|
|
102
|
+
# Is the user session token expired?
|
|
103
|
+
def expired?
|
|
104
|
+
@start_time + Rational(@expires_in - @refresh_timeout, 86400) < DateTime.now
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
def to_json(*a)
|
|
108
|
+
hash = {}
|
|
109
|
+
SESSION_ATTRIBUTES.each do |k|
|
|
110
|
+
value = self.send(k)
|
|
111
|
+
hash[k.to_s] = value unless value.nil?
|
|
112
|
+
end
|
|
113
|
+
hash.to_json(*a)
|
|
114
|
+
end
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
#=OAuth2 configuration provider for applications
|
|
118
|
+
# Applications planning to use OAuth2 authentication with the API must extend this class as
|
|
119
|
+
# part of the client configuration, providing values for the following attributes:
|
|
120
|
+
# @authorization_uri - User oauth2 login page for the Spark platform
|
|
121
|
+
# @access_uri - Location of the OAuth2 access token resource for the api. OAuth2 code and
|
|
122
|
+
# credentials will be sent to this uri to generate an access token.
|
|
123
|
+
# @redirect_uri - Application uri to redirect to
|
|
124
|
+
# @client_id - OAuth2 provided application identifier
|
|
125
|
+
# @client_secret - OAuth2 provided password for the client id
|
|
126
|
+
class BaseOAuth2Provider
|
|
127
|
+
attr_accessor *Configuration::OAUTH2_KEYS
|
|
128
|
+
# Requirements for authorization_code grant type
|
|
129
|
+
attr_accessor :code
|
|
130
|
+
attr_accessor :grant_type
|
|
131
|
+
|
|
132
|
+
def initialize(opts={})
|
|
133
|
+
Configuration::OAUTH2_KEYS.each do |key|
|
|
134
|
+
send("#{key}=", opts[key]) if opts.include? key
|
|
135
|
+
end
|
|
136
|
+
@grant_type = :authorization_code
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
def grant_type
|
|
140
|
+
# backwards compatibility check
|
|
141
|
+
@grant_type.nil? ? :authorization_code : @grant_type
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
# Application using the client must handle user redirect for user authentication. For
|
|
145
|
+
# command line applications, this method is called prior to initial client requests so that
|
|
146
|
+
# the process can notify the user to go to the url and retrieve the access_code for the app.
|
|
147
|
+
# In a web based web application, this method can be mostly ignored. However, the web based
|
|
148
|
+
# application is then responsible for ensuring the code is saved to the the provider instance
|
|
149
|
+
# prior to any client requests are performed (or the error below will be thrown).
|
|
150
|
+
def redirect(url)
|
|
151
|
+
raise "To be implemented by client application"
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
#==For any persistence to be supported outside application process, the application shall
|
|
155
|
+
# implement the following methods for storing and retrieving the user OAuth2 session
|
|
156
|
+
# (e.g. to and from memcached).
|
|
157
|
+
|
|
158
|
+
# Load the current OAuth session
|
|
159
|
+
# returns - active OAuthSession or nil
|
|
160
|
+
def load_session
|
|
161
|
+
nil
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
# Save current session
|
|
165
|
+
# session - active OAuthSession
|
|
166
|
+
def save_session(session)
|
|
167
|
+
|
|
168
|
+
end
|
|
169
|
+
|
|
170
|
+
# Provides a default session time out
|
|
171
|
+
# returns - the session timeout length (in seconds)
|
|
172
|
+
def session_timeout
|
|
173
|
+
86400 # 1.day
|
|
174
|
+
end
|
|
175
|
+
|
|
176
|
+
end
|
|
177
|
+
|
|
178
|
+
module OAuth2Impl
|
|
179
|
+
require 'spark_api/authentication/oauth2_impl/middleware'
|
|
180
|
+
require 'spark_api/authentication/oauth2_impl/grant_type_base'
|
|
181
|
+
require 'spark_api/authentication/oauth2_impl/grant_type_refresh'
|
|
182
|
+
require 'spark_api/authentication/oauth2_impl/grant_type_code'
|
|
183
|
+
require 'spark_api/authentication/oauth2_impl/grant_type_password'
|
|
184
|
+
require 'spark_api/authentication/oauth2_impl/password_provider'
|
|
185
|
+
|
|
186
|
+
# Loads a provider class from a string
|
|
187
|
+
def self.load_provider(string, args={})
|
|
188
|
+
constant = Object
|
|
189
|
+
string.split("::").compact.each { |name| constant = constant.const_get(name) unless name == ""}
|
|
190
|
+
constant.new(args)
|
|
191
|
+
rescue => e
|
|
192
|
+
raise ArgumentError, "The value '#{string}' is an invalid class name for an oauth2 provider: #{e.message}"
|
|
193
|
+
end
|
|
194
|
+
end
|
|
195
|
+
|
|
196
|
+
end
|
|
197
|
+
|
|
198
|
+
end
|