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 +4 -4
- data/CHANGELOG +12 -0
- data/README.md +78 -10
- data/Rakefile +9 -1
- data/lib/this_data.rb +40 -5
- data/lib/this_data/client.rb +25 -26
- data/lib/this_data/event.rb +21 -0
- data/lib/this_data/track_request.rb +48 -0
- data/lib/this_data/version.rb +1 -1
- data/lib/util/nested_struct.rb +27 -0
- metadata +4 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: c34091bf99a2ccce9303d2c1a3952c0c1b2a990e
|
4
|
+
data.tar.gz: abf7211329821a58bcecf4fb21045562ea24f350
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
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
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
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(:
|
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
|
31
|
-
#
|
32
|
-
#
|
33
|
-
#
|
34
|
-
#
|
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)
|
data/lib/this_data/client.rb
CHANGED
@@ -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
|
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
|
-
#
|
25
|
-
#
|
26
|
-
#
|
27
|
-
# - event (Required: Hash)
|
28
|
-
#
|
29
|
-
#
|
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
|
-
|
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
|
data/lib/this_data/version.rb
CHANGED
@@ -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.
|
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-
|
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:
|