thisdata 0.1.6 → 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: a460e8478eb4bca5802e86c76eba3017d42c5bfa
4
- data.tar.gz: 58af31a7fab25be9d628aa612641da581ca03009
3
+ metadata.gz: c34091bf99a2ccce9303d2c1a3952c0c1b2a990e
4
+ data.tar.gz: abf7211329821a58bcecf4fb21045562ea24f350
5
5
  SHA512:
6
- metadata.gz: 48adde7517313dcb521fbdd3c36386a5391a1b75366261b679c709c8ad5d8df62d4a3607f8908469ce910f4f317f307469b123c72257f591e7424333448b1198
7
- data.tar.gz: 3bf8a38732da9679277ddb7770fe2ec17064985e8f37ce50d51d15a2e7d7e277dd28fa17118ca62c1f86fa6f95aee3a3d0b2a2961b9034c5ada1c281ba898f33
6
+ metadata.gz: 16a1310073c2fff9cd51ca8dc9c1b60b12e389cbd3b9906ef73156d8f74090fe890c06395a8e38de1b8cf6fab455fd12f8c82af7f8e7e4c42acfcddffd036cc6
7
+ data.tar.gz: 3e1b4fa3752eafd370c4f398d56e3232aa1b3aa06e28ae0280331d19f05b7673294aa04c3d5301c38dd146ae14d32d84506a36faa6fdf18aa6081daa511febd6
data/CHANGELOG CHANGED
@@ -1,3 +1,15 @@
1
+ # 0.2.0
2
+
3
+ - Add support for two new API endpoints, POST /verify and GET /events
4
+ https://github.com/thisdata/thisdata-ruby/issues/19
5
+ - help.thisdata.com/docs/apiv1verify
6
+ - help.thisdata.com/docs/v1getevents
7
+ - `ThisData::Event.all` returns a filterable, pageable, list of Events
8
+ - `ThisData.verify(...)` uses ThisData to determine how likely it is that
9
+ the person who is currently logged in has had their account compromised.
10
+ - Includes `thisdata_verify` method in the TrackRequest module, for easier
11
+ use within Rails apps
12
+
1
13
  # 0.1.6
2
14
 
3
15
  - Add support for setting the authenticated value when using `thisdata_track`.
data/README.md CHANGED
@@ -52,20 +52,53 @@ to our app:
52
52
 
53
53
  ```ruby
54
54
  ThisData.track(
55
- {
56
- ip: request.remote_ip,
57
- user_agent: request.user_agent,
58
- verb: ThisData::Verbs::LOG_IN,
59
- user: {
60
- id: user.id.to_s,
61
- name: user.name,
62
- email: user.email,
63
- mobile: user.mobile
64
- }
55
+ ip: request.remote_ip,
56
+ user_agent: request.user_agent,
57
+ verb: ThisData::Verbs::LOG_IN,
58
+ user: {
59
+ id: user.id.to_s,
60
+ name: user.name,
61
+ email: user.email,
62
+ mobile: user.mobile
65
63
  }
66
64
  )
67
65
  ```
68
66
 
67
+ #### Verifying a User
68
+
69
+ ```ruby
70
+ response = ThisData.verify(
71
+ ip: request.remote_ip,
72
+ user_agent: request.user_agent,
73
+ user: {
74
+ id: user.id
75
+ }
76
+ )
77
+
78
+ if response["risk_level"] == ThisData::RISK_LEVEL_GREEN
79
+ # Let them log in
80
+ else
81
+ # Challenge for a Two Factor Authentication code
82
+ end
83
+ ```
84
+
85
+
86
+ #### Getting Events (Audit Log)
87
+
88
+ ```ruby
89
+ events = ThisData::Event.all(
90
+ verb: ThisData::Verbs::LOG_IN,
91
+ user_id: user.id,
92
+ limit: 25,
93
+ offset: 50
94
+ )
95
+
96
+ events.length
97
+ => 25
98
+
99
+ events.first.user.id
100
+ => "112233"
101
+ ```
69
102
 
70
103
  ### Rails
71
104
 
@@ -132,6 +165,41 @@ Note: as with many sensitive operations, taking different actions when an
132
165
  account exists vs. when an account doesn't exist can lead to a information
133
166
  disclosure through timing attacks.
134
167
 
