keycard 0.1.2 → 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 5ba441d104509e55f853e1003f6dd2d8c39f362e5e93ee619b0dd9378f8c13b8
4
- data.tar.gz: 0d503c7e8c4682348e2c6d1ba9d9d0425fdf61429a278882368bad5cb19c4823
3
+ metadata.gz: 99719e6ba14e97ad966b5a53b7e114f3eb6c6bc02aa674a1654f6c7f151ad793
4
+ data.tar.gz: 7ee5e247aab91665c1c2fcc394e6f7f535f2382af500ec723f185b130a0258ed
5
5
  SHA512:
6
- metadata.gz: 578ff3eb3e114a3060b0236f6c471fabf45e947b1f2184828f68206eab35071bfa182482cfbab4e9a1750a48e2e127d1aa4e4feda050a11e53c1937ad0778837
7
- data.tar.gz: a268b75e7207d0de7626b8f675567eddce0c070158343e2f55d424c6868c06837bfcd03c0be68a3ff8b1edc42aa5798df7d3b59cbadb5b00bea1173a0e855e6d
6
+ metadata.gz: ddc60f9e41240190b7143c86316de2234abc6cb034b12a39ed9c655c4595a0c1fcc572ddafc6f1ac39f0fac51264b9be7a05b9458452a5306506a87eb2abfbf0
7
+ data.tar.gz: 658684fbbc2ede87b6fb6ee85dd10e91d39537f8bc5435584df5e4152bcf28346d7ad6a25ee3d5eb5a9f30b2efa519ea183f9ccf95acd4d25cfc298a2b43d03e
data/.rubocop.yml CHANGED
@@ -26,5 +26,11 @@ Metrics/BlockLength:
26
26
  - '*.gemspec'
27
27
  ExcludedMethods: ['describe', 'context']
28
28
 
29
+ Naming/UncommunicativeMethodParamName:
30
+ AllowedNames: ['db']
31
+
29
32
  Style/StringLiterals:
30
33
  Enabled: false
34
+
35
+ Style/ClassAndModuleChildren:
36
+ EnforcedStyle: compact
data/keycard.gemspec CHANGED
@@ -1,7 +1,7 @@
1
1
 
2
2
  # frozen_string_literal: true
3
3
 
4
- lib = File.expand_path("../lib", __FILE__)
4
+ lib = File.expand_path('lib', __dir__)
5
5
  $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
6
6
  require "keycard/version"
7
7
 
data/lib/keycard/db.rb CHANGED
@@ -4,159 +4,157 @@ require 'ostruct'
4
4
  require 'logger'
5
5
  require 'yaml'
6
6
 
7
- module Keycard
8
- # Module for database interactions for Keycard.
9
- module DB
10
- # Any error with the database that Keycard itself detects but cannot handle.
11
- class DatabaseError < StandardError; end
12
-
13
- CONNECTION_ERROR = 'The Keycard database is not initialized. Call initialize! first.'
14
-
15
- ALREADY_CONNECTED = 'Already connected; refusing to connect to another database.'
16
-
17
- MISSING_CONFIG = <<~MSG
18
- KEYCARD_DATABASE_URL and DATABASE_URL are both missing and a connection
19
- has not been configured. Cannot connect to the Keycard database.
20
- See Keycard::DB.connect! for help.
21
- MSG
22
-
23
- LOAD_ERROR = <<~MSG
24
- Error loading Keycard database models.
25
- Verify connection information and that the database is migrated.
26
- MSG
27
-
28
- SCHEMA_HEADER = "# Keycard Database Version\n"
29
-
30
- class << self
31
- # Initialize Keycard
32
- #
33
- # This connects to the database if it has not already happened and
34
- # requires all of the Keycard model classes. It is required to do the
35
- # connection setup first because of the design decision in Sequel that
36
- # the schema is examined at the time of extending Sequel::Model.
37
- def initialize!
38
- connect! unless connected?
39
- begin
40
- model_files.each do |file|
41
- require_relative file
42
- end
43
- rescue Sequel::DatabaseError, NoMethodError => e
44
- raise DatabaseError, LOAD_ERROR + "\n" + e.message
7
+ # Module for database interactions for Keycard.
8
+ module Keycard::DB
9
+ # Any error with the database that Keycard itself detects but cannot handle.
10
+ class DatabaseError < StandardError; end
11
+
12
+ CONNECTION_ERROR = 'The Keycard database is not initialized. Call initialize! first.'
13
+
14
+ ALREADY_CONNECTED = 'Already connected; refusing to connect to another database.'
15
+
16
+ MISSING_CONFIG = <<~MSG
17
+ KEYCARD_DATABASE_URL and DATABASE_URL are both missing and a connection
18
+ has not been configured. Cannot connect to the Keycard database.
19
+ See Keycard::DB.connect! for help.
20
+ MSG
21
+
22
+ LOAD_ERROR = <<~MSG
23
+ Error loading Keycard database models.
24
+ Verify connection information and that the database is migrated.
25
+ MSG
26
+
27
+ SCHEMA_HEADER = "# Keycard Database Version\n"
28
+
29
+ class << self
30
+ # Initialize Keycard
31
+ #
32
+ # This connects to the database if it has not already happened and
33
+ # requires all of the Keycard model classes. It is required to do the
34
+ # connection setup first because of the design decision in Sequel that
35
+ # the schema is examined at the time of extending Sequel::Model.
36
+ def initialize!
37
+ connect! unless connected?
38
+ begin
39
+ model_files.each do |file|
40
+ require_relative file
45
41
  end
