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.
Files changed (34) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +69 -15
  3. data/lib/cogitate.rb +18 -2
  4. data/lib/cogitate/README.md +8 -0
  5. data/lib/cogitate/client.rb +97 -0
  6. data/lib/cogitate/client/agent_builder.rb +86 -0
  7. data/lib/cogitate/client/data_to_object_coercer.rb +38 -0
  8. data/lib/cogitate/client/exceptions.rb +10 -0
  9. data/lib/cogitate/client/identifier_builder.rb +26 -0
  10. data/lib/cogitate/client/request.rb +56 -0
  11. data/lib/cogitate/client/response_parsers.rb +19 -0
  12. data/lib/cogitate/client/response_parsers/agents_with_detailed_identifiers_extractor.rb +15 -0
  13. data/lib/cogitate/client/response_parsers/agents_without_group_membership_extractor.rb +23 -0
  14. data/lib/cogitate/client/response_parsers/basic_extractor.rb +14 -0
  15. data/lib/cogitate/client/response_parsers/email_extractor.rb +17 -0
  16. data/lib/cogitate/client/retrieve_agent_from_ticket.rb +43 -0
  17. data/lib/cogitate/client/ticket_to_token_coercer.rb +35 -0
  18. data/lib/cogitate/client/token_to_object_coercer.rb +38 -0
  19. data/lib/cogitate/configuration.rb +99 -0
  20. data/lib/cogitate/exceptions.rb +25 -0
  21. data/lib/cogitate/generators/install_generator.rb +10 -0
  22. data/lib/cogitate/generators/templates/cogitate_initializer.rb.erb +17 -0
  23. data/lib/cogitate/interfaces.rb +40 -0
  24. data/lib/cogitate/models.rb +6 -0
  25. data/lib/cogitate/models/agent.rb +143 -0
  26. data/lib/cogitate/models/agent/serializer.rb +75 -0
  27. data/lib/cogitate/models/agent/with_token.rb +29 -0
  28. data/lib/cogitate/models/identifier.rb +114 -0
  29. data/lib/cogitate/models/identifiers.rb +7 -0
  30. data/lib/cogitate/models/identifiers/with_attribute_hash.rb +44 -0
  31. data/lib/cogitate/railtie.rb +8 -0
  32. data/lib/cogitate/services/identifiers_decoder.rb +61 -0
  33. data/lib/cogitate/services/tokenizer.rb +80 -0
  34. metadata +105 -5
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: b72f297200afd3bfebdda28808cde1bd0c23a105
4
- data.tar.gz: 2096455e488ce567637e4762e187fa511cd9ac29
3
+ metadata.gz: bd4f1bc648d721edb5b1d0887b0cd1612182dff4
4
+ data.tar.gz: a8cd537d72bebc2a4557539220442f7d406409fa
5
5
  SHA512:
6
- metadata.gz: a7819b9a0cc1b86e7c232f6662182ec5f69fc8e739360f501a0aada1bfef9437e01ee3126dcab49f24db78c8eeb8a7ba47bfdeaa72d37c5691fda7c1134d35d0
7
- data.tar.gz: b20bb82409bb415fb3434d3c04ec14ca0b47b81544bb435f506b7026c331f88f1e872ec0c1ad3eaf2045428aa7955ceeae609c9417d2424e044c2bddae57ea4e
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 /auth?after_authentication_callback_url=<cgi escaped URL>
77
+ ### GET /authenticate?after_authentication_callback_url=<cgi escaped URL>
53
78
 
54
79
  ```console
55
- GET /auth?after_authentication_callback_url=https%3A%2F%2Fdeposit.library.nd.edu%2Fafter_authenticate
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("orcid\t0000-0002-1191-0873")
90
- => "b3JjaWQJMDAwMC0wMDAyLTExOTEtMDg3Mw=="
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/b3JjaWQJMDAwMC0wMDAyLTExOTEtMDg3Mw=="
133
+ "self": "http://localhost:3000/api/agents/bmV0aWQJaHdvcmxk"
101
134
  },
102
135
  "data": [{
103
136
  "type": "agents",
104
- "id": "b3JjaWQJMDAwMC0wMDAyLTExOTEtMDg3Mw==",
137
+ "id": "bmV0aWQJaHdvcmxk",
138
+ "links": {
139
+ "self": "http://localhost:3000/api/agents/bmV0aWQJaHdvcmxk"
140
+ },
105
141
  "attributes": {
106
- "strategy": "orcid",
107
- "identifying_value": "0000-0002-1191-0873"
142
+ "strategy": "netid",
143
+ "identifying_value": "hworld",
144
+ "emails": ["hworld@nd.edu"]
108
145
  },
109
146
  "relationships": {
110
- "identities": [{
111
- "type": "unverified/orcid",
112
- "id": "0000-0002-1191-0873"
147
+ "identifiers": [{
148
+ "type": "identifiers",
149
+ "id": "bmV0aWQJaHdvcmxk"
113
150
  }],
114
- "verified_identities": []
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
 
@@ -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
- # Used as a namespace grab
10
- VERSION = '0.0.1'.freeze
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,10 @@
1
+ module Cogitate
2
+ module Client
3
+ # Unable to find the parser give hints
4
+ class ResponseParserNotFound < NameError
5
+ def initialize(name, namespace)
6
+ super("Unable to find #{name.inspect} parser in #{namespace}")
7
+ end
8
+ end
9
+ end
10
+ 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