cogitate 0.0.1 → 0.0.2
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/README.md +69 -15
- data/lib/cogitate.rb +18 -2
- data/lib/cogitate/README.md +8 -0
- data/lib/cogitate/client.rb +97 -0
- data/lib/cogitate/client/agent_builder.rb +86 -0
- data/lib/cogitate/client/data_to_object_coercer.rb +38 -0
- data/lib/cogitate/client/exceptions.rb +10 -0
- data/lib/cogitate/client/identifier_builder.rb +26 -0
- data/lib/cogitate/client/request.rb +56 -0
- data/lib/cogitate/client/response_parsers.rb +19 -0
- data/lib/cogitate/client/response_parsers/agents_with_detailed_identifiers_extractor.rb +15 -0
- data/lib/cogitate/client/response_parsers/agents_without_group_membership_extractor.rb +23 -0
- data/lib/cogitate/client/response_parsers/basic_extractor.rb +14 -0
- data/lib/cogitate/client/response_parsers/email_extractor.rb +17 -0
- data/lib/cogitate/client/retrieve_agent_from_ticket.rb +43 -0
- data/lib/cogitate/client/ticket_to_token_coercer.rb +35 -0
- data/lib/cogitate/client/token_to_object_coercer.rb +38 -0
- data/lib/cogitate/configuration.rb +99 -0
- data/lib/cogitate/exceptions.rb +25 -0
- data/lib/cogitate/generators/install_generator.rb +10 -0
- data/lib/cogitate/generators/templates/cogitate_initializer.rb.erb +17 -0
- data/lib/cogitate/interfaces.rb +40 -0
- data/lib/cogitate/models.rb +6 -0
- data/lib/cogitate/models/agent.rb +143 -0
- data/lib/cogitate/models/agent/serializer.rb +75 -0
- data/lib/cogitate/models/agent/with_token.rb +29 -0
- data/lib/cogitate/models/identifier.rb +114 -0
- data/lib/cogitate/models/identifiers.rb +7 -0
- data/lib/cogitate/models/identifiers/with_attribute_hash.rb +44 -0
- data/lib/cogitate/railtie.rb +8 -0
- data/lib/cogitate/services/identifiers_decoder.rb +61 -0
- data/lib/cogitate/services/tokenizer.rb +80 -0
- metadata +105 -5
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: bd4f1bc648d721edb5b1d0887b0cd1612182dff4
|
4
|
+
data.tar.gz: a8cd537d72bebc2a4557539220442f7d406409fa
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 190c6832b7674aaa581f1a0bfc8793c08f3178cb3c69f9b980a9a6098314dd1397046ebcbaa05f29b9bf43bce53d0532643ccc47b7d0d1201656671f85881775
|
7
|
+
data.tar.gz: e04fddd3eddef8e429a87787f400d4184ff97435b8723bb315382f01d70e42bc9d3dedc10ebadf19d9f63791dfe7dc52f2987607b39c8569f8c71402d07ef5aa
|
data/README.md
CHANGED
@@ -16,6 +16,19 @@ Welcome to Cogitate, a federated identity management system for managing:
|
|
16
16
|
* Parroted identities (ask for the identity of a Kroger Card number, you'll get back a Kroger card number)
|
17
17
|
* User authentication through various providers
|
18
18
|
|
19
|
+
## Heads Up
|
20
|
+
|
21
|
+
Mixed within this repository are two concerns:
|
22
|
+
|
23
|
+
* Cogitate Rails application, the core of which is in `app/` though it leverages `lib/` files
|
24
|
+
* Cogitate client gem, which is contained within `lib/`
|
25
|
+
|
26
|
+
For now these are kept together for ease of application development.
|
27
|
+
The cogitate gem can be published via this repository (see cogitate.gemspec) for additional information.
|
28
|
+
|
29
|
+
In keeping these together, it is important to understand that files in `lib/` should not reference files in `app/`.
|
30
|
+
That is a general best practice, however it is a requirement of the cogitate gem.
|
31
|
+
|
19
32
|
## Documentation and Semantic Versioning
|
20
33
|
|
21
34
|
A note on documentation and semantic versioning.
|
@@ -47,12 +60,24 @@ def method_signature_and_return_value_may_be_changed
|
|
47
60
|
end
|
48
61
|
```
|
49
62
|
|
63
|
+
## Authentication
|
64
|
+
|
65
|
+
Authentication through Cogitate is a multi-step affair:
|
66
|
+
|
67
|
+
1. Client application requests `/authenticate?after_authentication_callback_url=<after_authentication_callback_url>` from Cogitate
|
68
|
+
1. Cogitate prompts user to choose authentication mechanism; **At present Cogitate makes the decision and redirects to CAS**
|
69
|
+
1. Authentication Service (i.e. CAS, OAuth2 Provider, etc.) handles authentication and reports back to Cogitate
|
70
|
+
1. Cogitate issues a ticket to the requesting Client via the given `:after_authentication_callback_url`
|
71
|
+
1. The Client application claims the ticket from Cogitate via `/claim?ticket=<ticket>`
|
72
|
+
1. Cogitate processes the claim and responds with a token
|
73
|
+
1. The Client application parses the token into an Agent
|
74
|
+
|
50
75
|
## API
|
51
76
|
|
52
|
-
### GET /
|
77
|
+
### GET /authenticate?after_authentication_callback_url=<cgi escaped URL>
|
53
78
|
|
54
79
|
```console
|
55
|
-
GET /
|
80
|
+
GET /authenticate?after_authentication_callback_url=https%3A%2F%2Fdeposit.library.nd.edu%2Fafter_authenticate
|
56
81
|
```
|
57
82
|
|
58
83
|
This resource is responsible for brokering the actual authentication service.
|
@@ -64,6 +89,14 @@ Cogitate will redirect to the URL specified in the `GET /auth` request's `after_
|
|
64
89
|
The payload will be a JSON Web Token.
|
65
90
|
That token should contain enough information for your application to adjudicate authorization questions.
|
66
91
|
|
92
|
+
### GET /claim?ticket=<cgi escaped TICKET>
|
93
|
+
|
94
|
+
```console
|
95
|
+
GET /claim?ticket=123456789
|
96
|
+
```
|
97
|
+
|
98
|
+
This resource is responsible for transforming a ticket into a token.
|
99
|
+
|
67
100
|
### GET Agents
|
68
101
|
|
69
102
|
#### Request
|
@@ -86,8 +119,8 @@ urlsafe_base64_encoded_identifiers = Base64.urlsafe_encode64(identifier)
|
|
86
119
|
|
87
120
|
```ruby
|
88
121
|
require 'base64'
|
89
|
-
Base64.urlsafe_encode64("
|
90
|
-
=> "
|
122
|
+
Base64.urlsafe_encode64("netid\thworld")
|
123
|
+
=> "bmV0aWQJaHdvcmxk"
|
91
124
|
```
|
92
125
|
|
93
126
|
**Note:** Delimit multiple identifiers with a new line (i.e. `\n`).
|
@@ -97,22 +130,43 @@ Base64.urlsafe_encode64("orcid\t0000-0002-1191-0873")
|
|
97
130
|
```json
|
98
131
|
{
|
99
132
|
"links": {
|
100
|
-
"self": "http://localhost:3000/api/agents/
|
133
|
+
"self": "http://localhost:3000/api/agents/bmV0aWQJaHdvcmxk"
|
101
134
|
},
|
102
135
|
"data": [{
|
103
136
|
"type": "agents",
|
104
|
-
"id": "
|
137
|
+
"id": "bmV0aWQJaHdvcmxk",
|
138
|
+
"links": {
|
139
|
+
"self": "http://localhost:3000/api/agents/bmV0aWQJaHdvcmxk"
|
140
|
+
},
|
105
141
|
"attributes": {
|
106
|
-
"strategy": "
|
107
|
-
"identifying_value": "
|
142
|
+
"strategy": "netid",
|
143
|
+
"identifying_value": "hworld",
|
144
|
+
"emails": ["hworld@nd.edu"]
|
108
145
|
},
|
109
146
|
"relationships": {
|
110
|
-
"
|
111
|
-
"type": "
|
112
|
-
"id": "
|
147
|
+
"identifiers": [{
|
148
|
+
"type": "identifiers",
|
149
|
+
"id": "bmV0aWQJaHdvcmxk"
|
113
150
|
}],
|
114
|
-
"
|
115
|
-
|
151
|
+
"verified_identifiers": [{
|
152
|
+
"type": "identifiers",
|
153
|
+
"id": "bmV0aWQJaHdvcmxk"
|
154
|
+
}]
|
155
|
+
},
|
156
|
+
"included": [{
|
157
|
+
"type": "identifiers",
|
158
|
+
"id": "bmV0aWQJaHdvcmxk",
|
159
|
+
"attributes": {
|
160
|
+
"identifying_value": "hworld",
|
161
|
+
"strategy": "netid",
|
162
|
+
"first_name": "Hello",
|
163
|
+
"last_name": "World",
|
164
|
+
"netid": "hworld",
|
165
|
+
"full_name": "Hello World",
|
166
|
+
"ndguid": "nd.edu.hworld",
|
167
|
+
"email": "hworld@nd.edu"
|
168
|
+
}
|
169
|
+
}]
|
116
170
|
}]
|
117
171
|
}
|
118
172
|
```
|
@@ -137,8 +191,8 @@ It was also clear that our institutional service was inadequate due to the natur
|
|
137
191
|
* Communication Channels
|
138
192
|
* ~~Extract email from NetID identifier~~
|
139
193
|
* Client library
|
140
|
-
* Decode the JSON Web Token (JWT) into a "User" object and related information
|
141
|
-
* Levarage RSA public key for decoding the JWT
|
194
|
+
* ~~Decode the JSON Web Token (JWT) into a "User" object and related information~~
|
195
|
+
* ~~Levarage RSA public key for decoding the JWT~~
|
142
196
|
|
143
197
|
### Phase 2
|
144
198
|
|
data/lib/cogitate.rb
CHANGED
@@ -1,3 +1,5 @@
|
|
1
|
+
require 'cogitate/configuration'
|
2
|
+
|
1
3
|
# Cogitate is a federated identity management system for managing:
|
2
4
|
# * User identities through:
|
3
5
|
# * Group membership
|
@@ -6,6 +8,20 @@
|
|
6
8
|
# * Parroted identities (ask for the identity of a Kroger Card number, you'll get back a Kroger card number)
|
7
9
|
# * User authentication through various providers
|
8
10
|
module Cogitate
|
9
|
-
#
|
10
|
-
VERSION = '0.0.
|
11
|
+
# This version reflects the gem version for release
|
12
|
+
VERSION = '0.0.2'.freeze
|
13
|
+
|
14
|
+
def self.configure
|
15
|
+
yield(configuration)
|
16
|
+
end
|
17
|
+
|
18
|
+
def self.configuration=(input)
|
19
|
+
@configuration = input
|
20
|
+
end
|
21
|
+
|
22
|
+
def self.configuration
|
23
|
+
@configuration ||= Cogitate::Configuration.new
|
24
|
+
end
|
11
25
|
end
|
26
|
+
|
27
|
+
require 'cogitate/railtie' if defined?(Rails)
|
@@ -0,0 +1,8 @@
|
|
1
|
+
Welcome to the Cogitate gem.
|
2
|
+
|
3
|
+
If you are looking at the code repository this gem may look a little unusual.
|
4
|
+
It is embedded inside a Rails application (Cogitate).
|
5
|
+
|
6
|
+
The goal is that the initial development shares many concerns, so I don't want to separate Client and Server side responsibilities.
|
7
|
+
|
8
|
+
**Note:** Any files in the lib/cogitate directory will be included in the cogitate gem.
|
@@ -0,0 +1,97 @@
|
|
1
|
+
require 'base64'
|
2
|
+
require 'cogitate'
|
3
|
+
require 'cogitate/exceptions'
|
4
|
+
require 'cogitate/client/ticket_to_token_coercer'
|
5
|
+
require 'cogitate/client/token_to_object_coercer'
|
6
|
+
require 'cogitate/client/response_parsers'
|
7
|
+
require 'cogitate/client/request'
|
8
|
+
|
9
|
+
module Cogitate
|
10
|
+
# Responsible for collecting the various client related behaviors.
|
11
|
+
module Client
|
12
|
+
# @api public
|
13
|
+
#
|
14
|
+
# Allows for a predictable and repeatable way for a developer to alter the configuration of Cogitate.
|
15
|
+
# This is something most helpful under tests.
|
16
|
+
#
|
17
|
+
# @example
|
18
|
+
# Coigtate::Client.with_custom_configuration do
|
19
|
+
# Do things with the alternate Cogitate end-point
|
20
|
+
# end
|
21
|
+
#
|
22
|
+
# @param remote_server_base_url [String] Where is a remote instance of Cogitate running?
|
23
|
+
# @param keywords [Hash] See Cogitate::Configuration for the detailed parameters.
|
24
|
+
# @return void
|
25
|
+
#
|
26
|
+
# @note This is a convenience method for testing; Woe is yeah that uses this in a non-test environment
|
27
|
+
def self.with_custom_configuration(remote_server_base_url: 'http://localhost:3000', **keywords)
|
28
|
+
old_configuration = Cogitate.configuration
|
29
|
+
configuration = Cogitate::Configuration.new(remote_server_base_url: remote_server_base_url, **keywords)
|
30
|
+
Cogitate.configuration = configuration
|
31
|
+
yield
|
32
|
+
ensure
|
33
|
+
Cogitate.configuration = old_configuration
|
34
|
+
end
|
35
|
+
|
36
|
+
# @api public
|
37
|
+
#
|
38
|
+
# A URL safe encoding of the given `strategy` and `identifying_value`
|
39
|
+
#
|
40
|
+
# @param strategy [String]
|
41
|
+
# @param identifying_value [String]
|
42
|
+
#
|
43
|
+
# @return [String] a URL safe encoding of the object's identifying attributes
|
44
|
+
#
|
45
|
+
# @see Cogitate::Models::Identifier
|
46
|
+
def self.encoded_identifier_for(strategy:, identifying_value:)
|
47
|
+
Base64.urlsafe_encode64("#{strategy.to_s.downcase}\t#{identifying_value}")
|
48
|
+
end
|
49
|
+
|
50
|
+
# @api public
|
51
|
+
def self.extract_strategy_and_identifying_value(encoded_string)
|
52
|
+
Base64.urlsafe_decode64(encoded_string).split("\t")
|
53
|
+
rescue ArgumentError
|
54
|
+
raise Cogitate::InvalidIdentifierEncoding, encoded_string: encoded_string
|
55
|
+
end
|
56
|
+
|
57
|
+
# @api public
|
58
|
+
def self.retrieve_token_from(ticket:)
|
59
|
+
TicketToTokenCoercer.call(ticket: ticket)
|
60
|
+
end
|
61
|
+
|
62
|
+
# @api public
|
63
|
+
def self.extract_agent_from(token:)
|
64
|
+
TokenToObjectCoercer.call(token: token)
|
65
|
+
end
|
66
|
+
|
67
|
+
# @api public
|
68
|
+
#
|
69
|
+
# @param identifiers [Array<String>]
|
70
|
+
# @param response_parser [#call(response:)]
|
71
|
+
def self.request(identifiers:, response_parser: :AgentsWithDetailedIdentfiers)
|
72
|
+
coerced_parser = response_parser_for(response_parser)
|
73
|
+
Request.call(identifiers: identifiers, response_parser: coerced_parser)
|
74
|
+
end
|
75
|
+
|
76
|
+
# @api public
|
77
|
+
# @param object [String, Symbol, #call]
|
78
|
+
# @return #call(identifier:)
|
79
|
+
def self.response_parser_for(object)
|
80
|
+
ResponseParsers.fetch(object)
|
81
|
+
end
|
82
|
+
|
83
|
+
# @api public
|
84
|
+
#
|
85
|
+
# @param identifiers [Array<String>]
|
86
|
+
def self.retrieve_primary_emails_associated_with(identifiers:, response_parser: :Email)
|
87
|
+
request(identifiers: identifiers, response_parser: response_parser)
|
88
|
+
end
|
89
|
+
|
90
|
+
# @api public
|
91
|
+
#
|
92
|
+
# @param identifiers [Array<String>]
|
93
|
+
def self.request_agents_without_group_membership(identifiers:, response_parser: :AgentsWithoutGroupMembership)
|
94
|
+
request(identifiers: identifiers, response_parser: response_parser)
|
95
|
+
end
|
96
|
+
end
|
97
|
+
end
|
@@ -0,0 +1,86 @@
|
|
1
|
+
module Cogitate
|
2
|
+
module Client
|
3
|
+
# Responsible for transforming a well formed Hash into an Agent and its constituent parts.
|
4
|
+
#
|
5
|
+
# @see Cogitate::Models::Agent
|
6
|
+
# @see Cogitate::Client::AgentBuilder.call
|
7
|
+
class AgentBuilder
|
8
|
+
# @api public
|
9
|
+
#
|
10
|
+
# Responsible for transforming a well formed Hash into an Agent and its constituent parts.
|
11
|
+
#
|
12
|
+
# @param data [Hash] with string keys
|
13
|
+
# @param [Hash] keywords
|
14
|
+
# @option keywords [#call] :identifier_builder converts the 'id' into an Cogitate::Models::Identifier
|
15
|
+
# @return [Cogitate::Models::Agent]
|
16
|
+
# @raise KeyError if the input data is not well formed
|
17
|
+
def self.call(data, **keywords)
|
18
|
+
new(data, **keywords).call
|
19
|
+
end
|
20
|
+
|
21
|
+
# @api private
|
22
|
+
def initialize(data, identifier_guard: default_identifier_guard, **keywords)
|
23
|
+
self.data = data
|
24
|
+
self.identifier_guard = identifier_guard
|
25
|
+
self.identifier_builder = keywords.fetch(:identifier_builder) { default_identifier_builder }
|
26
|
+
self.agent_builder = keywords.fetch(:agent_builder) { default_agent_builder }
|
27
|
+
set_agent!
|
28
|
+
end
|
29
|
+
|
30
|
+
# @api private
|
31
|
+
def call
|
32
|
+
assign_identifiers_to_agent
|
33
|
+
assign_verified_identifiers_to_agent
|
34
|
+
agent
|
35
|
+
end
|
36
|
+
|
37
|
+
private
|
38
|
+
|
39
|
+
def assign_identifiers_to_agent
|
40
|
+
with_assigning_relationship_for(key: 'identifiers') do |identifier|
|
41
|
+
agent.add_identifier(identifier)
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
def assign_verified_identifiers_to_agent
|
46
|
+
with_assigning_relationship_for(key: 'verified_identifiers') do |identifier|
|
47
|
+
agent.add_verified_identifier(identifier)
|
48
|
+
next unless identifier.respond_to?(:email)
|
49
|
+
agent.add_email(identifier.email)
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
def with_assigning_relationship_for(key:)
|
54
|
+
data.fetch('relationships').fetch(key).each do |relationship|
|
55
|
+
identifier = identifier_builder.call(encoded_identifier: relationship.fetch('id'), included: data.fetch('included', []))
|
56
|
+
next unless identifier_guard.call(identifier: identifier)
|
57
|
+
yield(identifier)
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
attr_accessor :data, :identifier_builder, :identifier_guard, :agent_builder
|
62
|
+
attr_reader :agent
|
63
|
+
|
64
|
+
def set_agent!
|
65
|
+
@agent = agent_builder.call(encoded_identifier: data.fetch('id'))
|
66
|
+
end
|
67
|
+
|
68
|
+
# @api private
|
69
|
+
# Because the identifiers decoder returns an array; However I want a single object.
|
70
|
+
def default_identifier_builder
|
71
|
+
require 'cogitate/client/identifier_builder' unless defined? Services::IdentifierBuilder
|
72
|
+
Client::IdentifierBuilder
|
73
|
+
end
|
74
|
+
|
75
|
+
# @api private
|
76
|
+
def default_agent_builder
|
77
|
+
require 'cogitate/models/agent' unless defined? Cogitate::Models::Agent
|
78
|
+
Cogitate::Models::Agent.method(:build_with_encoded_id)
|
79
|
+
end
|
80
|
+
|
81
|
+
def default_identifier_guard
|
82
|
+
-> (*) { true }
|
83
|
+
end
|
84
|
+
end
|
85
|
+
end
|
86
|
+
end
|
@@ -0,0 +1,38 @@
|
|
1
|
+
require 'cogitate/client/agent_builder'
|
2
|
+
|
3
|
+
module Cogitate
|
4
|
+
module Client
|
5
|
+
# Responsible for coercing a JSON hash into a Cogitate::Client object
|
6
|
+
#
|
7
|
+
# @see Cogitate::Client::DataToObjectCoercer.call
|
8
|
+
module DataToObjectCoercer
|
9
|
+
module_function
|
10
|
+
|
11
|
+
# @api public
|
12
|
+
#
|
13
|
+
# Responsible for coercing a JSON hash into a Cogitate::Client object
|
14
|
+
#
|
15
|
+
# @param data [Hash] with string keys
|
16
|
+
# @param type_to_builder_map [Hash] a lookup table of key to constant
|
17
|
+
# @return the result of building the object as per the :type_to_builder_map
|
18
|
+
# @raise KeyError if `data` does not have 'type' key or if `type_to_builder_map` does not have `data['type']` key
|
19
|
+
def call(data, type_to_builder_map: default_type_to_builder_map, **keywords)
|
20
|
+
type = data.fetch('type')
|
21
|
+
builder = type_to_builder_map.fetch(type.to_s)
|
22
|
+
builder.call(data, **keywords)
|
23
|
+
end
|
24
|
+
|
25
|
+
TYPE_TO_BUILDER_MAP = {
|
26
|
+
'agents' => AgentBuilder,
|
27
|
+
'agent' => AgentBuilder
|
28
|
+
}.freeze
|
29
|
+
private_constant :TYPE_TO_BUILDER_MAP
|
30
|
+
|
31
|
+
# @api private
|
32
|
+
def default_type_to_builder_map
|
33
|
+
TYPE_TO_BUILDER_MAP
|
34
|
+
end
|
35
|
+
private_class_method :default_type_to_builder_map
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
@@ -0,0 +1,26 @@
|
|
1
|
+
require 'cogitate/models/identifiers/with_attribute_hash'
|
2
|
+
|
3
|
+
module Cogitate
|
4
|
+
module Client
|
5
|
+
# Responsible for transforming an encoded identifier into an identifier
|
6
|
+
# then decorating that identifier with attributes that may be found in the
|
7
|
+
# array of :included objects
|
8
|
+
#
|
9
|
+
# @see Cogitate::Models::Identifier
|
10
|
+
# @see Cogitate::Client::IdentifierBuilder.call
|
11
|
+
module IdentifierBuilder
|
12
|
+
# @api public
|
13
|
+
def self.call(encoded_identifier:, included: [], identifier_decoder: default_identifier_decoder)
|
14
|
+
base_identifier = identifier_decoder.call(encoded_identifier: encoded_identifier)
|
15
|
+
included_object = included.detect { |obj| obj['id'] == encoded_identifier }
|
16
|
+
return base_identifier unless included_object
|
17
|
+
Models::Identifiers::WithAttributeHash.new(identifier: base_identifier, attributes: included_object.fetch('attributes', {}))
|
18
|
+
end
|
19
|
+
|
20
|
+
def self.default_identifier_decoder
|
21
|
+
require 'cogitate/services/identifiers_decoder' unless defined? Services::IdentifiersDecoder
|
22
|
+
->(encoded_identifier:) { Services::IdentifiersDecoder.call(encoded_identifier).first }
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|