46
- db
42
+ rescue Sequel::DatabaseError, NoMethodError => e
43
+ raise DatabaseError, LOAD_ERROR + "\n" + e.message
47
44
  end
45
+ db
46
+ end
48
47
 
49
- # Connect to the Keycard database.
50
- #
51
- # The default is to use the settings under {.config}, but can be
52
- # supplied here (and they will be merged into config as a side effect).
53
- # The keys that will be used from either source are documented here as
54
- # the options.
55
- #
56
- # Only one "mode" will be used; the first of these supplied will take
57
- # precedence:
58
- #
59
- # 1. An already-connected {Sequel::Database} object
60
- # 2. A connection string
61
- # 3. A connection options hash
62
- #
63
- # While Keycard serves as a singleton, this will raise a DatabaseError
64
- # if already connected. Check `connected?` if you are unsure.
65
- #
66
- # @see {Sequel.connect}
67
- # @param [Hash] config Optional connection config
68
- # @option config [String] :url A Sequel database URL
69
- # @option config [Hash] :opts A set of connection options
70
- # @option config [Sequel::Database] :db An already-connected database;
71
- # @return [Sequel::Database] The initialized database connection
72
- def connect!(config = {})
73
- raise DatabaseError, ALREADY_CONNECTED if connected?
74
- merge_config!(config)
75
- raise DatabaseError, MISSING_CONFIG if self.config.db.nil? && conn_opts.empty?
76
-
77
- # We splat here because we might give one or two arguments depending
78
- # on whether we have a string or not; to add our logger regardless.
79
- @db = self.config.db || Sequel.connect(*conn_opts)
80
- end
48
+ # Connect to the Keycard database.
49
+ #
50
+ # The default is to use the settings under {.config}, but can be
51
+ # supplied here (and they will be merged into config as a side effect).
52
+ # The keys that will be used from either source are documented here as
53
+ # the options.
54
+ #
55
+ # Only one "mode" will be used; the first of these supplied will take
56
+ # precedence:
57
+ #
58
+ # 1. An already-connected {Sequel::Database} object
59
+ # 2. A connection string
60
+ # 3. A connection options hash
61
+ #
62
+ # While Keycard serves as a singleton, this will raise a DatabaseError
63
+ # if already connected. Check `connected?` if you are unsure.
64
+ #
65
+ # @see {Sequel.connect}
66
+ # @param [Hash] config Optional connection config
67
+ # @option config [String] :url A Sequel database URL
68
+ # @option config [Hash] :opts A set of connection options
69
+ # @option config [Sequel::Database] :db An already-connected database;
70
+ # @return [Sequel::Database] The initialized database connection
71
+ def connect!(config = {})
72
+ raise DatabaseError, ALREADY_CONNECTED if connected?
73
+ merge_config!(config)
74
+ raise DatabaseError, MISSING_CONFIG if self.config.db.nil? && conn_opts.empty?
75
+
76
+ # We splat here because we might give one or two arguments depending
77
+ # on whether we have a string or not; to add our logger regardless.
78
+ @db = self.config.db || Sequel.connect(*conn_opts)
79
+ end
81
80
 
82
- # Run any pending migrations.
83
- # This will connect with the current config if not already conencted.
84
- def migrate!
85
- connect! unless connected?
86
- unless config.readonly
87
- Sequel.extension :migration
88
- Sequel::Migrator.run(db, File.join(__dir__, '../../db/migrations'), table: schema_table)
89
- end
90
- end
81
+ # Run any pending migrations.
82
+ # This will connect with the current config if not already conencted.
83
+ def migrate!
84
+ connect! unless connected?
85
+ return if config.readonly
91
86
 
92
- def schema_table
93
- :keycard_schema
94
- end
87
+ Sequel.extension :migration
88
+ Sequel::Migrator.run(db, File.join(__dir__, '../../db/migrations'), table: schema_table)
89
+ end
95
90
 
96
- def schema_file
97
- 'db/keycard.yml'
98
- end
91
+ def schema_table
92
+ :keycard_schema
93
+ end
99
94
 
100
- def dump_schema!
101
- connect! unless connected?
102
- version = db[schema_table].first.to_yaml
103
- File.write(schema_file, SCHEMA_HEADER + version)
104
- end
95
+ def schema_file
96
+ 'db/keycard.yml'
97
+ end
105
98
 
106
- def load_schema!
107
- connect! unless connected?
108
- version = YAML.load_file(schema_file)[:version]
109
- db[schema_table].delete
110
- db[schema_table].insert(version: version)
111
- end
99
+ def dump_schema!
100
+ connect! unless connected?
101
+ version = db[schema_table].first.to_yaml
102
+ File.write(schema_file, SCHEMA_HEADER + version)
103
+ end
112
104
 
113
- def model_files
114
- []
115
- end
105
+ def load_schema!
106
+ connect! unless connected?
107
+ version = YAML.load_file(schema_file)[:version]
108
+ db[schema_table].delete
109
+ db[schema_table].insert(version: version)
110
+ end
116
111
 
117
- # Merge url, opts, or db settings from a hash into our config
118
- def merge_config!(config = {})
119
- self.config.url = config[:url] if config.key?(:url)
120
- self.config.opts = config[:opts] if config.key?(:opts)
121
- self.config.db = config[:db] if config.key?(:db)
122
- end
112
+ def model_files
113
+ []
114
+ end
123
115
 
124
- def conn_opts
125
- log = { logger: Logger.new('db/keycard.log') }
126
- url = config.url
127
- opts = config.opts
128
- if url
129
- [url, log]
130
- elsif opts
131
- [log.merge(opts)]
132
- else
133
- []
134
- end
135
- end
116
+ # Merge url, opts, or db settings from a hash into our config
117
+ def merge_config!(config = {})
118
+ self.config.url = config[:url] if config.key?(:url)
119
+ self.config.opts = config[:opts] if config.key?(:opts)
120
+ self.config.db = config[:db] if config.key?(:db)
121
+ end
136
122
 
137
- def config
138
- @config ||= OpenStruct.new(
139
- url: ENV['KEYCARD_DATABASE_URL'] || ENV['DATABASE_URL'],
140
- readonly: false
141
- )
123
+ def conn_opts
124
+ log = { logger: Logger.new('db/keycard.log') }
125
+ url = config.url
126
+ opts = config.opts
127
+ if url
128
+ [url, log]
129
+ elsif opts
130
+ [log.merge(opts)]
131
+ else
132
+ []
142
133
  end
134
+ end
143
135
 
144
- def connected?
145
- !@db.nil?
146
- end
136
+ def config
137
+ @config ||= OpenStruct.new(
138
+ url: ENV['KEYCARD_DATABASE_URL'] || ENV['DATABASE_URL'],
139
+ readonly: false
140
+ )
141
+ end
147
142
 
148
- # The Keycard database
149
- # @return [Sequel::Database] The connected database; be sure to call initialize! first.
150
- def db
151
- raise DatabaseError, CONNECTION_ERROR unless connected?
152
- @db
153
- end
143
+ def connected?
144
+ !@db.nil?
145
+ end
154
146
 
155
- # Forward the Sequel::Database []-syntax down to db for convenience.
156
- # Everything else must be called on db directly, but this is nice sugar.
157
- def [](*args)
158
- db[*args]
159
- end
147
+ # The Keycard database
148
+ # @return [Sequel::Database] The connected database; be sure to call initialize! first.
149
+ def db
150
+ raise DatabaseError, CONNECTION_ERROR unless connected?
151
+ @db
152
+ end
153
+
154
+ # Forward the Sequel::Database []-syntax down to db for convenience.
155
+ # Everything else must be called on db directly, but this is nice sugar.
156
+ def [](*args)
157
+ db[*args]
160
158
  end
161
159
  end
162
160
  end
@@ -2,57 +2,61 @@
2
2
 
3
3
  require 'ipaddr'
4
4
 
5
- module Keycard
6
- # looks up institution ID(s) by IP address
7
- class InstitutionFinder
8
- INST_QUERY = <<~SQL
9
- SELECT inst FROM aa_network WHERE
10
- ? >= dlpsAddressStart
11
- AND ? <= dlpsAddressEnd
12
- AND dlpsAccessSwitch = 'allow'
13
- AND dlpsDeleted = 'f'
14
- AND inst is not null
15
- AND inst NOT IN
16
- ( SELECT inst FROM aa_network WHERE
17
- ? >= dlpsAddressStart
18
- AND ? <= dlpsAddressEnd
19
- AND dlpsAccessSwitch = 'deny'
20
- AND dlpsDeleted = 'f' )
21
- SQL
22
-
23
- def initialize(db: Keycard::DB.db)
24
- @db = db
25
- @stmt = @db[INST_QUERY, *[:$client_ip] * 4].prepare(:select, :unused)
26
- end
5
+ # looks up institution ID(s) by IP address
6
+ class Keycard::InstitutionFinder
7
+ IDENTITY_ATTRS = %i[dlpsInstitutionId].freeze
8
+
9
+ INST_QUERY = <<~SQL
10
+ SELECT inst FROM aa_network WHERE
11
+ ? >= dlpsAddressStart
12
+ AND ? <= dlpsAddressEnd
13
+ AND dlpsAccessSwitch = 'allow'
14
+ AND dlpsDeleted = 'f'
15
+ AND inst is not null
16
+ AND inst NOT IN
17
+ ( SELECT inst FROM aa_network WHERE
18
+ ? >= dlpsAddressStart
19
+ AND ? <= dlpsAddressEnd
20
+ AND dlpsAccessSwitch = 'deny'
21
+ AND dlpsDeleted = 'f' )
22
+ SQL
23
+
24
+ def initialize(db: Keycard::DB.db)
25
+ @db = db
26
+ @stmt = @db[INST_QUERY, *[:$client_ip] * 4].prepare(:select, :unused)
27
+ end
28
+
29
+ def identity_keys
30
+ IDENTITY_ATTRS
31
+ end
27
32
 
28
- def attributes_for(request)
29
- return {} unless (numeric_ip = numeric_ip(request.client_ip))
33
+ def attributes_for(request)
34
+ return {} unless (numeric_ip = numeric_ip(request.client_ip))
30
35
 
31
- insts = insts_for_ip(numeric_ip)
36
+ insts = insts_for_ip(numeric_ip)
32
37
 
33
- if !insts.empty?
34
- { 'dlpsInstitutionId' => insts }
35
- else
36
- {}
37
- end
38
+ if !insts.empty?
39
+ { dlpsInstitutionId: insts }
40
+ else
41
+ {}
38
42
  end
43
+ end
39
44
 
40
- private
45
+ private
41
46
 
42
- attr_reader :stmt
47
+ attr_reader :stmt
43
48
 
44
- def insts_for_ip(numeric_ip)
45
- stmt.call(client_ip: numeric_ip).map { |row| row[:inst] }
46
- end
49
+ def insts_for_ip(numeric_ip)
50
+ stmt.call(client_ip: numeric_ip).map { |row| row[:inst] }
51
+ end
47
52
 
48
- def numeric_ip(dotted_ip)
49
- return unless dotted_ip
53
+ def numeric_ip(dotted_ip)
54
+ return unless dotted_ip
50
55
 
51
- begin
52
- IPAddr.new(dotted_ip).to_i
53
- rescue IPAddr::InvalidAddressError
54
- nil
55
- end
56
+ begin
57
+ IPAddr.new(dotted_ip).to_i
58
+ rescue IPAddr::InvalidAddressError
59
+ nil
56
60
  end
57
61
  end
58
62
  end
@@ -1,108 +1,106 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- module Keycard
4
- # Railtie to hook Keycard into Rails applications.
5
- #
6
- # This does three things at present:
7
- #
8
- # 1. Loads our rake tasks, so you can run keycard:migrate from the app.
9
- # 2. Pulls the Rails database information off of the ActiveRecord
10
- # connection and puts it on Keycard::DB.config before any application
11
- # initializers are run.
12
- # 3. Sets up the Keycard database connection after application
13
- # initializers have run, if it has not already been done and we are not
14
- # running as a Rake task. This condition is key because when we are in
15
- # rails server or console, we want to initialize!, but when we are in
16
- # a rake task to update the database, we have to let it connect, but
17
- # not initialize.
18
- class Railtie < Rails::Railtie
19
- railtie_name :keycard
20
-
21
- class << self
22
- # Register a callback to run before anything in 'config/initializers' runs.
23
- # The block will get a reference to Keycard::DB.config as its only parameter.
24
- def before_initializers(&block)
25
- before_blocks << block
26
- end
3
+ # Railtie to hook Keycard into Rails applications.
4
+ #
5
+ # This does three things at present:
6
+ #
7
+ # 1. Loads our rake tasks, so you can run keycard:migrate from the app.
8
+ # 2. Pulls the Rails database information off of the ActiveRecord
9
+ # connection and puts it on Keycard::DB.config before any application
10
+ # initializers are run.
11
+ # 3. Sets up the Keycard database connection after application
12
+ # initializers have run, if it has not already been done and we are not
13
+ # running as a Rake task. This condition is key because when we are in
14
+ # rails server or console, we want to initialize!, but when we are in
15
+ # a rake task to update the database, we have to let it connect, but
16
+ # not initialize.
17
+ class Keycard::Railtie < Rails::Railtie
18
+ railtie_name :keycard
19
+
20
+ class << self
21
+ # Register a callback to run before anything in 'config/initializers' runs.
22
+ # The block will get a reference to Keycard::DB.config as its only parameter.
23
+ def before_initializers(&block)
24
+ before_blocks << block
25
+ end
27
26
 
28
- # Register a callback to run after anything in 'config/initializers' runs.
29
- # The block will get a reference to Keycard::DB.config as its only parameter.
30
- # Keycard::DB.initialize! will not have been automatically called at this
31
- # point, so this is an opportunity to do so if an initializer has not.
32
- def after_initializers(&block)
33
- after_blocks << block
34
- end
27
+ # Register a callback to run after anything in 'config/initializers' runs.
28
+ # The block will get a reference to Keycard::DB.config as its only parameter.
29
+ # Keycard::DB.initialize! will not have been automatically called at this
30
+ # point, so this is an opportunity to do so if an initializer has not.
31
+ def after_initializers(&block)
32
+ after_blocks << block
33
+ end
35
34
 
36
- # Register a callback to run when Keycard is ready and fully initialized.
37
- # This will happen once in production, and on each request in development.
38
- # If you need to do something once in development, you can choose between
39
- # keeping a flag or using the after_initializers.
40
- def when_keycard_is_ready(&block)
41
- ready_blocks << block
42
- end
35
+ # Register a callback to run when Keycard is ready and fully initialized.
36
+ # This will happen once in production, and on each request in development.
37
+ # If you need to do something once in development, you can choose between
38
+ # keeping a flag or using the after_initializers.
39
+ def when_keycard_is_ready(&block)
40
+ ready_blocks << block
41
+ end
43
42
 
44
- def before_blocks
45
- @before ||= []
46
- end
43
+ def before_blocks
44
+ @before_blocks ||= []
45
+ end
47
46
 
48
- def after_blocks
49
- @after ||= []
50
- end
47
+ def after_blocks
48
+ @after_blocks ||= []
49
+ end
51
50
 
52
- def ready_blocks
53
- @ready ||= []
54
- end
51
+ def ready_blocks
52
+ @ready_blocks ||= []
53
+ end
55
54
 
56
- def under_rake!
57
- @rake = true
58
- end
55
+ def under_rake!
56
+ @under_rake = true
57
+ end
59
58
 
60
- def under_rake?
61
- @rake ||= false
62
- end
59
+ def under_rake?
60
+ @under_rake ||= false
63
61
  end
62
+ end
64
63
 
65
- # This runs before anything in 'config/initializers' runs.
66
- initializer "keycard.before_initializers", before: :load_config_initializers do
67
- config = Keycard::DB.config
68
- unless config.url
69
- case Rails.env
70
- when "development"
71
- config[:opts] = { adapter: 'sqlite', database: "db/keycard_development.sqlite3" }
72
- when "test"
73
- config[:opts] = { adapter: 'sqlite' }
74
- else
75
- raise "Keycard::DB.config must be configured"
76
- end
64
+ # This runs before anything in 'config/initializers' runs.
65
+ initializer "keycard.before_initializers", before: :load_config_initializers do
66
+ config = Keycard::DB.config
67
+ unless config.url
68
+ case Rails.env
69
+ when "development"
70
+ config[:opts] = { adapter: 'sqlite', database: "db/keycard_development.sqlite3" }
71
+ when "test"
72
+ config[:opts] = { adapter: 'sqlite' }
73
+ else
74
+ raise "Keycard::DB.config must be configured"
77
75
  end
76
+ end
78
77
 
79
- Railtie.before_blocks.each do |block|
80
- block.call(config.to_h)
81
- end
78
+ Railtie.before_blocks.each do |block|
79
+ block.call(config.to_h)
82
80
  end
81
+ end
83
82
 
84
- # This runs after everything in 'config/initializers' runs.
85
- initializer "keycard.after_initializers", after: :load_config_initializers do
86
- config = Keycard::DB.config
87
- Railtie.after_blocks.each do |block|
88
- block.call(config.to_h)
89
- end
83
+ # This runs after everything in 'config/initializers' runs.
84
+ initializer "keycard.after_initializers", after: :load_config_initializers do
85
+ config = Keycard::DB.config
86
+ Railtie.after_blocks.each do |block|
87
+ block.call(config.to_h)
88
+ end
90
89
 
91
- Keycard::DB.initialize! unless Railtie.under_rake?
90
+ Keycard::DB.initialize! unless Railtie.under_rake?
92
91
 
93
- Railtie.ready_blocks.each do |block|
94
- block.call(Keycard::DB.db)
95
- end
92
+ Railtie.ready_blocks.each do |block|
93
+ block.call(Keycard::DB.db)
96
94
  end
95
+ end
97
96
 
98
- def rake_files
99
- base = Pathname(__dir__) + '../tasks/'
100
- [base + 'migrate.rake']
101
- end
97
+ def rake_files
98
+ base = Pathname(__dir__) + '../tasks/'
99
+ [base + 'migrate.rake']
100
+ end
102
101
 
103
- rake_tasks do
104
- Railtie.under_rake!
105
- rake_files.each { |file| load file }
106
- end
102
+ rake_tasks do
103
+ Railtie.under_rake!
104
+ rake_files.each { |file| load file }
107
105
  end
108
106
  end
@@ -0,0 +1,94 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Keycard::Request
4
+ # Base class for extracting attributes from a Rack request.
5
+ #
6
+ # This provides the interface for attribute extraction, independent of how
7
+ # the application is served and accessed. It is not intended to be used
8
+ # directly; you should use {AttributesFactory} to create an appropriate
9
+ # subclass based on configuration.
10
+ #
11
+ # The overall design is that a subclass will extract various attributes
12
+ # from the request headers and environment, and a set of attribute finders
13
+ # may be supplied to examine the base set and add additional attributes.
14
+ class Attributes
15
+ IDENTITY_ATTRS = %i[user_pid user_eid].freeze
16
+
17
+ def initialize(request, finders: [])
18
+ @request = request
19
+ @finders = finders
20
+ end
21
+
22
+ # The user's persistent identifier.
23
+ #
24
+ # If the client has authenticated as a user, this will be a peristent
25
+ # identifier suitable as a key for an application account. It is
26
+ # expressly opaque, meaning that it cannot be assumed to be resolvable
27
+ # to a person or be useful for display purposes. It can be relied on to
28
+ # be stable for the same person as the identity provider determines that.
29
+ def user_pid
30
+ nil
31
+ end
32
+
33
+ # The user's enterprise identifier.
34
+ #
35
+ # If the client has authenticated as a user and the identity provider has
36
+ # releases one, this will be the "enterprise" identifier, some string that
37
+ # is useful for display and resolving to a person. It will tend to be
38
+ # something recognizable to that person, such as a network ID or email
39
+ # address. It may be helpful for displaying who is logged in or looking
40
+ # up directory information, for example. It should not be assumed to be
41
+ # permanent; that is, the EID may change for a person (and PID), so this
42
+ # should not used as a database key, for example.
43
+ def user_eid
44
+ nil
45
+ end
46
+
47
+ # The user's IP address.
48
+ #
49
+ # This will be a string version of the IP address of the client, whether
50
+ # or not they have been proxied.
51
+ def client_ip
52
+ nil
53
+ end
54
+
55
+ # The set of base attributes for this request.
56
+ #
57
+ # Subclasses should implement user_pid, user_eid, and client_ip
58
+ # and include them in the hash under those keys.
59
+ def base
60
+ {}
61
+ end
62
+
63
+ def [](attr)
64
+ all[attr]
65
+ end
66
+
67
+ def all
68
+ base.merge!(external).delete_if { |_k, v| v.nil? || v == '' }
69
+ end
70
+
71
+ def external
72
+ finders
73
+ .map { |finder| finder.attributes_for(self) }
74
+ .reduce({}) { |hash, attrs| hash.merge!(attrs) }
75
+ end
76
+
77
+ def identity
78
+ all.select { |k, _v| identity_keys.include?(k.to_sym) }
79
+ end
80
+
81
+ def supplemental
82
+ all.reject { |k, _v| identity_keys.include?(k.to_sym) }
83
+ end
84
+
85
+ def identity_keys
86
+ @identity_keys ||= IDENTITY_ATTRS + finders.map(&:identity_keys).flatten
87
+ end
88
+
89
+ private
90
+
91
+ attr_reader :finders
92
+ attr_reader :request
93
+ end
94
+ end
@@ -0,0 +1,34 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Keycard::Request
4
+ # Factory to simplify creation of Attributes instances. It binds in a list
5
+ # of finders and inspects the Keycard.config.access mode to determine which
6
+ # subclass to use. You can register a factory instance as a service and then
7
+ # use .for instead of naming concrete classes when processing requests.
8
+ class AttributesFactory
9
+ MODE_MAP = {
10
+ direct: DirectAttributes,
11
+ proxy: ProxiedAttributes,
12
+ cosign: CosignAttributes,
13
+ shibboleth: ShibbolethAttributes
14
+ }.freeze
15
+
16
+ def initialize(finders: [InstitutionFinder.new])
17
+ @finders = finders
18
+ end
19
+
20
+ def for(request)
21
+ mode = MODE_MAP[Keycard.config.access.to_sym]
22
+ if mode.nil?
23
+ # TODO: Warn about this once to the appropriate log; probably in a config check, not here.
24
+ # puts "Keycard does not recognize the '#{access}' access mode, using 'direct'."
25
+ mode = DirectAttributes
26
+ end
27
+ mode.new(request, finders: finders)
28
+ end
29
+
30
+ private
31
+
32
+ attr_reader :finders
33
+ end
34
+ end
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Keycard::Request
4
+ # This class extracts attributes for Cosign-protected applications. It
5
+ # follows the same basic pattern as for general proxied requests; that is,
6
+ # the pid/eid are the same and there are currently no additional
7
+ # attributes extracted.
8
+ class CosignAttributes < Attributes
9
+ def base
10
+ {
11
+ user_pid: user_pid,
12
+ user_eid: user_eid,
13
+ client_ip: client_ip
14
+ }
15
+ end
16
+
17
+ def user_pid
18
+ request.env['HTTP_X_REMOTE_USER']
19
+ end
20
+
21
+ def user_eid
22
+ user_pid
23
+ end
24
+
25
+ def client_ip
26
+ (request.env['HTTP_X_FORWARDED_FOR'] || '').split(',').first
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,28 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Keycard::Request
4
+ # This class should be used to extract attributes when the application will
5
+ # serve HTTP requests directly or through a proxy that passes trusted
6
+ # values into the application environment to be accessed as usual.
7
+ class DirectAttributes < Attributes
8
+ def base
9
+ {
10
+ user_pid: user_pid,
11
+ user_eid: user_eid,
12
+ client_ip: client_ip
13
+ }
14
+ end
15
+
16
+ def user_pid
17
+ request.env['REMOTE_USER']
18
+ end
19
+
20
+ def user_eid
21
+ user_pid
22
+ end
23
+
24
+ def client_ip
25
+ (request.env['REMOTE_ADDR'] || '').split(',').first
26
+ end
27
+ end
28
+ end
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- module Keycard
3
+ module Keycard::Request
4
4
  # This request wrapper should be used when the application will be served
