Skip to content

Commit

Permalink
Merge pull request #690 from openstax/gdpr-flag
Browse files Browse the repository at this point in the history
Report GPDR location information for logged in users
  • Loading branch information
BryanHouston authored Aug 26, 2019
2 parents b68a27a + 343d7ab commit 1a9c036
Show file tree
Hide file tree
Showing 16 changed files with 494 additions and 0 deletions.
4 changes: 4 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -110,3 +110,7 @@ set the environment variable `USE_REAL_BACKGROUND_JOBS=true` in your `.env` file
and then start the `delayed_job` daemon:

`bin/rake jobs:work`

## GDPR

For logged-in users, Accounts reports GDPR status in the `/api/user` endpoint via a `is_not_gdpr_location` flag. When this value is `true`, the user is not in a GDPR location. Otherwise (`false` or `nil` or not in the response), the user may be in a GDPR location. To test this functionality in development, you can specify an IP address via the `IP_ADDRESS_FOR_GDPR` environment variable, which will override the normal localhost request IP address.
4 changes: 4 additions & 0 deletions app/controllers/api/v1/users_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,10 @@ def show

OSU::AccessPolicy.require_action_allowed!(:read, current_api_user, current_human_user)

SetGdprData.call(user: current_human_user,
session: session,
ip: ENV['IP_ADDRESS_FOR_GDPR'] || request.ip)

respond_with current_human_user,
represent_with: Api::V1::UserRepresenter,
user_options: { include_private_data: true },
Expand Down
2 changes: 2 additions & 0 deletions app/models/user.rb
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,8 @@ class User < ActiveRecord::Base

attr_readonly :uuid, :support_identifier

attribute :is_not_gdpr_location, :boolean, default: nil

before_validation :generate_uuid, :generate_support_identifier, on: :create

before_create :make_first_user_an_admin
Expand Down
6 changes: 6 additions & 0 deletions app/representers/api/v1/user_representer.rb
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,12 @@ class UserRepresenter < Roar::Decorator
readable: true,
writeable: false

property :is_not_gdpr_location,
if: ->(user_options:, **) { user_options.try(:fetch, :include_private_data, false) },
type: :boolean,
readable: true,
writeable: false

property :is_test?,
as: :is_test,
type: :boolean,
Expand Down
1 change: 1 addition & 0 deletions config/environment.rb
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
require 'subjects_utils'
require 'host'
require 'sso_cookie_jar'
require 'set_gdpr_data'

SITE_NAME = 'OpenStax Accounts'
PAGE_TITLE_SUFFIX = SITE_NAME
Expand Down
6 changes: 6 additions & 0 deletions config/secrets.yml.example
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,9 @@ development:
"SSO_COOKIE_HTTPONLY", true
) %>

# The ip-api.com service gives us location data based on user IP address
ip_api_key: the_ip_api_key

test:
# Used to encrypt and sign cookies
# Changing this will invalidate all cookies
Expand Down Expand Up @@ -147,3 +150,6 @@ test:
domain: ''
secure: true
httponly: true

# The ip-api.com service gives us location data based on user IP address
ip_api_key: the_ip_api_key
132 changes: 132 additions & 0 deletions lib/set_gdpr_data.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
require 'net/http'

module SetGdprData

class GdprSessionData
def initialize(session)
@session = session
end

def ip
@session[:gdpr].try(:[],1..-1)
end

def status
raw_value = @session[:gdpr]
return :unknown if raw_value.blank?
case raw_value[0]
when 'i'
return :inside_gdpr
when 'o'
return :outside_gdpr
else
raise "Session GDPR status is improperly formatted"
end
end

def set(ip:, status:)
case status
when :unknown
@session.delete(:gdpr)
when :inside_gdpr
@session[:gdpr] = "i#{ip}"
when :outside_gdpr
@session[:gdpr] = "o#{ip}"
else
raise "Invalid GDPR status: #{status}"
end
end
end

def self.call(user:, session:, ip:)
gdpr_data = GdprSessionData.new(session)

if ip == gdpr_data.ip
# No need to lookup the location, it is already available in the session
status = gdpr_data.status
else
country_code = country_code(ip: ip)
status =
case country_code
when nil
:unknown
when *GDPR_COUNTRY_CODES
:inside_gdpr
else
:outside_gdpr
end

gdpr_data.set(ip: ip, status: status)
end

user.is_not_gdpr_location = :outside_gdpr == status
end

def self.country_code(ip:)
uri = URI("https://pro.ip-api.com/json/#{ip}?key=#{Rails.application.secrets.ip_api_key}")

begin
Net::HTTP.start(uri.host, uri.port, use_ssl: true, read_timeout: LOOKUP_TIMEOUT) do |http|
response = Net::HTTP.get_response uri
body = JSON.parse(response.body)

if body["status"] == "success"
return body["countryCode"]
else
Raven.capture_message("Failed IP address location lookup", extra: body)
return nil
end
end
rescue Net::ReadTimeout => ee
Raven.capture_message("IP address location lookup timed out")
return nil
rescue => ee
# We don't want explosions here to trickle out and impact callers
Raven.capture_exception(ee)
return nil
end
end

LOOKUP_TIMEOUT = 1

GDPR_COUNTRY_CODES = [
'AT', # Austria
'BE', # Belgium
'BG', # Bulgaria
'HR', # Croatia
'CY', # Cyprus
'CZ', # Czech Republic
'DK', # Denmark
'EE', # Estonia
'FI', # Finland
'FR', # France
'GF', # French Guiana
'DE', # Germany
'GR', # Greece
'GP', # Guadeloupe
'HU', # Hungary
'IS', # Iceland
'IE', # Ireland
'IT', # Italy
'LV', # Latvia
'LI', # Liechtenstein
'LT', # Lithuania
'LU', # Luxembourg
'MT', # Malta
'MQ', # Martinique
'YT', # Mayotte
'NL', # Netherlands
'NO', # Norway
'PL', # Poland
'PT', # Portugal
'RE', # Reunion
'RO', # Romania
'MF', # Saint Martin
'SK', # Slovakia
'SI', # Slovenia
'ES', # Spain
'SE', # Sweden
'GB', # United Kingdom
];

end

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

37 changes: 37 additions & 0 deletions spec/cassettes/SetGdprData/_country_code/succeeds.yml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading

0 comments on commit 1a9c036

Please sign in to comment.