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 +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:
|