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