168
+
169
+ #### Verifying
170
+
171
+ Similar to the approach above, there is also a convenience method for verifying
172
+ the current user.
173
+
174
+ ```ruby
175
+ class SessionsController < ApplicationController
176
+ include ThisData::TrackRequest
177
+
178
+ def create
179
+ if User.authenticate(params[:email], params[:password])
180
+
181
+ # They used the right credentials, but does this login look unusual?
182
+ response = thisdata_verify
183
+
184
+ if response["risk_level"] == ThisData::RISK_LEVEL_GREEN
185
+ # The login looks OK. Do the things one usually does for a successful
186
+ # auth
187
+
188
+ # And track it
189
+ thisdata_track
190
+ else
191
+ # There is a chance the account could be breached.
192
+ # Confirm authentication by asking for a Two Factor Authentication code
193
+ # ....
194
+ end
195
+
196
+ else
197
+ # Their credentials are wrong...
198
+ end
199
+ end
200
+ end
201
+ ```
202
+
135
203
  ### Will this break my app?
136
204
 
137
205
  We hope not! We encourage you to use the asynchronous API call where possible
data/Rakefile CHANGED
@@ -1,10 +1,18 @@
1
1
  require "bundler/gem_tasks"
2
2
  require "rake/testtask"
3
3
 
4
- Rake::TestTask.new(:test) do |t|
4
+ Rake::TestTask.new(:test_task) do |t|
5
5
  t.libs << "test"
6
6
  t.libs << "lib"
7
7
  t.test_files = FileList['test/**/*_test.rb']
8
8
  end
9
9
 
10
+ task :test do
11
+ begin
12
+ Rake::Task['test_task'].invoke
13
+ rescue
14
+ # Supress the verbose failure output of TestTask
15
+ end
16
+ end
17
+
10
18
  task :default => :test
data/lib/this_data.rb CHANGED
@@ -2,14 +2,25 @@ require "httparty"
2
2
  require "logger"
3
3
  require "json"
4
4
 
5
+ require "this_data/event"
5
6
  require "this_data/version"
6
7
  require "this_data/verbs"
7
8
  require "this_data/client"
8
9
  require "this_data/configuration"
9
10
  require "this_data/track_request"
11
+ require "util/nested_struct"
10
12
 
11
13
  module ThisData
12
14
 
15
+ # API Endpoint Paths
16
+ EVENTS_ENDPOINT = '/events'
17
+ VERIFY_ENDPOINT = '/verify'
18
+
19
+ # Risk level constants, defined at http://help.thisdata.com/docs/apiv1verify#what-does-the-risk_level-mean
20
+ RISK_LEVEL_GREEN = 'green'
21
+ RISK_LEVEL_ORANGE = 'orange'
22
+ RISK_LEVEL_RED = 'red'
23
+
13
24
  class << self
14
25
 
15
26
  # Configuration Object (instance of ThisData::Configuration)
@@ -27,11 +38,15 @@ module ThisData
27
38
  configuration.defaults
28
39
  end
29
40
 
30
- # Tracks an event. If `ThisData.configuration.async` is true, this action
31
- # will be performed in a new Thread.
32
- # Event must be a Hash
33
- # When performed asynchronously, true is always returned.
34
- # Otherwise an HTTPRequest is returned.
41
+ # Tracks a user initiated event which has occurred within your app, e.g.
42
+ # a user logging in.
43
+ #
44
+ # Performs asynchronously if ThisData.configuration.async is true.
45
+ #
46
+ # Parameters:
47
+ # - event (Required: Hash) a Hash containing details about the event.
48
+ # See http://help.thisdata.com/v1.0/docs/apiv1events for a
49
+ # full & current list of available options.
35
50
  def track(event)
36
51
  if ThisData.configuration.async
37
52
  track_async(event)
@@ -40,6 +55,26 @@ module ThisData
40
55
  end
41
56
  end
42
57
 
58
+ # Verify asks ThisData's API "is this request really from this user?",
59
+ # and returns a response with a risk score.
60
+ #
61
+ # Note: this method does not perform error handling.
62
+ #
63
+ # Parameters:
64
+ # - params (Required: Hash) a Hash containing details about the current
65
+ # request & user.
66
+ # See http://help.thisdata.com/docs/apiv1verify for a
67
+ # full & current list of available options.
68
+ #
69
+ # Returns a Hash
70
+ def verify(params)
71
+ response = Client.new.post(
72
+ ThisData::VERIFY_ENDPOINT,
73
+ body: JSON.generate(params)
74
+ )
75
+ response.parsed_response
76
+ end
77
+
43
78
  # A helper method to track a log-in event. Validates that the minimum
44
79
  # required data is present.
45
80
  def track_login(ip: '', user: {}, user_agent: nil)
