keycard 0.1.2 → 0.2.0

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 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