cogitate 0.0.1 → 0.0.2

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