@@ -4,7 +4,7 @@ module ThisData
4
4
  class Client
5
5
 
6
6
  USER_AGENT = "ThisData Ruby v#{ThisData::VERSION}"
7
- NO_API_KEY_MESSAGE = "Oops: you've got no ThisData API Key configured, so we can't send events. Specify your ThisData API key using ThisData#setup (find yours at https://thisdata.com)"
7
+ NO_API_KEY_MESSAGE = "Oops: you've got no ThisData API Key configured, so we can't talk to the API. Specify your ThisData API key using ThisData#setup (find yours at https://thisdata.com)"
8
8
 
9
9
  include HTTParty
10
10
 
@@ -15,33 +15,37 @@ module ThisData
15
15
  @headers = {
16
16
  "User-Agent" => USER_AGENT
17
17
  }
18
+ @default_query = {
19
+ api_key: ThisData.configuration.api_key
20
+ }
18
21
  end
19
22
 
20
23
  def require_api_key
21
24
  ThisData.configuration.api_key || print_api_key_warning
22
25
  end
23
26
 
24
- # Tracks a user initiated event which has occurred within your app, e.g.
25
- # a user logging in.
26
- # See http://help.thisdata.com/v1.0/docs/apiv1events for more information.
27
- # - event (Required: Hash) the event, containing the following keys:
28
- # - verb (Required: String) 'what' the user did, e.g. 'log-in'.
29
- # See ThisData::Verbs for predefined options.
30
- # - ip (Required: String) the IP address of the request
31
- # - user_agent (Optional: String) the user agent from the request
32
- # - user (Required: Hash)
33
- # - id (Required: String) a unique identifier for this User
34
- # - email (Optional*: String) the user's email address.
35
- # - mobile (Optional*: String) a mobile phone number in E.164 format
36
- # *email and/or mobile MUST be passed if you want ThisData
37
- # to send 'Was This You?' notifications via email and/or SMS
38
- # - name (Optional: String) the user's name, used in notifications
39
- # - session (Optional: Hash) details about the user's session
40
- # - td_cookie_expected (Optional: Boolean) whether you expect a JS cookie
41
- # to be present
42
- # - td_cookie_id (Optional: String) the value of the JS cookie
27
+ # A convenience method for tracking Events.
28
+ #
29
+ # Parameters:
30
+ # - event (Required: Hash) a Hash containing details about the event.
31
+ # See http://help.thisdata.com/v1.0/docs/apiv1events for a
32
+ # full & current list of available options.
43
33
  def track(event)
44
- post_event(event)
34
+ post(ThisData::EVENTS_ENDPOINT, body: JSON.generate(event))
35
+ end
36
+
37
+ # Perform a GET request against the ThisData API, with the API key
38
+ # prepopulated
39
+ def get(path, query: {})
40
+ query = @default_query.merge(query)
41
+ self.class.get(path, query: query, headers: @headers)
42
+ end
43
+
44
+ # Perform a POST request against the ThisData API, with the API key
45
+ # prepopulated
46
+ def post(path, query: {}, body: {})
47
+ query = @default_query.merge(query)
48
+ self.class.post(path, query: query, headers: @headers, body: body)
45
49
  end
46
50
 
47
51
  private
@@ -50,11 +54,6 @@ module ThisData
50
54
  ThisData.configuration.version
51
55
  end
52
56
 
53
- def post_event(payload_hash)
54
- path_with_key = "/events?api_key=#{ThisData.configuration.api_key}"
55
- self.class.post(path_with_key, headers: @headers, body: JSON.generate(payload_hash))
56
- end
57
-
58
57
  def print_api_key_warning
59
58
  $stderr.puts(NO_API_KEY_MESSAGE)
60
59
  end
@@ -0,0 +1,21 @@
1
+ # A wrapper for the GET /events API
2
+ #
3
+ module ThisData
4
+ class Event
5
+
6
+ # Fetch an array of Events from the ThisData API
7
+ # Available options can be found at
8
+ # http://help.thisdata.com/docs/v1getevents
9
+ #
10
+ # Returns: Array of OpenStruct Event objects
11
+ def self.all(options={})
12
+ response = ThisData::Client.new.get(ThisData::EVENTS_ENDPOINT, query: options)
13
+ # Use NestedStruct to turn this Array of deep Hashes into an array of
14
+ # OpenStructs
15
+ response.parsed_response["results"].collect do |event_hash|
16
+ NestedStruct.new(event_hash)
17
+ end
18
+ end
19
+
20
+ end
21
+ end
@@ -2,12 +2,15 @@
2
2
  # track method which looks at the request and current_user variables to
