chef_fixie 0.2.0 → 0.5.1
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 +5 -5
- data/bin/chef_fixie +1 -1
- data/doc/BulkFixup.md +1 -1
- data/doc/CommonTasks.md +14 -3
- data/lib/chef_fixie.rb +7 -7
- data/lib/chef_fixie/authz_mapper.rb +26 -28
- data/lib/chef_fixie/authz_objects.rb +51 -41
- data/lib/chef_fixie/bulk_edit_permissions.rb +24 -20
- data/lib/chef_fixie/check_org_associations.rb +56 -58
- data/lib/chef_fixie/config.rb +58 -23
- data/lib/chef_fixie/console.rb +15 -10
- data/lib/chef_fixie/context.rb +2 -4
- data/lib/chef_fixie/sql.rb +12 -12
- data/lib/chef_fixie/sql_objects.rb +74 -34
- data/lib/chef_fixie/utility_helpers.rb +13 -9
- data/lib/chef_fixie/version.rb +1 -1
- data/spec/chef_fixie/acl_spec.rb +23 -25
- data/spec/chef_fixie/assoc_invite_spec.rb +5 -8
- data/spec/chef_fixie/check_org_associations_spec.rb +14 -17
- data/spec/chef_fixie/groups_spec.rb +7 -11
- data/spec/chef_fixie/org_spec.rb +4 -5
- data/spec/chef_fixie/orgs_spec.rb +6 -9
- data/spec/spec_helper.rb +5 -6
- metadata +13 -51
- data/bin/bundler +0 -16
- data/bin/chef-apply +0 -16
- data/bin/chef-client +0 -16
- data/bin/chef-shell +0 -16
- data/bin/chef-solo +0 -16
- data/bin/chef-zero +0 -16
- data/bin/coderay +0 -16
- data/bin/edit_json.rb +0 -16
- data/bin/erubis +0 -16
- data/bin/ffi-yajl-bench +0 -16
- data/bin/fixie~ +0 -231
- data/bin/htmldiff +0 -16
- data/bin/knife +0 -16
- data/bin/ldiff +0 -16
- data/bin/net-dhcp +0 -16
- data/bin/ohai +0 -16
- data/bin/prettify_json.rb +0 -16
- data/bin/pry +0 -16
- data/bin/rackup +0 -16
- data/bin/rake +0 -16
- data/bin/rdoc +0 -16
- data/bin/restclient +0 -16
- data/bin/ri +0 -16
- data/bin/rspec +0 -16
- data/bin/s3sh +0 -16
- data/bin/sequel +0 -16
- data/bin/serverspec-init +0 -16
- data/doc/AccessingSQL.md~ +0 -32
- data/doc/BulkFixup.md~ +0 -28
- data/doc/CommonTasks.md~ +0 -0
- data/doc/GETTING_STARTED.md~ +0 -6
- data/spec/chef_fixie/assoc_invite_spec.rb~ +0 -26
- data/spec/chef_fixie/check_org_associations_spec.rb~ +0 -34
- data/spec/chef_fixie/org_spec.rb~ +0 -53
@@ -16,50 +16,53 @@
|
|
16
16
|
#
|
17
17
|
# Author: Mark Anderson <mark@chef.io>
|
18
18
|
#
|
19
|
-
require
|
19
|
+
require "sequel"
|
20
20
|
|
21
|
-
require_relative
|
22
|
-
require_relative
|
23
|
-
require_relative
|
21
|
+
require_relative "config.rb"
|
22
|
+
require_relative "authz_objects.rb"
|
23
|
+
require_relative "authz_mapper.rb"
|
24
24
|
|
25
|
-
require
|
25
|
+
require "pp"
|
26
26
|
|
27
27
|
module ChefFixie
|
28
28
|
module BulkEditPermissions
|
29
29
|
def self.orgs
|
30
30
|
@orgs ||= ChefFixie::Sql::Orgs.new
|
31
31
|
end
|
32
|
+
|
32
33
|
def self.users
|
33
34
|
@users ||= ChefFixie::Sql::Users.new
|
34
35
|
end
|
36
|
+
|
35
37
|
def self.assocs
|
36
38
|
@assocs ||= ChefFixie::Sql::Associations.new
|
37
39
|
end
|
40
|
+
|
38
41
|
def self.invites
|
39
42
|
invites ||= ChefFixie::Sql::Invites.new
|
40
43
|
end
|
41
44
|
|
42
45
|
def self.check_permissions(org)
|
43
46
|
org = orgs[org] if org.is_a?(String)
|
44
|
-
admins = org.groups[
|
45
|
-
pivotal = users[
|
47
|
+
admins = org.groups["admins"].authz_id
|
48
|
+
pivotal = users["pivotal"].authz_id
|
46
49
|
errors = Hash.new({})
|
47
50
|
org.each_authz_object do |object|
|
48
|
-
begin
|
51
|
+
begin
|
49
52
|
acl = object.acl_raw
|
50
|
-
rescue RestClient::ResourceNotFound=>e
|
53
|
+
rescue RestClient::ResourceNotFound => e
|
51
54
|
puts "#{object.class} '#{object.name}' id '#{object.id}' missing authz info"
|
52
55
|
# pp :object=>object, :e=>e
|
53
56
|
next
|
54
57
|
end
|
55
58
|
broken_acl = {}
|
56
59
|
# the one special case
|
57
|
-
acl.each do |k,v|
|
60
|
+
acl.each do |k, v|
|
58
61
|
list = []
|
59
|
-
list << "pivotal" if !v[
|
62
|
+
list << "pivotal" if !v["actors"].member?(pivotal)
|
60
63
|
# admins doesn't belong to the billing admins group
|
61
|
-
if object.class != ChefFixie::Sql::Group || object.name !=
|
62
|
-
list << "admins" if !v[
|
64
|
+
if object.class != ChefFixie::Sql::Group || object.name != "billing-admins"
|
65
|
+
list << "admins" if !v["groups"].member?(admins)
|
63
66
|
end
|
64
67
|
broken_acl[k] = list if !list.empty?
|
65
68
|
end
|
@@ -69,7 +72,7 @@ module ChefFixie
|
|
69
72
|
errors[classname][object.name] = broken_acl
|
70
73
|
end
|
71
74
|
end
|
72
|
-
|
75
|
+
errors
|
73
76
|
end
|
74
77
|
|
75
78
|
def self.ace_add(list, ace_type, entity)
|
@@ -78,17 +81,18 @@ module ChefFixie
|
|
78
81
|
item.ace_add(ace_type, entity)
|
79
82
|
else
|
80
83
|
puts "item.class is not a native authz type"
|
81
|
-
return
|
84
|
+
return nil
|
82
85
|
end
|
83
86
|
end
|
84
87
|
end
|
88
|
+
|
85
89
|
def self.ace_delete(list, ace_type, entity)
|
86
90
|
list.each do |item|
|
87
91
|
if item.respond_to?(:ace_delete)
|
88
92
|
item.ace_delete(ace_type, entity)
|
89
93
|
else
|
90
94
|
puts "item.class is not a native authz type"
|
91
|
-
return
|
95
|
+
return nil
|
92
96
|
end
|
93
97
|
end
|
94
98
|
end
|
@@ -128,11 +132,11 @@ module ChefFixie
|
|
128
132
|
def self.add_admin_permissions(org)
|
129
133
|
org = orgs[org] if org.is_a?(String)
|
130
134
|
# rework when ace add takes multiple items...
|
131
|
-
admins = org.groups[
|
132
|
-
pivotal = users[
|
135
|
+
admins = org.groups["admins"]
|
136
|
+
pivotal = users["pivotal"]
|
133
137
|
org.each_authz_object do |object|
|
134
138
|
object.ace_add(:all, pivotal)
|
135
|
-
if object.class != ChefFixie::Sql::Group || object.name !=
|
139
|
+
if object.class != ChefFixie::Sql::Group || object.name != "billing-admins"
|
136
140
|
object.ace_add(:all, admins)
|
137
141
|
end
|
138
142
|
end
|
@@ -150,7 +154,7 @@ module ChefFixie
|
|
150
154
|
puts "#{obj.name} from #{c.name}"
|
151
155
|
end
|
152
156
|
end
|
153
|
-
|
157
|
+
nil
|
154
158
|
end
|
155
159
|
|
156
160
|
end
|
@@ -18,45 +18,55 @@
|
|
18
18
|
# Author: Mark Anderson <mark@chef.io>
|
19
19
|
#
|
20
20
|
|
21
|
-
require_relative
|
22
|
-
require_relative
|
23
|
-
require_relative
|
24
|
-
require_relative
|
21
|
+
require_relative "config"
|
22
|
+
require_relative "authz_objects"
|
23
|
+
require_relative "authz_mapper"
|
24
|
+
require_relative "utility_helpers"
|
25
25
|
|
26
26
|
module ChefFixie
|
27
27
|
module CheckOrgAssociations
|
28
28
|
def self.orgs
|
29
29
|
@orgs ||= ChefFixie::Sql::Orgs.new
|
30
30
|
end
|
31
|
+
|
31
32
|
def self.users
|
32
33
|
@users ||= ChefFixie::Sql::Users.new
|
33
34
|
end
|
35
|
+
|
34
36
|
def self.assocs
|
35
37
|
@assocs ||= ChefFixie::Sql::Associations.new
|
36
38
|
end
|
39
|
+
|
37
40
|
def self.invites
|
38
41
|
invites ||= ChefFixie::Sql::Invites.new
|
39
42
|
end
|
40
43
|
|
41
44
|
def self.make_user(user)
|
42
45
|
if user.is_a?(String)
|
43
|
-
|
46
|
+
users[user]
|
44
47
|
elsif user.is_a?(ChefFixie::Sql::User)
|
45
|
-
|
48
|
+
user
|
46
49
|
else
|
47
50
|
raise "Expected a user, got a #{user.class}"
|
48
51
|
end
|
49
52
|
end
|
53
|
+
|
50
54
|
def self.make_org(org)
|
51
55
|
if org.is_a?(String)
|
52
|
-
|
56
|
+
orgs[org]
|
53
57
|
elsif org.is_a?(ChefFixie::Sql::Org)
|
54
|
-
|
58
|
+
org
|
55
59
|
else
|
56
60
|
raise "Expected an org, got a #{org.class}"
|
57
61
|
end
|
58
62
|
end
|
59
63
|
|
64
|
+
def usag_for_user(org, user)
|
65
|
+
user = make_user(user)
|
66
|
+
org = make_org(org)
|
67
|
+
org.groups[user.id]
|
68
|
+
end
|
69
|
+
|
60
70
|
def self.check_association(org, user, global_admins = nil)
|
61
71
|
# magic to make usage easier
|
62
72
|
org = make_org(org)
|
@@ -78,7 +88,7 @@ module ChefFixie
|
|
78
88
|
return :user_not_in_usag
|
79
89
|
end
|
80
90
|
|
81
|
-
if !org.groups[
|
91
|
+
if !org.groups["users"].member?(usag)
|
82
92
|
return :usag_not_in_users
|
83
93
|
end
|
84
94
|
|
@@ -89,7 +99,7 @@ module ChefFixie
|
|
89
99
|
if invites.by_org_id_user_id(org.id, user.id)
|
90
100
|
return :zombie_invite
|
91
101
|
end
|
92
|
-
|
102
|
+
true
|
93
103
|
end
|
94
104
|
|
95
105
|
def self.fix_association(org, user, global_admins = nil)
|
@@ -98,7 +108,7 @@ module ChefFixie
|
|
98
108
|
user = users[user] if user.is_a?(String)
|
99
109
|
global_admins ||= org.global_admins
|
100
110
|
|
101
|
-
failure = check_association(org,user,global_admins)
|
111
|
+
failure = check_association(org, user, global_admins)
|
102
112
|
|
103
113
|
case failure
|
104
114
|
when true
|
@@ -108,14 +118,14 @@ module ChefFixie
|
|
108
118
|
usag.group_add(user)
|
109
119
|
when :usag_not_in_users
|
110
120
|
usag = org.groups[user.id]
|
111
|
-
org.groups[
|
121
|
+
org.groups["users"].group_add(usag)
|
112
122
|
when :global_admins_lacks_read
|
113
123
|
user.ace_add(:read, global_admins)
|
114
124
|
else
|
115
125
|
puts "#{org.name} #{user.name} can't fix problem #{failure} yet"
|
116
126
|
return false
|
117
127
|
end
|
118
|
-
|
128
|
+
true
|
119
129
|
end
|
120
130
|
|
121
131
|
def self.check_associations(org)
|
@@ -133,56 +143,54 @@ module ChefFixie
|
|
133
143
|
users_assoc = assocs.by_org_id(org.id).all(:all)
|
134
144
|
users_invite = invites.by_org_id(org.id).all(:all)
|
135
145
|
|
136
|
-
user_ids = users_assoc.map {|a| a.user_id }
|
137
|
-
users_in_org = user_ids.map {|i| users.by_id(i).all.first }
|
138
|
-
usernames = users_in_org.map {|u| u.name }
|
139
|
-
|
146
|
+
user_ids = users_assoc.map { |a| a.user_id }
|
147
|
+
users_in_org = user_ids.map { |i| users.by_id(i).all.first }
|
148
|
+
usernames = users_in_org.map { |u| u.name }
|
140
149
|
|
141
150
|
# check that users aren't both invited and associated
|
142
|
-
invited_ids = users_invite.map {|a| a.user_id }
|
151
|
+
invited_ids = users_invite.map { |a| a.user_id }
|
143
152
|
overlap_ids = user_ids & invited_ids
|
144
153
|
|
145
154
|
if !overlap_ids.empty?
|
146
|
-
overlap_names = overlap_ids.map {|i| users.by_id(i).all.first.name rescue "#{i}" }
|
147
|
-
puts "#{orgname} users both associated and invited: #{overlap_names.join(', ')
|
155
|
+
overlap_names = overlap_ids.map { |i| users.by_id(i).all.first.name rescue "#{i}" }
|
156
|
+
puts "#{orgname} users both associated and invited: #{overlap_names.join(', ')}"
|
148
157
|
success = false
|
149
158
|
end
|
150
159
|
|
151
160
|
# Check that we don't have zombie USAGs left around (not 100% reliable)
|
152
161
|
# because someone could create a group that looks like a USAG
|
153
162
|
possible_usags = org.groups.list(:all) - user_ids
|
154
|
-
usags = possible_usags.select {|n| n =~ /^\h+{20}$/ }
|
163
|
+
usags = possible_usags.select { |n| n =~ /^\h+{20}$/ }
|
155
164
|
if !usags.empty?
|
156
|
-
puts "#{orgname} Suspicious USAGS without associated user #{usags.join(', ')
|
165
|
+
puts "#{orgname} Suspicious USAGS without associated user #{usags.join(', ')}"
|
157
166
|
end
|
158
167
|
|
159
168
|
# Check group membership for sanity
|
160
|
-
success &= check_group(org,
|
161
|
-
success &= check_group(org,
|
169
|
+
success &= check_group(org, "billing-admins", usernames)
|
170
|
+
success &= check_group(org, "admins", usernames)
|
162
171
|
|
163
172
|
# TODO check for non-usags in users!
|
164
|
-
users_members = org.groups[
|
165
|
-
users_actors = users_members[
|
173
|
+
users_members = org.groups["users"].group
|
174
|
+
users_actors = users_members["actors"] - [[:global, "pivotal"]]
|
166
175
|
if !users_actors.empty?
|
167
176
|
puts "#{orgname} has actors in it's users group #{users_actors}"
|
168
177
|
end
|
169
|
-
non_usags = users_members[
|
178
|
+
non_usags = users_members["groups"].map { |g| g[1] } - user_ids
|
170
179
|
if !non_usags.empty?
|
171
180
|
puts "#{orgname} warning: has non usags in it's users group #{non_usags.join(', ')}"
|
172
181
|
end
|
173
182
|
|
174
|
-
|
175
183
|
# Check individual associations
|
176
184
|
users_in_org.each do |user|
|
177
|
-
result =
|
178
|
-
if
|
185
|
+
result = check_association(org, user, global_admins)
|
186
|
+
if result != true
|
179
187
|
puts "Org #{orgname} Association check failed for #{user.name} #{result}"
|
180
188
|
success = false
|
181
189
|
end
|
182
190
|
end
|
183
191
|
|
184
192
|
puts "Org #{orgname} is #{success ? 'ok' : 'bad'} (#{users_in_org.count} users)"
|
185
|
-
|
193
|
+
success
|
186
194
|
end
|
187
195
|
|
188
196
|
# expect at least one current user to be in admins and billing admins
|
@@ -192,50 +200,40 @@ module ChefFixie
|
|
192
200
|
puts "#{orgname} Missing group #{groupname}"
|
193
201
|
return :no_such_group
|
194
202
|
end
|
195
|
-
actors = g.group[
|
203
|
+
actors = g.group["actors"].map { |x| x[1] }
|
196
204
|
live = actors & users
|
197
205
|
|
198
206
|
if live.count == 0
|
199
207
|
puts "Org #{org.name} has no active users in #{groupname}"
|
200
208
|
return false
|
201
209
|
end
|
202
|
-
|
210
|
+
true
|
203
211
|
end
|
204
212
|
|
205
|
-
|
206
|
-
## TODO: Port this
|
207
213
|
def self.remove_association(org, user)
|
208
214
|
# magic to make usage easier
|
209
|
-
org =
|
210
|
-
user =
|
215
|
+
org = make_org(org)
|
216
|
+
user = make_user(user)
|
211
217
|
|
212
218
|
# remove USAG
|
213
|
-
|
219
|
+
usag = org.groups[user.id]
|
220
|
+
usag.delete if usag
|
214
221
|
|
215
|
-
# remove
|
216
|
-
|
217
|
-
|
218
|
-
|
222
|
+
# remove from any groups they are in
|
223
|
+
org.groups.all(:all).each do |g|
|
224
|
+
g.group_delete(user) if g.member?(user)
|
225
|
+
end
|
219
226
|
|
220
|
-
|
221
|
-
|
222
|
-
OrgMapper::RawAuth.put("actors/#{u_aid}/acl/read", read_ace)
|
227
|
+
# remove read ACE
|
228
|
+
user.ace_delete(:read, org.global_admins)
|
223
229
|
|
224
230
|
# remove association record
|
231
|
+
assoc = assocs.by_org_id_user_id(org.id, user.id)
|
232
|
+
assoc.delete if assoc
|
225
233
|
|
226
|
-
|
227
|
-
|
228
|
-
|
229
|
-
OrgMapper::CouchSupport.delete_account_doc(doc[:id])
|
230
|
-
|
231
|
-
# clean up excess invites
|
232
|
-
invites = OrgMapper::CouchSupport.invites_for_org(orgname)
|
233
|
-
invite_map = invites.inject({}) {|a,e| a[e[:name]] = e; a}
|
234
|
-
|
235
|
-
if invite_map.has_key?(username)
|
236
|
-
invite = invite_map[username]
|
237
|
-
OrgMapper::CouchSupport.delete_account_doc(invite[:id])
|
238
|
-
end
|
234
|
+
# remove any invites
|
235
|
+
invite = invites.by_org_id_user_id(org.id, user.id)
|
236
|
+
invite.delete if invite
|
239
237
|
end
|
240
238
|
end
|
241
239
|
end
|
data/lib/chef_fixie/config.rb
CHANGED
@@ -1,5 +1,5 @@
|
|
1
1
|
#
|
2
|
-
# Copyright (c) 2014-2015 Chef Software Inc.
|
2
|
+
# Copyright (c) 2014-2015 Chef Software Inc.
|
3
3
|
# License :: Apache License, Version 2.0
|
4
4
|
#
|
5
5
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
@@ -18,9 +18,10 @@
|
|
18
18
|
#
|
19
19
|
# Much of this code was orginally derived from the orgmapper tool, which had many varied authors.
|
20
20
|
|
21
|
-
require
|
22
|
-
require
|
23
|
-
require
|
21
|
+
require "singleton"
|
22
|
+
require "ffi_yajl"
|
23
|
+
require "pathname"
|
24
|
+
require "veil"
|
24
25
|
|
25
26
|
module ChefFixie
|
26
27
|
def self.configure
|
@@ -29,11 +30,11 @@ module ChefFixie
|
|
29
30
|
|
30
31
|
def self.load_config(config_file = nil)
|
31
32
|
if config_file
|
32
|
-
puts "loading config: #{config_file}..."
|
33
|
+
puts "loading config: #{config_file}..." if ChefFixie::Console.started_from_command_line?
|
33
34
|
Kernel.load(config_file)
|
34
35
|
else
|
35
36
|
path = "/etc/opscode"
|
36
|
-
puts "loading config from #{path}"
|
37
|
+
puts "loading config from #{path}" if ChefFixie::Console.started_from_command_line?
|
37
38
|
ChefFixie::Config.instance.load_from_pc(path)
|
38
39
|
end
|
39
40
|
end
|
@@ -65,7 +66,7 @@ module ChefFixie
|
|
65
66
|
KEYS = [:authz_uri, :sql_database, :superuser_id, :pivotal_key]
|
66
67
|
KEYS.each { |k| attr_accessor k }
|
67
68
|
|
68
|
-
def merge_opts(opts={})
|
69
|
+
def merge_opts(opts = {})
|
69
70
|
opts.each do |key, value|
|
70
71
|
send("#{key}=".to_sym, value)
|
71
72
|
end
|
@@ -83,7 +84,7 @@ module ChefFixie
|
|
83
84
|
key_len > max ? key_len : max
|
84
85
|
end
|
85
86
|
KEYS.each do |key|
|
86
|
-
value = send(key) ||
|
87
|
+
value = send(key) || "default"
|
87
88
|
txt << "# %#{max_key_len}s: %s" % [key.to_s, value]
|
88
89
|
end
|
89
90
|
txt.join("\n")
|
@@ -101,25 +102,28 @@ module ChefFixie
|
|
101
102
|
def load_from_pc(dir = "/etc/opscode")
|
102
103
|
configdir = Pathname.new(dir)
|
103
104
|
|
104
|
-
config_files = %w
|
105
|
+
config_files = %w{chef-server-running.json}
|
105
106
|
config = load_json_from_path([configdir], config_files)
|
106
107
|
|
107
|
-
|
108
|
-
|
109
|
-
|
108
|
+
secrets = load_secrets_from_path([configdir], %w{private-chef-secrets.json} )
|
109
|
+
|
110
|
+
authz_config = config["private_chef"]["oc_bifrost"]
|
111
|
+
authz_vip = authz_config["vip"]
|
112
|
+
authz_port = authz_config["port"]
|
110
113
|
@authz_uri = "http://#{authz_vip}:#{authz_port}"
|
111
|
-
|
112
|
-
@superuser_id = authz_config[
|
113
|
-
|
114
|
-
sql_config = config[
|
115
|
-
|
116
|
-
|
117
|
-
|
118
|
-
|
119
|
-
|
120
|
-
|
114
|
+
|
115
|
+
@superuser_id = dig(secrets, %w{oc_bifrost superuser_id}) || authz_config["superuser_id"]
|
116
|
+
|
117
|
+
sql_config = config["private_chef"]["postgresql"]
|
118
|
+
erchef_config = config["private_chef"]["opscode-erchef"]
|
119
|
+
|
120
|
+
sql_user = sql_config["sql_user"] || erchef_config["sql_user"]
|
121
|
+
sql_pw = dig(secrets, %w{opscode_erchef sql_password}) || sql_config["sql_password"] || erchef_config["sql_password"]
|
122
|
+
sql_vip = sql_config["vip"]
|
123
|
+
sql_port = sql_config["port"]
|
124
|
+
|
121
125
|
@sql_database = "postgres://#{sql_user}:#{sql_pw}@#{sql_vip}/opscode_chef"
|
122
|
-
|
126
|
+
|
123
127
|
@pivotal_key = configdir + "pivotal.pem"
|
124
128
|
end
|
125
129
|
|
@@ -135,5 +139,36 @@ module ChefFixie
|
|
135
139
|
end
|
136
140
|
end
|
137
141
|
end
|
142
|
+
|
143
|
+
def load_secrets_from_path(pathlist, filelist)
|
144
|
+
pathlist.each do |path|
|
145
|
+
filelist.each do |file|
|
146
|
+
configfile = path + file
|
147
|
+
if configfile.file?
|
148
|
+
data = Veil::CredentialCollection::ChefSecretsFile.from_file(configfile)
|
149
|
+
return data
|
150
|
+
end
|
151
|
+
end
|
152
|
+
end
|
153
|
+
nil
|
154
|
+
end
|
155
|
+
|
156
|
+
def dig(hash, list)
|
157
|
+
if hash.respond_to?(:get)
|
158
|
+
hash.get(*list)
|
159
|
+
elsif hash.nil?
|
160
|
+
nil
|
161
|
+
elsif list.empty?
|
162
|
+
hash
|
163
|
+
else
|
164
|
+
element = list.shift
|
165
|
+
if hash.has_key?(element)
|
166
|
+
dig(hash[element], list)
|
167
|
+
else
|
168
|
+
nil
|
169
|
+
end
|
170
|
+
end
|
171
|
+
end
|
172
|
+
|
138
173
|
end
|
139
174
|
end
|