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 +4 -4
- data/.rubocop.yml +6 -0
- data/keycard.gemspec +1 -1
- data/lib/keycard/db.rb +135 -137
- data/lib/keycard/institution_finder.rb +46 -42
- data/lib/keycard/railtie.rb +84 -86
- data/lib/keycard/request/attributes.rb +94 -0
- data/lib/keycard/request/attributes_factory.rb +34 -0
- data/lib/keycard/request/cosign_attributes.rb +29 -0
- data/lib/keycard/request/direct_attributes.rb +28 -0
- data/lib/keycard/{proxied_request.rb → request/proxied_attributes.rb} +15 -7
- data/lib/keycard/request/shibboleth_attributes.rb +86 -0
- data/lib/keycard/request.rb +12 -0
- data/lib/keycard/version.rb +1 -1
- data/lib/keycard.rb +1 -3
- metadata +9 -5
- data/lib/keycard/direct_request.rb +0 -19
- data/lib/keycard/request_attributes.rb +0 -49
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 99719e6ba14e97ad966b5a53b7e114f3eb6c6bc02aa674a1654f6c7f151ad793
|
4
|
+
data.tar.gz: 7ee5e247aab91665c1c2fcc394e6f7f535f2382af500ec723f185b130a0258ed
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
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
data/lib/keycard/db.rb
CHANGED
@@ -4,159 +4,157 @@ require 'ostruct'
|
|
4
4
|
require 'logger'
|
5
5
|
require 'yaml'
|
6
6
|
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
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
|
-
|
42
|
+
rescue Sequel::DatabaseError, NoMethodError => e
|
43
|
+
raise DatabaseError, LOAD_ERROR + "\n" + e.message
|
47
44
|
end
|
45
|
+
db
|
46
|
+
end
|
48
47
|
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
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
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
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
|
-
|
93
|
-
|
94
|
-
|
87
|
+
Sequel.extension :migration
|
88
|
+
Sequel::Migrator.run(db, File.join(__dir__, '../../db/migrations'), table: schema_table)
|
89
|
+
end
|
95
90
|
|
96
|
-
|
97
|
-
|
98
|
-
|
91
|
+
def schema_table
|
92
|
+
:keycard_schema
|
93
|
+
end
|
99
94
|
|
100
|
-
|
101
|
-
|
102
|
-
|
103
|
-
File.write(schema_file, SCHEMA_HEADER + version)
|
104
|
-
end
|
95
|
+
def schema_file
|
96
|
+
'db/keycard.yml'
|
97
|
+
end
|
105
98
|
|
106
|
-
|
107
|
-
|
108
|
-
|
109
|
-
|
110
|
-
|
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
|
-
|
114
|
-
|
115
|
-
|
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
|
-
|
118
|
-
|
119
|
-
|
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
|
-
|
125
|
-
|
126
|
-
|
127
|
-
|
128
|
-
|
129
|
-
|
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
|
-
|
138
|
-
|
139
|
-
|
140
|
-
|
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
|
-
|
145
|
-
|
146
|
-
|
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
|
-
|
149
|
-
|
150
|
-
|
151
|
-
raise DatabaseError, CONNECTION_ERROR unless connected?
|
152
|
-
@db
|
153
|
-
end
|
143
|
+
def connected?
|
144
|
+
!@db.nil?
|
145
|
+
end
|
154
146
|
|
155
|
-
|
156
|
-
|
157
|
-
|
158
|
-
|
159
|
-
|
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
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
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
|
-
|
29
|
-
|
33
|
+
def attributes_for(request)
|
34
|
+
return {} unless (numeric_ip = numeric_ip(request.client_ip))
|
30
35
|
|
31
|
-
|
36
|
+
insts = insts_for_ip(numeric_ip)
|
32
37
|
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
end
|
38
|
+
if !insts.empty?
|
39
|
+
{ dlpsInstitutionId: insts }
|
40
|
+
else
|
41
|
+
{}
|
38
42
|
end
|
43
|
+
end
|
39
44
|
|
40
|
-
|
45
|
+
private
|
41
46
|
|
42
|
-
|
47
|
+
attr_reader :stmt
|
43
48
|
|
44
|
-
|
45
|
-
|
46
|
-
|
49
|
+
def insts_for_ip(numeric_ip)
|
50
|
+
stmt.call(client_ip: numeric_ip).map { |row| row[:inst] }
|
51
|
+
end
|
47
52
|
|
48
|
-
|
49
|
-
|
53
|
+
def numeric_ip(dotted_ip)
|
54
|
+
return unless dotted_ip
|
50
55
|
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
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
|
data/lib/keycard/railtie.rb
CHANGED
@@ -1,108 +1,106 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
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
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
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
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
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
|
-
|
45
|
-
|
46
|
-
|
43
|
+
def before_blocks
|
44
|
+
@before_blocks ||= []
|
45
|
+
end
|
47
46
|
|
48
|
-
|
49
|
-
|
50
|
-
|
47
|
+
def after_blocks
|
48
|
+
@after_blocks ||= []
|
49
|
+
end
|
51
50
|
|
52
|
-
|
53
|
-
|
54
|
-
|
51
|
+
def ready_blocks
|
52
|
+
@ready_blocks ||= []
|
53
|
+
end
|
55
54
|
|
56
|
-
|
57
|
-
|
58
|
-
|
55
|
+
def under_rake!
|
56
|
+
@under_rake = true
|
57
|
+
end
|
59
58
|
|
60
|
-
|
61
|
-
|
62
|
-
end
|
59
|
+
def under_rake?
|
60
|
+
@under_rake ||= false
|
63
61
|
end
|
62
|
+
end
|
64
63
|
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
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
|
-
|
80
|
-
|
81
|
-
end
|
78
|
+
Railtie.before_blocks.each do |block|
|
79
|
+
block.call(config.to_h)
|
82
80
|
end
|
81
|
+
end
|
83
82
|
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
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
|
-
|
90
|
+
Keycard::DB.initialize! unless Railtie.under_rake?
|
92
91
|
|
93
|
-
|
94
|
-
|
95
|
-
end
|
92
|
+
Railtie.ready_blocks.each do |block|
|
93
|
+
block.call(Keycard::DB.db)
|
96
94
|
end
|
95
|
+
end
|
97
96
|
|
98
|
-
|
99
|
-
|
100
|
-
|
101
|
-
|
97
|
+
def rake_files
|
98
|
+
base = Pathname(__dir__) + '../tasks/'
|
99
|
+
[base + 'migrate.rake']
|
100
|
+
end
|
102
101
|
|
103
|
-
|
104
|
-
|
105
|
-
|
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
|
12
|
-
def
|
13
|
-
|
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
|
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'
|
data/lib/keycard/version.rb
CHANGED
data/lib/keycard.rb
CHANGED
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.
|
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-
|
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/
|
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
|