3
3
  # generate an event.
4
4
  #
5
+ # This module will also provide access to the verify API.
6
+ #
5
7
  # If you include this in a non-ActionController instance, you must respond to
6
8
  # `request` and `ThisData.configuration.user_method`
7
9
  #
8
10
  module ThisData
9
11
  module TrackRequest
10
12
  class ThisDataTrackError < StandardError; end
13
+ class NoUserSpecifiedError < ArgumentError; end
11
14
 
12
15
  # Will pull request and user details from the controller, and send an event
13
16
  # to ThisData.
@@ -61,6 +64,51 @@ module ThisData
61
64
  false
62
65
  end
63
66
 
67
+ # Will pull request and user details from the controller, and use
68
+ # ThisData's verify API to determine how likely it is that the person who
69
+ # is currently logged in has had their account compromised.
70
+ #
71
+ # Arguments:
72
+ # user: (Object, Required). If you want to override the user record
73
+ # that we would usually fetch, you can pass it here.
74
+ # Unless a user is specified here we'll attempt to get the user record
75
+ # as specified in the ThisData gem configuration. This defaults to
76
+ # `current_user`.
77
+ # The object must respond to at least
78
+ # `ThisData.configuration.user_id_method`, which defaults to `id`.
79
+ #
80
+ #
81
+ # Returns a Hash with risk information.
82
+ # See http://help.thisdata.com/docs/apiv1verify for details
83
+ def thisdata_verify(user: nil)
84
+ # Fetch the user unless it's been overridden
85
+ if user.nil?
86
+ user = send(ThisData.configuration.user_method)
87
+ end
88
+ # If it's still nil, raise an error
89
+ raise NoUserSpecifiedError, "A user must be provided for verification" if user.nil?
90
+
91
+ # Get a Hash of details for the user
92
+ user_details = user_details(user)
93
+
94
+ event = {
95
+ ip: request.remote_ip,
96
+ user_agent: request.user_agent,
97
+ user: user_details,
98
+ session: {
99
+ td_cookie_expected: ThisData.configuration.expect_js_cookie,
100
+ td_cookie_id: td_cookie_value,
101
+ }
102
+ }
103
+
104
+ ThisData.verify(event)
105
+ rescue => e
106
+ ThisData.error "Could not verify current user:"
107
+ ThisData.error e
108
+ ThisData.error e.backtrace[0..5].join("\n")
109
+ nil
110
+ end
111
+
64
112
  private
65
113
 
66
114
  # Will return a Hash of details for a user using the methods specified
@@ -1,3 +1,3 @@
1
1
  module ThisData
2
- VERSION = "0.1.6"
2
+ VERSION = "0.2.0"
3
3
  end
@@ -0,0 +1,27 @@
1
+ # Public: turns JSON into a deeply nested OpenStruct. Will find Hashes
2
+ # with Hashes and within Arrays.
3
+ class NestedStruct < OpenStruct
4
+ def initialize(json=Hash.new)
5
+ @table = {}
6
+
7
+ json.each do |key, value|
8
+ @table[key.to_sym] = if value.is_a? Hash
9
+ self.class.new(value)
10
+ elsif value.is_a? Array
11
+ # Turn all the jsons in the array into DeepStructs
12
+ value.collect do |i|
13
+ i.is_a?(Hash) ? self.class.new(i) : i
14
+ end
15
+ else
16
+ value
17
+ end
18
+
19
+ new_ostruct_member(key)
20
+ end
21
+ end
22
+
23
+ def as_json(*args)
24
+ json = super
25
+ json["table"]
26
+ end
27
+ end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: thisdata
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.6
4
+ version: 0.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - ThisData Ltd
@@ -9,7 +9,7 @@ authors:
9
9
  autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
- date: 2016-10-25 00:00:00.000000000 Z
12
+ date: 2016-10-26 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: httparty
@@ -131,9 +131,11 @@ files:
131
131
  - lib/this_data.rb
132
132
  - lib/this_data/client.rb
133
133
  - lib/this_data/configuration.rb
134
+ - lib/this_data/event.rb
134
135
  - lib/this_data/track_request.rb
135
136
  - lib/this_data/verbs.rb
136
137
  - lib/this_data/version.rb
138
+ - lib/util/nested_struct.rb
137
139
  - thisdata.gemspec
138
140
  homepage: https://github.com/thisdata/thisdata-ruby
139
141
  licenses: