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
@@ -0,0 +1,56 @@
1
+ require 'active_support/core_ext/array/wrap'
2
+
3
+ module Cogitate
4
+ module Client
5
+ # Request from Cogitate the given identifiers and parse leveraging the custom parser.
6
+ class Request
7
+ # @api public
8
+ def self.call(identifiers:, **keywords)
9
+ new(identifiers: identifiers, **keywords).call
10
+ end
11
+
12
+ def initialize(identifiers:, response_parser:, configuration: default_configuration)
13
+ self.identifiers = identifiers
14
+ self.configuration = configuration
15
+ self.response_parser = response_parser
16
+ initialize_urlsafe_base64_encoded_identifiers!
17
+ initialize_url_for_request!
18
+ end
19
+
20
+ def call
21
+ response = client_request_handler.call(url: url_for_request)
22
+ response_parser.call(response: response)
23
+ end
24
+
25
+ private
26
+
27
+ extend Forwardable
28
+ def_delegator :configuration, :client_request_handler
29
+
30
+ attr_accessor :response_parser, :configuration
31
+
32
+ attr_reader :identifiers, :urlsafe_base64_encoded_identifiers, :url_for_request
33
+
34
+ def identifiers=(input)
35
+ @identifiers = Array.wrap(input)
36
+ end
37
+
38
+ def initialize_urlsafe_base64_encoded_identifiers!
39
+ @urlsafe_base64_encoded_identifiers = Base64.urlsafe_encode64(
40
+ identifiers.map { |identifier| Base64.urlsafe_decode64(identifier) }.join("\n")
41
+ )
42
+ end
43
+
44
+ def initialize_url_for_request!
45
+ @url_for_request = configuration.url_for_retrieving_agents_for(
46
+ urlsafe_base64_encoded_identifiers: urlsafe_base64_encoded_identifiers
47
+ )
48
+ end
49
+
50
+ def default_configuration
51
+ require 'cogitate'
52
+ Cogitate.configuration
53
+ end
54
+ end
55
+ end
56
+ end
@@ -0,0 +1,19 @@
1
+ require 'cogitate/client/exceptions'
2
+
3
+ module Cogitate
4
+ module Client
5
+ # Responsibl
6
+ module ResponseParsers
7
+ def self.fetch(name)
8
+ return name if name.respond_to?(:call)
9
+ const_get("#{name}Extractor")
10
+ rescue NameError
11
+ raise Client::ResponseParserNotFound.new(name, self)
12
+ end
13
+ end
14
+ end
15
+ end
16
+
17
+ Dir.glob(File.expand_path('../response_parsers/**/*', __FILE__)).each do |filename|
18
+ require filename
19
+ end
@@ -0,0 +1,15 @@
1
+ require 'json'
2
+ require 'cogitate/client/data_to_object_coercer'
3
+
4
+ module Cogitate
5
+ module Client
6
+ module ResponseParsers
7
+ # Responsible for parsing a Cogitate response and just getting the basic data
8
+ module AgentsWithDetailedIdentifiersExtractor
9
+ def self.call(response:)
10
+ JSON.parse(response).fetch('data').map { |datum| DataToObjectCoercer.call(datum) }
11
+ end
12
+ end
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,23 @@
1
+ require 'json'
2
+ require 'cogitate/client/data_to_object_coercer'
3
+ require 'cogitate/models/identifier'
4
+
5
+ module Cogitate
6
+ module Client
7
+ module ResponseParsers
8
+ # When you want an agent that omits identifiers associated with a group
9
+ module AgentsWithoutGroupMembershipExtractor
10
+ def self.call(response:)
11
+ identifier_guard = method(:identifier_is_not_a_group?)
12
+ JSON.parse(response).fetch('data').map { |datum| DataToObjectCoercer.call(datum, identifier_guard: identifier_guard) }
13
+ end
14
+
15
+ # @api private
16
+ # Perhaps a weird place to put this code, but it appears to work
17
+ def self.identifier_is_not_a_group?(identifier:)
18
+ identifier.strategy != Cogitate::Models::Identifier::GROUP_STRATEGY_NAME
19
+ end
20
+ end
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,14 @@
1
+ require 'json'
2
+
3
+ module Cogitate
4
+ module Client
5
+ module ResponseParsers
6
+ # Responsible for parsing a Cogitate response and just getting the basic data
7
+ module BasicExtractor
8
+ def self.call(response:)
9
+ JSON.parse(response).fetch('data')
10
+ end
11
+ end
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,17 @@
1
+ require 'json'
2
+ module Cogitate
3
+ module Client
4
+ module ResponseParsers
5
+ # Responsible for parsing a Cogitate response with a focus on getting emails
6
+ module EmailExtractor
7
+ def self.call(response:)
8
+ data = JSON.parse(response).fetch('data')
9
+ data.each_with_object({}) do |datum, mem|
10
+ mem[datum.fetch('id')] = datum.fetch('attributes').fetch('emails')
11
+ mem
12
+ end
13
+ end
14
+ end
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,43 @@
1
+ require 'cogitate/interfaces'
2
+ require 'cogitate/models/agent/with_token'
3
+
4
+ module Cogitate
5
+ module Client
6
+ # Responsible for converting a ticket into an agent
7
+ class RetrieveAgentFromTicket
8
+ # @api public
9
+ def self.call(ticket:, **keywords)
10
+ new(ticket: ticket, **keywords).call
11
+ end
12
+
13
+ def initialize(ticket:, ticket_coercer: default_ticket_coercer, token_coercer: default_token_coercer)
14
+ self.ticket = ticket
15
+ self.ticket_coercer = ticket_coercer
16
+ self.token_coercer = token_coercer
17
+ end
18
+
19
+ include Contracts
20
+ Contract(Contracts::None => Cogitate::Interfaces::AgentWithTokenInterface)
21
+ def call
22
+ token = ticket_coercer.call(ticket: ticket)
23
+ agent = token_coercer.call(token: token)
24
+ Cogitate::Models::Agent::WithToken.new(token: token, agent: agent)
25
+ end
26
+
27
+ private
28
+
29
+ attr_accessor :ticket, :ticket_coercer, :token_coercer
30
+
31
+ def default_ticket_coercer
32
+ # Responsible for issuing a request back to the Cogitate Server and reading the body
33
+ require 'cogitate/client/ticket_to_token_coercer' unless defined?(TicketToTokenCoercer)
34
+ TicketToTokenCoercer
35
+ end
36
+
37
+ def default_token_coercer
38
+ require 'cogitate/client/token_to_object_coercer' unless defined?(TokenToObjectCoercer)
39
+ TokenToObjectCoercer
40
+ end
41
+ end
42
+ end
43
+ end
@@ -0,0 +1,35 @@
1
+ require 'rest-client'
2
+ module Cogitate
3
+ module Client
4
+ # Responsible for converting a ticket into a token by leveraging a remote call to a Cogitate server.
5
+ class TicketToTokenCoercer
6
+ # @api public
7
+ # @param ticket [String] A ticket issued by a Cogitate server
8
+ # @return token [String] An encoded token
9
+ #
10
+ # @see Cogitate::Services::UriSafeTicketForIdentifierCreator
11
+ def self.call(ticket:, **keywords)
12
+ new(ticket: ticket, **keywords).call
13
+ end
14
+
15
+ def initialize(ticket:, configuration: default_configuration)
16
+ self.ticket = ticket
17
+ self.configuration = configuration
18
+ end
19
+
20
+ def call
21
+ response = RestClient.get(configuration.url_for_claiming_a_ticket, params: { ticket: ticket })
22
+ response.body
23
+ end
24
+
25
+ private
26
+
27
+ attr_accessor :ticket, :configuration
28
+
29
+ def default_configuration
30
+ require 'cogitate'
31
+ Cogitate.configuration
32
+ end
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,38 @@
1
+ module Cogitate
2
+ module Client
3
+ # Responsible for decoding a Cogitate token into an object (or objects)
4
+ #
5
+ # @see Cogitate::Services::Tokenizer for how the tokens get encoded and decoded
6
+ class TokenToObjectCoercer
7
+ # @api public
8
+ def self.call(token:, **keywords)
9
+ new(token: token, **keywords).call
10
+ end
11
+
12
+ def initialize(token:, token_to_data_coercer: default_token_to_data_coercer, data_to_object_coercer: default_data_to_object_coercer)
13
+ self.token = token
14
+ self.token_to_data_coercer = token_to_data_coercer
15
+ self.data_to_object_coercer = data_to_object_coercer
16
+ end
17
+
18
+ def call
19
+ data = token_to_data_coercer.call(token: token)
20
+ data_to_object_coercer.call(data)
21
+ end
22
+
23
+ private
24
+
25
+ attr_accessor :token, :token_to_data_coercer, :data_to_object_coercer
26
+
27
+ def default_token_to_data_coercer
28
+ require 'cogitate/services/tokenizer' unless defined?(Cogitate::Services::Tokenizer)
29
+ Cogitate::Services::Tokenizer.method(:from_token)
30
+ end
31
+
32
+ def default_data_to_object_coercer
33
+ require 'cogitate/client/data_to_object_coercer' unless defined?(Cogitate::Client::DataToObjectCoercer)
34
+ Cogitate::Client::DataToObjectCoercer
35
+ end
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,99 @@
1
+ module Cogitate
2
+ # Responsible for containing the configuration information for Cogitate
3
+ class Configuration
4
+ # If something is not properly configured
5
+ class ConfigurationError < RuntimeError
6
+ def initialize(method_name)
7
+ super("Cogitate::Configuration##{method_name} has not been set")
8
+ end
9
+ end
10
+
11
+ CONFIG_ATTRIBUTE_NAMES = [
12
+ # !@attribute [rw] after_authentication_callback_url
13
+ # Where should the Cogitate server redirect to after a successful authentication?
14
+ # @note Cogitate::Client configuration (and not Cogitate::Server)
15
+ :after_authentication_callback_url,
16
+ # !@attribute [rw] remote_server_base_url
17
+ # @note Cogitate::Client configuration
18
+ # @return [String] What is the URL of the Cogitate server you want to connect to?
19
+ :remote_server_base_url,
20
+ # !@attribute [rw] tokenizer_password
21
+ # What is the tokenizer password you are going to be using to:
22
+ # * create a token (i.e. a private RSA key)
23
+ # * decode a token (i.e. a public RSA key)
24
+ #
25
+ # If you are implemnting a Cogitate client, you'll want to use the public key
26
+ # @return [String]
27
+ # @note Cogitate::Client and Cogitate::Server configuration
28
+ # @see Cogitate::Services::Tokenizer
29
+ :tokenizer_password,
30
+ # !@attribute [rw] tokenizer_encryption_type
31
+ # What is the encryption type for the tokenizer. You will need to ensure that
32
+ # the Cogitate server and client are using the same encryption mechanism.
33
+ # @return [String]
34
+ # @note Cogitate::Client and Cogitate::Server configuration
35
+ # @example `configuration.tokenizer_encryption_type = 'RS256'`
36
+ # @see Cogitate::Services::Tokenizer
37
+ :tokenizer_encryption_type,
38
+ # !@attribute [rw] tokenizer_issuer_claim
39
+ # As per JSON Web Token specification, what is the Issuer Claim
40
+ # the Cogitate server and client are using the same encryption mechanism.
41
+ #
42
+ # @note Cogitate::Client and Cogitate::Server configuration
43
+ # @return [String]
44
+ # @example `configuration.tokenizer_issuer_claim = 'https://library.nd.edu'`
45
+ # @see https://tools.ietf.org/html/rfc7519#section-4.1.1
46
+ # @see https://github.com/jwt/ruby-jwt#issuer-claim
47
+ :tokenizer_issuer_claim
48
+ ].freeze
49
+
50
+ def initialize(client_request_handler: default_client_request_handler, **keywords)
51
+ CONFIG_ATTRIBUTE_NAMES.each do |name|
52
+ send("#{name}=", keywords.fetch(name)) if keywords.key?(name)
53
+ end
54
+ self.client_request_handler = client_request_handler
55
+ end
56
+
57
+ CONFIG_ATTRIBUTE_NAMES.each do |method_name|
58
+ attr_writer method_name
59
+ define_method(method_name) do
60
+ instance_variable_get("@#{method_name}") || fail(ConfigurationError, method_name)
61
+ end
62
+ end
63
+
64
+ # What is the authentication URL of the client's configured Cogitate server
65
+ # @note Cogitate::Client configuration (and not Cogitate::Server)
66
+ # @return String
67
+ def url_for_authentication
68
+ query_params = "?after_authentication_callback_url=#{CGI.escape(after_authentication_callback_url)}"
69
+ File.join(remote_server_base_url, '/authenticate') << query_params
70
+ end
71
+
72
+ # What is the URL for claiming a ticket
73
+ # @note Cogitate::Client configuration (and not Cogitate::Server)
74
+ # @return String
75
+ def url_for_claiming_a_ticket
76
+ File.join(remote_server_base_url, '/claim')
77
+ end
78
+
79
+ # What is the URL for retrieving the agents based on the given identifiers
80
+ # @note Cogitate::Client configuration (and not Cogitate::Server)
81
+ # @param urlsafe_base64_encoded_identifiers [String]
82
+ # @return String
83
+ def url_for_retrieving_agents_for(urlsafe_base64_encoded_identifiers:)
84
+ File.join(remote_server_base_url, '/api/agents', urlsafe_base64_encoded_identifiers)
85
+ end
86
+
87
+ # What will be negotiating the remote request to the Cogitate::Server
88
+ #
89
+ # @return [#call(url:)]
90
+ # @see #default_client_request_handler for interface
91
+ attr_accessor :client_request_handler
92
+
93
+ private
94
+
95
+ def default_client_request_handler
96
+ -> (url:) { RestClient.get(url).body }
97
+ end
98
+ end
99
+ end
@@ -0,0 +1,25 @@
1
+ module Cogitate
2
+ # When the thing we are trying to decode is improperly encoded, this exception is to provide clarity
3
+ class InvalidIdentifierEncoding < ArgumentError
4
+ def initialize(encoded_string:)
5
+ super("Unable to decode #{encoded_string}; Expected it to be URL-safe Base64 encoded (use Base64.urlsafe_encode64)")
6
+ end
7
+ end
8
+
9
+ # When the thing we have decoded is not properly formated, this exception is to provide clarity
10
+ class InvalidIdentifierFormat < RuntimeError
11
+ EXPECTED_FORMAT = "strategy\tvalue\nstrategy\tvalue".freeze
12
+ def initialize(decoded_string:)
13
+ super("Expected #{decoded_string.inspect} to be of the format #{EXPECTED_FORMAT.inspect}")
14
+ end
15
+ end
16
+
17
+ # When the thing we have decoded is not properly formated, this exception is to provide clarity
18
+ class InvalidMembershipVisitationKeys < RuntimeError
19
+ def initialize(identifier:, visitation_type:)
20
+ super(
21
+ "Unable to find membership visitation service for visitation_type: #{visitation_type.inspect}, identifier: #{identifier.inspect}"
22
+ )
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,10 @@
1
+ module Commitment
2
+ # :nodoc:
3
+ class InstallGenerator < Rails::Generators::Base
4
+ source_root(File.expand_path("../templates", __FILE__))
5
+
6
+ def create_cogitate_initializer
7
+ template('cogitate_initializer.rb.erb', 'config/initializers/cogitate_initializer.rb')
8
+ end
9
+ end
10
+ end
@@ -0,0 +1,17 @@
1
+ Cogitate.configure do |config|
2
+ <% if defined?(Figaro) %>
3
+ config.tokenizer_password = Figaro.env.cogitate_services_tokenizer_public_password!
4
+ config.remote_server_base_url = Figaro.env.cogitate_services_remote_server_base_url!
5
+ config.tokenizer_encryption_type = Figaro.env.cogitate_services_tokenizer_encryption_type!
6
+ config.tokenizer_issuer_claim = Figaro.env.cogitate_services_tokenizer_issuer_claim!
7
+ config.after_authentication_callback_url = Figaro.env.cogitate_services_after_authentication_callback_url!
8
+ <% else %>
9
+ # The public key for the RSA key
10
+ config.tokenizer_password = 'You need to provide this'
11
+ config.tokenizer_encryption_type = 'RS256'
12
+ config.tokenizer_issuer_claim = 'CHANGE THIS: Cogitate Client'
13
+ # Change this
14
+ config.remote_server_base_url = "http://localhost:3001"
15
+ config.after_authentication_callback_url = "http://localhost:3000/authenticate/from/cogitate"
16
+ <% end %>
17
+ end
@@ -0,0 +1,40 @@
1
+ require 'contracts'
2
+
3
+ module Cogitate
4
+ # Herein lies the Cogitate namespace
5
+ module Interfaces
6
+ include Contracts
7
+ IdentifierInterface = RespondTo[:strategy, :identifying_value, :<=>, :name, :id, :encoded_id]
8
+ IdentifierCollectionInterface = Contracts::ArrayOf[Cogitate::Interfaces::IdentifierInterface]
9
+
10
+ AgentInterface = RespondTo[
11
+ :with_identifiers, :with_verified_identifiers, :with_emails, :add_identifier, :add_verified_identifier, :add_email, :ids, :name
12
+ ]
13
+ AgentCollectionInterface = Contracts::ArrayOf[Cogitate::Interfaces::AgentInterface]
14
+ AgentWithTokenInterface = And[AgentInterface, RespondTo[:to_token]]
15
+
16
+ VisitorInterface = RespondTo[:visit]
17
+ VisitorV2Interface = And[VisitorInterface, RespondTo[:return_from_visitations]]
18
+ AgentCollectorInitializationInterface = KeywordArgs[visitor: VisitorInterface, agent: Optional[AgentInterface]]
19
+
20
+ # All identifiers must be comparable, otherwise we could spiral into an endless visitation of related identifiers
21
+ VerifiedIdentifierInterface = IdentifierInterface
22
+
23
+ AuthenticationVectorNetidInterface = And[
24
+ RespondTo[:first_name, :last_name, :netid, :full_name, :ndguid, :strategy, :identifying_value],
25
+ IdentifierInterface
26
+ ]
27
+
28
+ HostInterface = RespondTo[:invite]
29
+
30
+ IdentifierInitializationInterface = KeywordArgs[strategy: RespondTo[:to_s], identifying_value: Any]
31
+ IdentifierBuilderInterface = Func[IdentifierInitializationInterface]
32
+
33
+ FindNetidRepositoryInterface = RespondTo[:find]
34
+
35
+ MembershipVisitationStrategyInterface = RespondTo[:call]
36
+
37
+ VerifiableIdentifierInterface = And[IdentifierInterface, RespondTo[:verified?]]
38
+ VerifiedGroupInterface = And[VerifiableIdentifierInterface, RespondTo[:name, :description]]
39
+ end
40
+ end