talis 0.12.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|
+
[![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
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
|