thisdata 0.1.6 → 0.2.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.
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: