talis 0.12.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 +7 -0
- data/.gitignore +12 -0
- data/.rspec +2 -0
- data/.rubocop.yml +2 -0
- data/.travis.yml +24 -0
- data/.yardopts +1 -0
- data/CONTRIBUTING.md +28 -0
- data/Gemfile +4 -0
- data/Guardfile +5 -0
- data/README.md +76 -0
- data/Rakefile +8 -0
- data/bin/console +14 -0
- data/bin/setup +8 -0
- data/lib/talis.rb +25 -0
- data/lib/talis/analytics.rb +31 -0
- data/lib/talis/analytics/event.rb +67 -0
- data/lib/talis/authentication.rb +14 -0
- data/lib/talis/authentication/client.rb +82 -0
- data/lib/talis/authentication/login.rb +169 -0
- data/lib/talis/authentication/public_key.rb +53 -0
- data/lib/talis/authentication/token.rb +172 -0
- data/lib/talis/bibliography.rb +52 -0
- data/lib/talis/bibliography/ebook.rb +50 -0
- data/lib/talis/bibliography/manifestation.rb +141 -0
- data/lib/talis/bibliography/result_set.rb +34 -0
- data/lib/talis/bibliography/work.rb +164 -0
- data/lib/talis/constants.rb +9 -0
- data/lib/talis/errors.rb +10 -0
- data/lib/talis/errors/authentication_failed_error.rb +4 -0
- data/lib/talis/errors/client_errors.rb +19 -0
- data/lib/talis/errors/server_communication_error.rb +4 -0
- data/lib/talis/errors/server_error.rb +4 -0
- data/lib/talis/extensions/object.rb +11 -0
- data/lib/talis/feeds.rb +8 -0
- data/lib/talis/feeds/annotation.rb +129 -0
- data/lib/talis/feeds/feed.rb +58 -0
- data/lib/talis/hierarchy.rb +9 -0
- data/lib/talis/hierarchy/asset.rb +265 -0
- data/lib/talis/hierarchy/node.rb +200 -0
- data/lib/talis/hierarchy/resource.rb +159 -0
- data/lib/talis/oauth_service.rb +18 -0
- data/lib/talis/resource.rb +68 -0
- data/lib/talis/user.rb +112 -0
- data/lib/talis/version.rb +3 -0
- data/talis.gemspec +39 -0
- 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
data/.rspec
ADDED
data/.rubocop.yml
ADDED
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
data/Guardfile
ADDED
data/README.md
ADDED
@@ -0,0 +1,76 @@
|
|
1
|
+
# Talis Ruby Client
|
2
|
+
|
3
|
+
[](https://travis-ci.org/talis/talis_rb)
|
4
|
+
[](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
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
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
|