talis 0.12.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (46) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +12 -0
  3. data/.rspec +2 -0
  4. data/.rubocop.yml +2 -0
  5. data/.travis.yml +24 -0
  6. data/.yardopts +1 -0
  7. data/CONTRIBUTING.md +28 -0
  8. data/Gemfile +4 -0
  9. data/Guardfile +5 -0
  10. data/README.md +76 -0
  11. data/Rakefile +8 -0
  12. data/bin/console +14 -0
  13. data/bin/setup +8 -0
  14. data/lib/talis.rb +25 -0
  15. data/lib/talis/analytics.rb +31 -0
  16. data/lib/talis/analytics/event.rb +67 -0
  17. data/lib/talis/authentication.rb +14 -0
  18. data/lib/talis/authentication/client.rb +82 -0
  19. data/lib/talis/authentication/login.rb +169 -0
  20. data/lib/talis/authentication/public_key.rb +53 -0
  21. data/lib/talis/authentication/token.rb +172 -0
  22. data/lib/talis/bibliography.rb +52 -0
  23. data/lib/talis/bibliography/ebook.rb +50 -0
  24. data/lib/talis/bibliography/manifestation.rb +141 -0
  25. data/lib/talis/bibliography/result_set.rb +34 -0
  26. data/lib/talis/bibliography/work.rb +164 -0
  27. data/lib/talis/constants.rb +9 -0
  28. data/lib/talis/errors.rb +10 -0
  29. data/lib/talis/errors/authentication_failed_error.rb +4 -0
  30. data/lib/talis/errors/client_errors.rb +19 -0
  31. data/lib/talis/errors/server_communication_error.rb +4 -0
  32. data/lib/talis/errors/server_error.rb +4 -0
  33. data/lib/talis/extensions/object.rb +11 -0
  34. data/lib/talis/feeds.rb +8 -0
  35. data/lib/talis/feeds/annotation.rb +129 -0
  36. data/lib/talis/feeds/feed.rb +58 -0
  37. data/lib/talis/hierarchy.rb +9 -0
  38. data/lib/talis/hierarchy/asset.rb +265 -0
  39. data/lib/talis/hierarchy/node.rb +200 -0
  40. data/lib/talis/hierarchy/resource.rb +159 -0
  41. data/lib/talis/oauth_service.rb +18 -0
  42. data/lib/talis/resource.rb +68 -0
  43. data/lib/talis/user.rb +112 -0
  44. data/lib/talis/version.rb +3 -0
  45. data/talis.gemspec +39 -0
  46. metadata +327 -0
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: e8926e08a42d050d4b27917867150635361762ac
4
+ data.tar.gz: 46819d589a371999e95e6a39a8747162244751ae
5
+ SHA512:
6
+ metadata.gz: 437793795a8103843222cef1ca78ac7b70381315e4a179059b98edf80bf4706e9e7ab30778a7c38c42c3cb1fe76e4461f16c82c44d5c976661438e0596785b11
7
+ data.tar.gz: 016d3734dbd71a3b643c4041e65edd683a7c3a40d0b4ef3e5826c737695f3f9e01577e02bd63d1b654a30046cd820f53c7203d13557930bf08ffdc8631ca8b6b
data/.gitignore ADDED
@@ -0,0 +1,12 @@
1
+ /.bundle/
2
+ /.yardoc
3
+ /Gemfile.lock
4
+ /_yardoc/
5
+ /coverage/
6
+ /doc/
7
+ /pkg/
8
+ /spec/reports/
9
+ /tmp/
10
+ .env
11
+ .idea/
12
+ *.gem
data/.rspec ADDED
@@ -0,0 +1,2 @@
1
+ --format documentation
2
+ --color
data/.rubocop.yml ADDED
@@ -0,0 +1,2 @@
1
+ AllCops:
2
+ TargetRubyVersion: 2.2
data/.travis.yml ADDED
@@ -0,0 +1,24 @@
1
+ language: ruby
2
+ rvm:
3
+ - 2.2.5
4
+ - 2.3.1
5
+ before_install: gem install bundler -v 1.11.2
6
+ notifications:
7
+ hipchat:
8
+ on_pull_requests: false
9
+ rooms:
10
+ secure: KPz9FpSupvzP2ts8BOeAp0rZe+E5VyqbfzPX5Ho46+ctj41tppO2ufYFg2eBdKpBtA1d3KqgAGZaXWtH03rq7lp9NdgTjIzt2Y1qpv3lKTKLs3BPm4JUEBLJZ9Udzatp3W+sDlF3Or8cLgsgvZ4Jx9tehI4TlI4bQn3u7Iyy38Ssgzdw13U5p7SgFnOMU3i+/L6Ki1uzB5wVi0Nvf/5f8/MVRjqXLO0HyLxUzM1g7RtWxqd3B4SzpQS1jndfXuXD0bndlOSQxODz1D6umCk0H0tGtESzMQeMZi0EiKTTTlYyJhvw9U9xZrIdz+Ecd+Sgf3N5dIzKzi6xNZm/B+YONrhvMOClgyUFk0kzOScGASlGxIlgz6uMEc9L1kGB8Y72ezEPkr5RtCjC7+MCnnJN+J2ChWiqDnTRkjshStpKRbQxRx6f3Rj7iItJcfTx1iKOxGw53xoh5OaqPVkqvbVmEMsnObg9kVFpHeTKDXzbqEuOgPmNC/9y1b32DASCU3VIISr9nN1parEthgkYuVE4s50FU2IGHBIu+1ch7J2uKg8wkYGiLxAvZlKxRl/zUCjD/XndfTWa73mY0ykN0y18TJEHgvUrFcQfveetNmMLcYF1cqXhbcCRRG2b92+2ZD9wJf2RfmSys+p33Jvu7FGtnPs1ol+a/JWg/7WVGcgoEXU=
11
+ template:
12
+ - "%{repository}#%{build_number} (%{branch} - %{commit} : %{author}): %{message}"
13
+ notify: true
14
+ env:
15
+ global:
16
+ - PERSONA_TEST_HOST: https://staging-users.talis.com
17
+ - BLUEPRINT_TEST_HOST: https://staging-hierarchy.talis.com
18
+ - METATRON_OAUTH_HOST: https://users.talis.com
19
+ - ECHO_TEST_HOST: https://staging-analytics.talis.com
20
+ - BABEL_TEST_HOST: https://staging-feeds.talis.com
21
+ - secure: BE2l30U/CLy50vFGV33JCu8t5sr3S0tjCVHcyvby9ChLj7SbceiL7oFhdInCfUFP5uOn9JoyAeULq0JSJK+j5tPsWVGXW0j1hJGvOjcDPA9gsvA4IpgH0WNs6eX5GZkYJIMioJB1MrRP8Hq6teUIC8fb51DQOkL8IRydD1qJsLOgebKfJHLB1hEXHUO0O4Mn3lZrJNjthZrNJG4bhBLvTnki4dmBu+vzEVBb8QMxpnNbBVUI8P3Uo4WlygOnirnW6naxBizHK7EAV6Hk221eTyarepe9u0CpWPXVG5XiHtedUDUU6SdT9XW0lgk26jC78qjf4+VbKIq7Xnmet9bO0K25A8A9ggwEbYv60KPhSeVbTlFt8a1cvthxmfhcgTBdX1JuZ+QbTt6Ex1Pq5efDM1XTKM9wDa1fEz2/+JTIAD6t6nOP8I45M7bRY/WyBXD1TNEU3XWDuJHRf9RlHgfALbc+GD4PHa6/QvWv6gmAAfeGMWbTnl/bDIk6zghfLcM9NhiZErz9V+AX7uQPlKSzqsz/WtjCthz4nFltNVisKftFLZieOR0QOJNDo10KPAbMnRk5R/5znlMd/zyH7C2oGlMCAjVK5GETJnRyw26/y0S5PWwxovf3p+IjXeNrRiHmsiSiVUK/e9EPiSya5n/GPRR9x9VoYEmo9fS88g8+Buo=
22
+ - secure: ndyBRplFd1IKVlabpPm/YxLKcD18MPflgtIjmhsEHzEQdvBlviwL7zXjpcISlrSLD5y7s4VMookrSXSWMMve4pFonjKUEOXS8yH8ARKe40i2PcdKuiGvD5DWKZXJvNfmOKoWFgG0uK/v8PJcniW/NyLjAJ915MAEvI0fBUjq4B6ZZYHd3oEIpDctDxN3jF/xK9058LCgT68IDAun16F+UnqdNdWj6I4BDM2ljhIZpsCYjVQAgPvYE85AYdJpPJUlO28MMj+iJzS2ZlubDngligtu4OWyDzTua8aez3V9OobPLNEfoCIW+a+9IiBM8QrUfomIUlccQ9j2B2u5i7n6w6KqfKi3PZd3jtkoG+nScWHnCCovTi8w9Dw5umiQrAr4y80XgQclWhi3HpTuzb/jKinec6Q7kXpdwIem+LEhX0Gp54azuJna51nw1Pf0sym3Ic096/SLgvxgtMi3JDUFu/VCHrHYnCAkjy42c7SKoMWzQ69dyTTK3+X32xasP8FT+F7CSEKbPS0D1zhpR9xpNDFmQkQklccBa9nY9CzFPe7YsvtaP7fW739lH8NfrD8faD7/BPoVIgX8/7ZKRKj1mXGDukvg4pizrd+U4O6iDkqQUfPkQE6+PLvdb2FZS+WjeR7xtXReH3dAk8D2sqZVoGl6nbDAKcxJUABKozPNl6s=
23
+ - secure: kfPQchN+t2fsbOPuxTYfpAwamguynuebLHwq/qD7dglz/aBhF8kPtBOQxtaMBfth1FQ+Xx0d73G9nW1TkIsCj1fYVZkNC2JhRojIUiKaKQoTj0anw04YOvvnX6AUnyx6+Sov8M5rf6EAprKKLh2lPXxXmrDtGxXVACA/EDyQNDYNeIy+TVJ1kzI8KQ//lbnc9EhISPgnYLe/HETi1v7abKm18pmIw5fATOszou2a/ziH1TW1Io8iX3Z1wUQ60WaXgL7+y/pEs6JHy3UXl3zNSYW5EeeloRmob71HlZ01xAQSLyqW8UUFnnCuxln23TmQXOEULMjy35kino+3wMDsx8GZCOA0YqZG4k+OXFsV5/gjgIEE7NzzYwbOzTx/us3B1Z9QIHe0S7k8HbFwnhD64Sma/Id++HhWUz+lBtJ/tbk5TyrEZwRxWC2vhWoyfAcepCTZYdRo76PAQlgjxywmdasRmTLUCPzrGBhwg2IHoHkmBFvMMVN6+mWwSJ8u5QC5lTuh4pXvHY4xMOZ/jw5wjQUucMGK2VTkRd6rveQcro7GkJMe6rvF3JTYqa6mjDt+CbZHX+17GQVmnX5D9a1/RhyRbYOsHtFv5ojiKIzBmVNxf3Nfmy3MtPu99NFhJNPostbaOSECCPfE7NJ1H/+cc2LdEaz5rghTpjdoE8bAIIM=
24
+ - secure: foSVBXRpbEdgD3Xvm8hy1KcsRLlqlAiH3Vwq6ZwvwWmvt/la8/iaDKOym0YNIL1hB+tMfRhHeKtD6ruPuag+MVbj8veE89pbzztHTinY/3J9TXoYNuC98xvEtWm3Vqgvb9XMvo1O0mN2jIlTRAdLmHC6So6/3tXWnfZLJrU+wfX5BuCHAuileUX08LX1GQGs1lCIh4r9G3PL7QzITzacI7PPw3tPkgTwAqwvsyS2g9E8nc+lGBLL6Y8VXo9F/63phRJf1fpK0obXJqwJluoiVO38QxlWa188aLBOCZjBoAA3p8gOcYlYoFT86RI6ZINdqqqbCKTqgCGtLEUIeVXNG0c4EPecoD2W7MUHNEwCl3Ee1o/QD2+4drPMQwWyQAKtdQTVKS+kwwW23DJDMiCWD882IL7LJ7DCZcx+5FgIEWdXJBr+ZGsQ0K6AIZRdiNe93GAr7cuZGJgF6u3vNLMD9g5VLA5WirxIJBoVF3WyEp7fLop0OqaCpwsjPo6t8tzhft9OXts3rmMHOFeJnOK2S3zwxGPb5YFDO7TEWqX3oDuI1Pzt7Rky0T0wEFQgyb+WMoijg+ic+H3yXOfKoOhymr2+UHO5vzTdfVbdXF2gR+atU5yv3mhZlLSc2tJdKErLQRQM5kSMIpJHFd8EEeC7XjJ+6zGho+EH3f4jwsLHNiM=
data/.yardopts ADDED
@@ -0,0 +1 @@
1
+ --no-private
data/CONTRIBUTING.md ADDED
@@ -0,0 +1,28 @@
1
+ Firstly, thank you for contributing! Please read our [Engineering Handbook]
2
+ (http://talis.github.io/) for some guidelines.
3
+
4
+ # Local Development
5
+
6
+ ## Setup
7
+
8
+ bundle install
9
+
10
+ Create a `.env` file and add the following:
11
+
12
+ PERSONA_TEST_HOST=<your local persona host>
13
+ PERSONA_OAUTH_CLIENT=<your local persona client ID (with su scope)>
14
+ PERSONA_OAUTH_SECRET=<your local persona client secret (with su scope)>
15
+ BLUEPRINT_TEST_HOST=<your local blueprint host>
16
+ METATRON_TEST_HOST=<your local metatron host>
17
+ METATRON_BASE_PATH=<if not a production env, use /development>
18
+
19
+ If these variables are not set, the production primitives are used.
20
+
21
+ ## Running Tests
22
+
23
+ bundle exec rspec
24
+
25
+ ## Versioning
26
+
27
+ Please use [semantic versioning](http://semver.org/) and bump
28
+ `lib/talis/version.rb` when submitting a pull request.
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in talis.gemspec
4
+ gemspec
data/Guardfile ADDED
@@ -0,0 +1,5 @@
1
+ guard :rspec, cmd: 'bundle exec rspec' do
2
+ watch(%r{^spec/.+_spec\.rb$})
3
+ watch(%r{^lib/.+\.rb$}) { 'spec' }
4
+ watch('spec/spec_helper.rb') { 'spec' }
5
+ end
data/README.md ADDED
@@ -0,0 +1,76 @@
1
+ # Talis Ruby Client
2
+
3
+ [![Build Status](https://travis-ci.org/talis/talis_rb.svg?branch=master)](https://travis-ci.org/talis/talis_rb)
4
+ [![Dependency Status](https://dependencyci.com/github/talis/talis_rb/badge)](https://dependencyci.com/github/talis/talis_rb)
5
+
6
+ A ruby gem that provides interactions with Talis primitives.
7
+
8
+ ## Ubuntu Prerequisites
9
+
10
+ sudo apt-get install libgmp-dev
11
+
12
+ ## Installation
13
+
14
+ Add this line to your application's Gemfile:
15
+
16
+ ```ruby
17
+ gem 'talis', github: talis/talis_rb
18
+ ```
19
+
20
+ And then execute:
21
+
22
+ $ bundle
23
+
24
+ Or install it yourself as:
25
+
26
+ $ gem install talis
27
+
28
+ ## Usage
29
+
30
+ ### Security Configuration
31
+
32
+ Many client operations require an OAuth token.
33
+ In order to carry out these operations, configure the OAuth client:
34
+
35
+ require 'talis/authentication'
36
+
37
+ Talis::Authentication.client_id = 'client_id'
38
+ Talis::Authentication.client_secret = 'client_secret'
39
+
40
+ See the code for each class for specific usage:
41
+ * `lib/talis/authentication/login.rb` For server-side login workflow.
42
+ * `lib/talis/authentication/token.rb` For OAuth token generation and validation.
43
+ * `lib/talis/hierarchy/node.rb` For managing hierarchies.
44
+ * `lib/talis/hierarchy/asset.rb` For managing hierarchy assets.
45
+ * `lib/talis/user.rb` For managing Talis users.
46
+ * `lib/talis/bibliography/work.rb` For querying works.
47
+ * `lib/talis/analytics.rb` For sending analytical data.
48
+
49
+ ## Development
50
+
51
+ After checking out the repo, run `bin/setup` to install dependencies.
52
+
53
+ Create a `.env` file in the project root and configure the following variables:
54
+
55
+ PERSONA_TEST_HOST=http://persona
56
+ PERSONA_OAUTH_CLIENT=<client ID>
57
+ PERSONA_OAUTH_SECRET=<client secret>
58
+ BLUEPRINT_TEST_HOST=http://blueprint
59
+ ECHO_TEST_HOST=http://echo
60
+
61
+ METATRON_TEST_HOST=<Metatron host>
62
+ METATRON_BASE_PATH=<set this to /env_name/2 if not using production>
63
+ METATRON_OAUTH_HOST=<Persona host the Metatron host uses>
64
+ METATRON_OAUTH_CLIENT=<client ID associated with oauth host above>
65
+ METATRON_OAUTH_SECRET=<client secret associated with oauth host above>
66
+ TEST_USER_GUID=<The GUID of the Talis test user (test.tn@talis.com)>
67
+
68
+
69
+ Adjust the values according to the primitives you want to test against (local or remote).
70
+ If testing locally then make sure the host names are in `/etc/hosts`.
71
+
72
+ Then, run `rake spec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
73
+
74
+ This project enforces style and lint rules via [Rubocop](https://github.com/bbatsov/rubocop). Run `rake rubocop` to check for violations.
75
+
76
+ To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and tags, and push the `.gem` file to [rubygems.org](https://rubygems.org).
data/Rakefile ADDED
@@ -0,0 +1,8 @@
1
+ require 'bundler/gem_tasks'
2
+ require 'rspec/core/rake_task'
3
+ require 'rubocop/rake_task'
4
+
5
+ RSpec::Core::RakeTask.new(:spec)
6
+ RuboCop::RakeTask.new
7
+
8
+ task default: [:rubocop, :spec]
data/bin/console ADDED
@@ -0,0 +1,14 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'bundler/setup'
4
+ require 'talis'
5
+
6
+ # You can add fixtures and/or initialization code here to make experimenting
7
+ # with your gem easier. You can also use a different console, if you like.
8
+
9
+ # (If you use this, don't forget to add pry to your Gemfile!)
10
+ # require 'pry'
11
+ # Pry.start
12
+
13
+ require 'irb'
14
+ IRB.start
data/bin/setup ADDED
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+ IFS=$'\n\t'
4
+ set -vx
5
+
6
+ bundle install
7
+
8
+ # Do any other automated setup that you need to do here
data/lib/talis.rb ADDED
@@ -0,0 +1,25 @@
1
+ require 'talis/version'
2
+ require 'talis/constants'
3
+ require 'talis/errors'
4
+ require 'talis/extensions/object'
5
+ require 'talis/resource'
6
+ require 'talis/authentication'
7
+ require 'talis/oauth_service'
8
+ require 'talis/analytics'
9
+ require 'talis/hierarchy'
10
+ require 'talis/feeds'
11
+ require 'talis/user'
12
+ require 'talis/bibliography'
13
+ require 'bundler/setup'
14
+ require 'httparty'
15
+ require 'json'
16
+
17
+ # Main entry point
18
+ module Talis
19
+ class << self
20
+ def new(opts = {})
21
+ token = Talis::Authentication::Token.generate(opts)
22
+ Talis::Authentication::Client.new(token, opts)
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,31 @@
1
+ require_relative 'analytics/event'
2
+
3
+ module Talis
4
+ # Use this as a mixin within your classes to be able to send analytics events.
5
+ module Analytics
6
+ # Create a single analytics event.
7
+ # In order to send events, the client must be configured with a
8
+ # valid OAuth client that is allowed to search for users:
9
+ #
10
+ # Talis::Authentication.client_id = 'client_id'
11
+ # Talis::Authentication.client_secret = 'client_secret'
12
+ #
13
+ # @param event [Hash] The event to send. It must contain the
14
+ # minimum keys:
15
+ # {
16
+ # class: 'my.class.name',
17
+ # source: 'my.source.name'
18
+ # }
19
+ # Other valid keys include: timestamp, user and props. Props can
20
+ # contain any key-value pair of custom data. All other keys will be
21
+ # ignored.
22
+ # @param request_id [String] ('uuid') unique ID for the remote request.
23
+ # @raise [Talis::ClientError] if the request was invalid.
24
+ # @raise [Talis::ServerError] if the search failed on the
25
+ # server.
26
+ # @raise [Talis::ServerCommunicationError] for network issues.
27
+ def send_analytics_event(event, request_id: nil)
28
+ Event.create(request_id: request_id, event: event)
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,67 @@
1
+ module Talis
2
+ module Analytics
3
+ # Represents an event for analytical purposes.
4
+ class Event < Talis::Resource
5
+ base_uri Talis::ECHO_HOST
6
+
7
+ class << self
8
+ # Create a single analytics event.
9
+ # In order to send events, the client must be configured with a
10
+ # valid OAuth client that is allowed to search for users:
11
+ #
12
+ # Talis::Authentication.client_id = 'client_id'
13
+ # Talis::Authentication.client_secret = 'client_secret'
14
+ #
15
+ # @param request_id [String] ('uuid') unique ID for the remote request.
16
+ # @param event [Hash] The event to send. It must contain the
17
+ # minimum keys:
18
+ # {
19
+ # class: 'my.class.name',
20
+ # source: 'my.source.name'
21
+ # }
22
+ # Other valid keys include: timestamp, user and props. Props can
23
+ # contain any key-value pair of custom data. All other keys will be
24
+ # ignored.
25
+ # @raise [Talis::ClientError] if the request was invalid.
26
+ # @raise [Talis::ServerError] if the search failed on the
27
+ # server.
28
+ # @raise [Talis::ServerCommunicationError] for network issues.
29
+ def create(request_id: new_req_id, event:)
30
+ request_id = new_req_id unless request_id
31
+ validate_event event
32
+ payload = whitelist_event event
33
+ begin
34
+ response = post_event(request_id, payload)
35
+ handle_response(response, 204)
36
+ rescue SocketError
37
+ raise Talis::ServerCommunicationError
38
+ end
39
+ end
40
+
41
+ private
42
+
43
+ def post_event(request_id, payload)
44
+ post('/1/events',
45
+ headers: {
46
+ 'Content-Type' => 'application/json',
47
+ 'X-Request-Id' => request_id,
48
+ 'Authorization' => "Bearer #{token}"
49
+ },
50
+ body: [payload].to_json)
51
+ end
52
+
53
+ def validate_event(event)
54
+ error_message = 'event must contain class and source'
55
+ required = [:class, :source]
56
+ provided = event.select { |attr| required.include? attr }.keys
57
+ raise ArgumentError, error_message unless required == provided
58
+ end
59
+
60
+ def whitelist_event(event)
61
+ valid = [:class, :source, :timestamp, :user, :props]
62
+ event.select { |attribute| valid.include? attribute }
63
+ end
64
+ end
65
+ end
66
+ end
67
+ end
@@ -0,0 +1,14 @@
1
+ require_relative 'authentication/client'
2
+ require_relative 'authentication/public_key'
3
+ require_relative 'authentication/token'
4
+ require_relative 'authentication/login'
5
+
6
+ module Talis
7
+ # Encompasses all classes associated with user authentication
8
+ module Authentication
9
+ # The ID of the OAuth client to allow requests for asset resources.
10
+ cattr_accessor :client_id
11
+ # The secret of the OAuth client to allow requests for asset resources.
12
+ cattr_accessor :client_secret
13
+ end
14
+ end
@@ -0,0 +1,82 @@
1
+ require 'httparty'
2
+
3
+ module Talis
4
+ module Authentication
5
+ # Represents an OAuth client
6
+ class Client
7
+ include HTTParty
8
+
9
+ attr_reader :host, :client_id, :client_secret, :token, :scopes
10
+ def initialize(token, opts = {})
11
+ @token = token
12
+ acquire_host!(opts)
13
+ acquire_credentials!
14
+
15
+ response = authenticate!
16
+ body = JSON.parse response.body
17
+ @scopes = body['scope']
18
+ raise Talis::AuthenticationFailedError unless
19
+ response.code == 200
20
+ end
21
+
22
+ def add_scope(scope)
23
+ if scope.is_a? String
24
+ response = modify_scope(:add, scope)
25
+ @scopes << scope if response.code == 204
26
+ end
27
+ end
28
+
29
+ def remove_scope(scope)
30
+ if scope.is_a? String
31
+ response = modify_scope(:remove, scope)
32
+ @scopes.delete(scope) if response.code == 204
33
+ end
34
+ end
35
+
36
+ def modify_scope(action, scope)
37
+ action = case action
38
+ when :add
39
+ '$add'
40
+ when :remove
41
+ '$remove'
42
+ else
43
+ raise 'Unknown action'
44
+ end
45
+ patch_client_scope(action, scope)
46
+ end
47
+
48
+ private
49
+
50
+ def authenticate!
51
+ self.class.get("/clients/#{client_id}",
52
+ headers: { 'Authorization' => bearer_token })
53
+ end
54
+
55
+ def bearer_token
56
+ "Bearer #{token}"
57
+ end
58
+
59
+ def patch_client_scope(action, scope)
60
+ self.class.patch("/clients/#{client_id}",
61
+ headers: {
62
+ 'Content-Type' => 'application/json',
63
+ 'Authorization' => bearer_token
64
+ },
65
+ body: { scope: { action => scope } }.to_json)
66
+ end
67
+
68
+ def acquire_host!(opts)
69
+ if opts[:host].present?
70
+ self.class.base_uri(opts[:host])
71
+ else
72
+ self.class.base_uri(PERSONA_HOST)
73
+ end
74
+ end
75
+
76
+ def acquire_credentials!
77
+ @client_id = ENV['PERSONA_OAUTH_CLIENT']
78
+ @client_secret = ENV['PERSONA_OAUTH_SECRET']
79
+ end
80
+ end
81
+ end
82
+ end
@@ -0,0 +1,169 @@
1
+ require 'base64'
2
+ require 'date'
3
+ require 'digest'
4
+ require 'multi_json'
5
+ require 'uuid'
6
+
7
+ module Talis
8
+ module Authentication
9
+ # Represents the user login flow for server-side applications.
10
+ # A prerequisite to using this class is having an application registered
11
+ # with Persona in order to obtain an app ID and secret. Application
12
+ # registration also provides Persona a callback URL to POST the login
13
+ # response back to the application.
14
+ # @example Redirect user to authentication provider.
15
+ # # First create a login object and redirect the user, storing the state:
16
+ # options = {
17
+ # app_id: 'my_app',
18
+ # app_secret: 'my_secret',
19
+ # provider: 'google',
20
+ # redirect_uri: 'https://my_app/secret_area'
21
+ # }
22
+ # login = Talis::Authentication::Login.new(options)
23
+ # url = login.generate_url
24
+ # session[:state] = login.state
25
+ # redirect_to url
26
+ # @example Handle data after user has logged in.
27
+ # # After the user has logged in, handle the POST callback:
28
+ # state = session.delete(:state)
29
+ # login.validate!(payload: params, state: state)
30
+ # if login.valid?
31
+ # session[:current_user_id] = login.user.guid
32
+ # redirect_to login.redirect_uri
33
+ # else
34
+ # # handle invalid login
35
+ # puts login.error
36
+ # end
37
+ # @example Logging out a user.
38
+ # # When a user logs out, make sure to clean up the session:
39
+ # session.delete(:current_user_id)
40
+ # redirect_to login.logout_url('some/logout/path')
41
+ class Login
42
+ include HTTParty
43
+ # @return [String] a non-guessable alphanumeric string used to prevent
44
+ # CSRF attacks. Store this in the user session after generating a
45
+ # login URL.
46
+ attr_reader :state
47
+ # @return [Talis::User] the logged-in user. This will be nil unless
48
+ # validation has passed.
49
+ attr_reader :user
50
+ # @return [String] if present, this will be the reason why the login
51
+ # failed.
52
+ attr_reader :error
53
+
54
+ base_uri Talis::PERSONA_HOST
55
+
56
+ # Creates a new login object to manage the login flow.
57
+ # @param app_id [String] ID of the application registered to Persona.
58
+ # @param secret [String] secret of the application registered to Persona.
59
+ # @param provider [String] name of the auth provider to use for login.
60
+ # @param redirect_uri [String] where to redirect back to after login.
61
+ def initialize(app_id:, secret:, provider:, redirect_uri:)
62
+ @uuid = UUID.new
63
+
64
+ @app = app_id
65
+ @secret = secret
66
+ @provider = provider
67
+ @redirect_uri = redirect_uri
68
+ end
69
+
70
+ # Use this URL to redirect the user wishing to login to their auth
71
+ # provider. After generating the URL the state will be available to
72
+ # store in a session.
73
+ # @param require [Symbol] (nil) If :profile, upon successful
74
+ # login, the user will be redirected to a profile form if they do not
75
+ # have one.
76
+ # @return [String] the generated URL.
77
+ def generate_url(require: nil)
78
+ @state = Digest::MD5.hexdigest("#{@app}::#{@uuid.generate}")
79
+ params = {
80
+ app: @app,
81
+ state: @state,
82
+ redirectUri: @redirect_uri
83
+ }
84
+ params = params.merge(require: 'profile') if require == :profile
85
+ params = params.to_query
86
+ "#{self.class.base_uri}/auth/providers/#{@provider}/login?#{params}"
87
+ end
88
+
89
+ # Validate a login request after the user has logged in to their auth
90
+ # provider. Validation will fail if the provided payload:
91
+ # - Is not a hash.
92
+ # - When decoded, contains invalid JSON.
93
+ # - Has no state or the state it contains does not match the param state.
94
+ # - Has an invalid signature.
95
+ # If validation succeeds, the #user attribute will return the
96
+ # logged-in user. If it fails, check the #error attribute for the
97
+ # reason why.
98
+ # @param payload [Hash] the payload POSTed to the application server.
99
+ # @param state [String] use the value stored at the start of the session.
100
+ def validate!(payload:, state:)
101
+ return @error = 'payload is not a hash' unless payload.is_a? Hash
102
+
103
+ key = 'persona:payload'
104
+ return @error = "payload missing key #{key}" unless payload.key? key
105
+
106
+ @payload = decode_and_parse_payload(payload)
107
+ return @error = 'payload is not valid JSON' if @payload.nil?
108
+
109
+ state_error = 'payload state does not match provided'
110
+ return @error = state_error if @payload['state'] != state
111
+
112
+ signature_error = 'payload signature does not match expected'
113
+ return @error = signature_error unless signature_valid?(@payload)
114
+
115
+ @user = build_user(@payload)
116
+ end
117
+
118
+ # Indicate whether or not the login succeeded.
119
+ # @return [Boolean] true if the login is valid, otherwise false.
120
+ def valid?
121
+ @error.nil?
122
+ end
123
+
124
+ # The redirect to follow once login has successfully completed.
125
+ # @return [String] the URL to redirect to.
126
+ def redirect_uri
127
+ @payload.present? ? @payload['redirect'] : @redirect_uri
128
+ end
129
+
130
+ # Logs a user out by terminating the session with Persona.
131
+ # @param redirect_url [String] where to return the user on logout.
132
+ # @return [String] the URL to redirect the user to when logging out.
133
+ def logout_url(redirect_url)
134
+ "#{self.class.base_uri}/auth/logout?redirectUri=#{redirect_url}"
135
+ end
136
+
137
+ private
138
+
139
+ def build_user(data)
140
+ profile = data['profile'] || {}
141
+ Talis::User.build(guid: data['guid'],
142
+ first_name: profile['first_name'],
143
+ surname: profile['surname'],
144
+ email: profile['email'],
145
+ access_token: data.fetch('token', {})['access_token'])
146
+ end
147
+
148
+ def decode_and_parse_payload(payload)
149
+ begin
150
+ json = MultiJson.load(Base64.decode64(payload['persona:payload']))
151
+ rescue MultiJson::LoadError
152
+ return nil
153
+ end
154
+ json
155
+ end
156
+
157
+ def signature_valid?(payload)
158
+ received_signature = payload.delete('signature')
159
+
160
+ # Persona PHP code escapes forward slashes when encoding JSON
161
+ json_payload = payload.to_json.gsub('/', '\/')
162
+
163
+ digest = OpenSSL::Digest.new('sha256')
164
+ signature = OpenSSL::HMAC.hexdigest(digest, @secret, json_payload)
165
+ received_signature == signature
166
+ end
167
+ end
168
+ end
169
+ end