5
5
  # behind a reverse proxy. It relies on the trusted relationship with the
6
6
  # proxy to use HTTP headers for forwarded values.
@@ -8,17 +8,25 @@ module Keycard
8
8
  # The typical headers forwarded are X-Forwarded-User and X-Forwarded-For,
9
9
  # which, somewhat confusingly, are transposed into HTTP_X_REMOTE_USER and
10
10
  # HTTP_X_FORWARDED_FOR once the Rack request is assembled.
11
- class ProxiedRequest < SimpleDelegator
12
- def self.for(request)
13
- new(request)
11
+ class ProxiedAttributes < Attributes
12
+ def base
13
+ {
14
+ user_pid: user_pid,
15
+ user_eid: user_eid,
16
+ client_ip: client_ip
17
+ }
14
18
  end
15
19
 
16
- def username
17
- env['HTTP_X_REMOTE_USER'] || ''
20
+ def user_pid
21
+ request.env['HTTP_X_REMOTE_USER']
22
+ end
23
+
24
+ def user_eid
25
+ user_pid
18
26
  end
19
27
 
20
28
  def client_ip
21
- (env['HTTP_X_FORWARDED_FOR'] || '').split(',').first || ''
29
+ (request.env['HTTP_X_FORWARDED_FOR'] || '').split(',').first
22
30
  end
23
31
  end
24
32
  end
@@ -0,0 +1,86 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Keycard::Request
4
+ # This class extracts attributes for Shibboleth-enabled applications.
5
+ # It trusts specific HTTP headers, so the app must not be exposed to direct
6
+ # requests. The pid is typically a SAML2 Persistent NameID, which is very
7
+ # long and cumbersome. The presence of an eid depends on attribute release
8
+ # by the IdP, and will commonly be an eduPersonPrincipalName. The only two
9
+ # attributes guaranteed to have usable values are the client_ip, for all
10
+ # requests, and the user_pid, for requests from authenticated users.
11
+ class ShibbolethAttributes < Attributes
12
+ def base # rubocop:disable Metrics/MethodLength
13
+ {
14
+ user_pid: user_pid,
15
+ user_eid: user_eid,
16
+ client_ip: client_ip,
17
+ persistentNameID: persistent_id,
18
+ eduPersonPrincipalName: principal_name,
19
+ eduPersonScopedAffiliation: affiliation,
20
+ displayName: display_name,
21
+ mail: email,
22
+ authnContextClassRef: authn_context,
23
+ authenticationMethod: authn_method,
24
+ identity_provider: identity_provider
25
+ }
26
+ end
27
+
28
+ def user_pid
29
+ persistent_id
30
+ end
31
+
32
+ def user_eid
33
+ principal_name
34
+ end
35
+
36
+ def client_ip
37
+ safe('HTTP_X_FORWARDED_FOR').split(',').first
38
+ end
39
+
40
+ def persistent_id
41
+ get 'HTTP_X_SHIB_PERSISTENT_ID'
42
+ end
43
+
44
+ def principal_name
45
+ get 'HTTP_X_SHIB_EDUPERSONPRINCIPALNAME'
46
+ end
47
+
48
+ def display_name
49
+ get 'HTTP_X_SHIB_DISPLAYNAME'
50
+ end
51
+
52
+ def email
53
+ get 'HTTP_X_SHIB_MAIL'
54
+ end
55
+
56
+ def affiliation
57
+ safe('HTTP_X_SHIB_EDUPERSONSCOPEDAFFILIATION').split(';')
58
+ end
59
+
60
+ def authn_method
61
+ get 'HTTP_X_SHIB_AUTHENTICATION_METHOD'
62
+ end
63
+
64
+ def authn_context
65
+ get 'HTTP_X_SHIB_AUTHNCONTEXT_CLASS'
66
+ end
67
+
68
+ def identity_provider
69
+ get 'HTTP_X_SHIB_IDENTITY_PROVIDER'
70
+ end
71
+
72
+ def identity_keys
73
+ %i[user_pid user_eid eduPersonScopedAffiliation]
74
+ end
75
+
76
+ private
77
+
78
+ def get(key)
79
+ request.env[key]
80
+ end
81
+
82
+ def safe(key)
83
+ get(key) || ''
84
+ end
85
+ end
86
+ end
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ # A container module for classes related to processing HTTP/Rack requests
4
+ module Keycard::Request
5
+ end
6
+
7
+ require_relative 'request/attributes'
8
+ require_relative 'request/cosign_attributes'
9
+ require_relative 'request/direct_attributes'
10
+ require_relative 'request/proxied_attributes'
11
+ require_relative 'request/shibboleth_attributes'
12
+ require_relative 'request/attributes_factory'
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Keycard
4
- VERSION = "0.1.2"
4
+ VERSION = "0.2.0"
5
5
  end
