cogitate 0.0.1 → 0.0.2
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 +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
|