data/lib/keycard.rb CHANGED
@@ -15,7 +15,5 @@ end
15
15
 
16
16
  require "keycard/db"
17
17
  require "keycard/railtie" if defined?(Rails)
18
- require "keycard/request_attributes"
19
18
  require "keycard/institution_finder"
20
- require "keycard/direct_request"
21
- require "keycard/proxied_request"
19
+ require "keycard/request"
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: keycard
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.2
4
+ version: 0.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Noah Botimer
@@ -9,7 +9,7 @@ authors:
9
9
  autorequire:
10
10
  bindir: exe
11
11
  cert_chain: []
12
- date: 2018-06-08 00:00:00.000000000 Z
12
+ date: 2018-07-11 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: sequel
@@ -188,11 +188,15 @@ files:
188
188
  - keycard.gemspec
189
189
  - lib/keycard.rb
190
190
  - lib/keycard/db.rb
191
- - lib/keycard/direct_request.rb
192
191
  - lib/keycard/institution_finder.rb
193
- - lib/keycard/proxied_request.rb
194
192
  - lib/keycard/railtie.rb
195
- - lib/keycard/request_attributes.rb
193
+ - lib/keycard/request.rb
194
+ - lib/keycard/request/attributes.rb
195
+ - lib/keycard/request/attributes_factory.rb
196
+ - lib/keycard/request/cosign_attributes.rb
197
+ - lib/keycard/request/direct_attributes.rb
198
+ - lib/keycard/request/proxied_attributes.rb
199
+ - lib/keycard/request/shibboleth_attributes.rb
196
200
  - lib/keycard/version.rb
197
201
  - lib/tasks/migrate.rake
198
202
  homepage: https://github.com/mlibrary/keycard
@@ -1,19 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Keycard
4
- # This request wrapper should be used when the application will serve HTTP
5
- # requests directly or through a proxy that sets up the usual environment.
6
- class DirectRequest < SimpleDelegator
7
- def self.for(request)
8
- new(request)
9
- end
10
-
11
- def username
12
- env['REMOTE_USER'] || ''
13
- end
14
-
15
- def client_ip
16
- (env['REMOTE_ADDR'] || '').split(',').first || ''
17
- end
18
- end
19
- end
@@ -1,49 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Keycard
4
- # This class is responsible for extracting the user attributes (i.e. the
5
- # complete set of things that determine the user's #identity), given a Rack
6
- # request.
7
- class RequestAttributes
8
- def initialize(request, finder: InstitutionFinder.new, request_factory: default_factory)
9
- @request = request
10
- @finder = finder
11
- @request_factory = request_factory
12
- end
13
-
14
- def [](attr)
15
- all[attr]
16
- end
17
-
18
- def all
19
- user_attributes.merge(finder.attributes_for(request))
20
- end
21
-
22
- private
23
-
24
- def user_attributes
25
- { username: request.username }
26
- end
27
-
28
- def request
29
- request_factory.for(@request)
30
- end
31
-
32
- def default_factory
33
- access = Keycard.config.access.to_sym
34
- case access
35
- when :direct
36
- DirectRequest
37
- when :proxy
38
- ProxiedRequest
39
- else
40
- # TODO: Warn about this once to the appropriate log; probably in a config check, not here.
41
- # puts "Keycard does not recognize the '#{access}' access mode, using 'direct'."
42
- DirectRequest
43
- end
44
- end
45
-
46
- attr_reader :finder
47
- attr_reader :request_factory
48
- end
49
- end