entitlements-app 1.2.1 → 1.2.3
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/lib/entitlements/cli.rb +2 -0
- data/lib/entitlements/service/ldap.rb +33 -7
- data/lib/entitlements.rb +90 -16
- data/lib/version.rb +1 -1
- metadata +16 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 1d78c3e43bb1a892e7d45aa8c1a9c608c26db75f36a8adda13646b5ae9a756dc
|
|
4
|
+
data.tar.gz: afd0505ea8106dbaaa3486e1c2dfab168c6209b97d99322cc6f560a6526f8aab
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 0b1279f7726543ffcaf95228c7b0f831f462bcb05431db9acd195afa01f6f40c00561dd0cb81c3c097c9d75291d87db1c2eb16a237e2ced1b22ac3d00b8967b4
|
|
7
|
+
data.tar.gz: b0f8f91d63df0e79a77bf8e00d39a2403a2281a71dc533f7d45bb0ba476c30f45953f828011006a9533ec2f3f77796859a16d5bd832a2e1d4272f8a598894bb8
|
data/lib/entitlements/cli.rb
CHANGED
|
@@ -17,9 +17,9 @@ module Entitlements
|
|
|
17
17
|
# This keeps the schema happy.
|
|
18
18
|
attr_reader :binddn, :person_dn_format
|
|
19
19
|
|
|
20
|
-
# Constructor-like object that ensures only one LDAP object
|
|
21
|
-
#
|
|
22
|
-
#
|
|
20
|
+
# Constructor-like object that ensures only one LDAP service object is made for a
|
|
21
|
+
# given LDAP server. Takes the same parameters as the constructor and returns the
|
|
22
|
+
# same object type.
|
|
23
23
|
#
|
|
24
24
|
# addr - URL of LDAP server e.g. ldaps://ldap.example.net:636
|
|
25
25
|
# binddn - DN to bind with
|
|
@@ -80,6 +80,8 @@ module Entitlements
|
|
|
80
80
|
@ca_file = ca_file
|
|
81
81
|
@disable_ssl_verification = disable_ssl_verification
|
|
82
82
|
@person_dn_format = person_dn_format
|
|
83
|
+
@known_existing_dns = {}
|
|
84
|
+
@known_existing_dns_mutex = Mutex.new
|
|
83
85
|
end
|
|
84
86
|
|
|
85
87
|
# Read a single entry identified by its DN and return the value. Returns nil if
|
|
@@ -119,7 +121,13 @@ module Entitlements
|
|
|
119
121
|
downcased_attrs = attrs == "*" ? "*" : attrs.map { |a| a.downcase }
|
|
120
122
|
|
|
121
123
|
result = {}
|
|
122
|
-
ldap.search(
|
|
124
|
+
search_succeeded = ldap.search(
|
|
125
|
+
base: base,
|
|
126
|
+
filter: filter,
|
|
127
|
+
attributes: downcased_attrs,
|
|
128
|
+
scope: scope,
|
|
129
|
+
return_result: false
|
|
130
|
+
) do |entry|
|
|
123
131
|
result_key = index == :dn ? entry.dn : entry[index]
|
|
124
132
|
unless result_key
|
|
125
133
|
raise EntryError, "#{entry.dn} has no value for #{index.inspect}"
|
|
@@ -132,6 +140,7 @@ module Entitlements
|
|
|
132
140
|
|
|
133
141
|
result[result_key] = entry
|
|
134
142
|
end
|
|
143
|
+
remember_existing_dn(base) if search_succeeded
|
|
135
144
|
|
|
136
145
|
Entitlements.logger.debug "Completed search: #{result.keys.size} result(s)"
|
|
137
146
|
|
|
@@ -145,6 +154,7 @@ module Entitlements
|
|
|
145
154
|
# Returns true if the entry exists, false otherwise.
|
|
146
155
|
Contract String => C::Bool
|
|
147
156
|
def exists?(dn)
|
|
157
|
+
return true if known_existing_dn?(dn)
|
|
148
158
|
read(dn).is_a?(Net::LDAP::Entry)
|
|
149
159
|
end
|
|
150
160
|
|
|
@@ -180,7 +190,10 @@ module Entitlements
|
|
|
180
190
|
|
|
181
191
|
ldap.delete(dn: dn)
|
|
182
192
|
operation_result = ldap.get_operation_result
|
|
183
|
-
|
|
193
|
+
if operation_result["code"] == 0
|
|
194
|
+
forget_dn(dn)
|
|
195
|
+
return true
|
|
196
|
+
end
|
|
184
197
|
Entitlements.logger.error "Error deleting #{dn}: #{operation_result['message']}"
|
|
185
198
|
false
|
|
186
199
|
end
|
|
@@ -215,11 +228,24 @@ module Entitlements
|
|
|
215
228
|
|
|
216
229
|
attr_reader :addr, :bindpw
|
|
217
230
|
|
|
218
|
-
|
|
231
|
+
def known_existing_dn?(dn)
|
|
232
|
+
@known_existing_dns_mutex.synchronize { @known_existing_dns.key?(dn) }
|
|
233
|
+
end
|
|
234
|
+
|
|
235
|
+
def remember_existing_dn(dn)
|
|
236
|
+
@known_existing_dns_mutex.synchronize { @known_existing_dns[dn] = true }
|
|
237
|
+
end
|
|
238
|
+
|
|
239
|
+
def forget_dn(dn)
|
|
240
|
+
@known_existing_dns_mutex.synchronize { @known_existing_dns.delete(dn) }
|
|
241
|
+
@dn_cache&.delete(dn)
|
|
242
|
+
end
|
|
243
|
+
|
|
244
|
+
# The LDAP object is initialized and its credentials are validated on demand.
|
|
219
245
|
#
|
|
220
246
|
# Takes no arguments.
|
|
221
247
|
#
|
|
222
|
-
# Returns a Net::LDAP object
|
|
248
|
+
# Returns a Net::LDAP object configured to connect and bind for each operation.
|
|
223
249
|
Contract C::None => Net::LDAP
|
|
224
250
|
def ldap
|
|
225
251
|
@ldap ||= begin
|
data/lib/entitlements.rb
CHANGED
|
@@ -16,9 +16,11 @@ end
|
|
|
16
16
|
# :nocov:
|
|
17
17
|
|
|
18
18
|
require "contracts"
|
|
19
|
+
require "datadog/statsd"
|
|
19
20
|
require "erb"
|
|
20
21
|
require "logger"
|
|
21
22
|
require "ostruct"
|
|
23
|
+
require "resolv"
|
|
22
24
|
require "stringio"
|
|
23
25
|
require "uri"
|
|
24
26
|
require "yaml"
|
|
@@ -89,6 +91,7 @@ module Entitlements
|
|
|
89
91
|
@config_file = nil
|
|
90
92
|
@config_path_override = nil
|
|
91
93
|
@person_extra_methods = {}
|
|
94
|
+
@statsd = nil
|
|
92
95
|
|
|
93
96
|
reset_extras!
|
|
94
97
|
Entitlements::Data::Groups::Calculated.reset!
|
|
@@ -354,6 +357,53 @@ module Entitlements
|
|
|
354
357
|
end
|
|
355
358
|
# :nocov:
|
|
356
359
|
|
|
360
|
+
def self.statsd
|
|
361
|
+
@statsd ||= build_statsd
|
|
362
|
+
end
|
|
363
|
+
|
|
364
|
+
def self.set_statsd(statsd)
|
|
365
|
+
@statsd = statsd
|
|
366
|
+
end
|
|
367
|
+
|
|
368
|
+
def self.close_statsd
|
|
369
|
+
@statsd&.close
|
|
370
|
+
@statsd = nil
|
|
371
|
+
end
|
|
372
|
+
|
|
373
|
+
def self.build_statsd
|
|
374
|
+
host = Resolv.getaddress(ENV.fetch("DOGSTATSD_HOST", "localhost"))
|
|
375
|
+
port = Integer(ENV.fetch("DOGSTATSD_PORT", 28_125))
|
|
376
|
+
tags = [
|
|
377
|
+
"application:entitlements",
|
|
378
|
+
"kube_pod_name:#{ENV.fetch('KUBE_POD_NAME', 'not-on-kubernetes')}",
|
|
379
|
+
"app_env:#{ENV.fetch('APP_ENV', 'development')}",
|
|
380
|
+
"deployment_id:#{metric_deployment_id}"
|
|
381
|
+
]
|
|
382
|
+
Datadog::Statsd.new(host, port, tags: tags)
|
|
383
|
+
end
|
|
384
|
+
|
|
385
|
+
def self.metric_deployment_id
|
|
386
|
+
ENV["HEAVEN_DEPLOYMENT_ID"] || ENV["GITHUB_RUN_ID"] || "not-in-deployment"
|
|
387
|
+
end
|
|
388
|
+
|
|
389
|
+
def self.timed_operation(phase:, provider: nil, target: nil, span: "leaf", concurrent: false, count: nil)
|
|
390
|
+
tags = [
|
|
391
|
+
"phase:#{phase}",
|
|
392
|
+
"status:error",
|
|
393
|
+
"span:#{span}",
|
|
394
|
+
"concurrent:#{concurrent}"
|
|
395
|
+
]
|
|
396
|
+
tags << "provider:#{provider}" if provider
|
|
397
|
+
tags << "target:#{target}" if target
|
|
398
|
+
tags << "count:#{count}" if count
|
|
399
|
+
|
|
400
|
+
statsd.time("entitlements.operation.duration", tags: tags) do
|
|
401
|
+
result = yield
|
|
402
|
+
tags[1] = "status:success"
|
|
403
|
+
result
|
|
404
|
+
end
|
|
405
|
+
end
|
|
406
|
+
|
|
357
407
|
# Calculate - This runs the entitlements logic to calculate the differences, ultimately
|
|
358
408
|
# populating a cache and returning a list of actions. The cache and actions can then be
|
|
359
409
|
# consumed by `execute` to implement the changes.
|
|
@@ -363,14 +413,22 @@ module Entitlements
|
|
|
363
413
|
# Returns the array of actions.
|
|
364
414
|
Contract C::None => C::ArrayOf[Entitlements::Models::Action]
|
|
365
415
|
def self.calculate
|
|
416
|
+
timed_operation(phase: "calculate_total", span: "parent") { calculate_actions }
|
|
417
|
+
end
|
|
418
|
+
|
|
419
|
+
def self.calculate_actions
|
|
366
420
|
# Load extras that are configured.
|
|
367
|
-
|
|
421
|
+
if Entitlements.config.key?("extras")
|
|
422
|
+
timed_operation(phase: "load_extras") { Entitlements.load_extras }
|
|
423
|
+
end
|
|
368
424
|
|
|
369
425
|
# Pre-fetch people from configured people data sources.
|
|
370
|
-
Entitlements.prefetch_people
|
|
426
|
+
timed_operation(phase: "prefetch_people") { Entitlements.prefetch_people }
|
|
371
427
|
|
|
372
428
|
# Register filters that are configured.
|
|
373
|
-
|
|
429
|
+
if Entitlements.config.key?("filters")
|
|
430
|
+
timed_operation(phase: "register_filters") { Entitlements.register_filters }
|
|
431
|
+
end
|
|
374
432
|
|
|
375
433
|
# Keep track of the total change count.
|
|
376
434
|
cache[:change_count] = 0
|
|
@@ -385,8 +443,9 @@ module Entitlements
|
|
|
385
443
|
Concurrent::Future.execute({ executor: thread_pool }) do
|
|
386
444
|
group_start = Time.now
|
|
387
445
|
logger.debug("Begin prefetch and validate for #{group_name}")
|
|
388
|
-
|
|
389
|
-
obj.
|
|
446
|
+
provider = Entitlements.config["groups"].fetch(group_name).fetch("type")
|
|
447
|
+
timed_operation(phase: "prefetch", provider: provider, target: group_name, concurrent: true) { obj.prefetch }
|
|
448
|
+
timed_operation(phase: "validate", provider: provider, target: group_name, concurrent: true) { obj.validate }
|
|
390
449
|
logger.debug("Finished prefetch and validate for #{group_name} in #{Time.now - group_start}")
|
|
391
450
|
end
|
|
392
451
|
end
|
|
@@ -398,7 +457,8 @@ module Entitlements
|
|
|
398
457
|
calc_start = Time.now
|
|
399
458
|
actions = []
|
|
400
459
|
Entitlements.child_classes.map do |group_name, obj|
|
|
401
|
-
|
|
460
|
+
provider = Entitlements.config["groups"].fetch(group_name).fetch("type")
|
|
461
|
+
timed_operation(phase: "calculate", provider: provider, target: group_name) { obj.calculate }
|
|
402
462
|
if obj.change_count > 0
|
|
403
463
|
logger.debug "Group #{group_name.inspect} contributes #{obj.change_count} change(s)."
|
|
404
464
|
cache[:change_count] += obj.change_count
|
|
@@ -422,8 +482,14 @@ module Entitlements
|
|
|
422
482
|
actions: C::ArrayOf[Entitlements::Models::Action]
|
|
423
483
|
] => nil
|
|
424
484
|
def self.execute(actions:)
|
|
485
|
+
timed_operation(phase: "execute_total", span: "parent") { execute_actions(actions: actions) }
|
|
486
|
+
end
|
|
487
|
+
|
|
488
|
+
def self.execute_actions(actions:)
|
|
425
489
|
# Set up auditors.
|
|
426
|
-
Entitlements.auditors.each
|
|
490
|
+
Entitlements.auditors.each do |auditor|
|
|
491
|
+
timed_operation(phase: "audit_setup", provider: auditor.provider_id) { auditor.setup }
|
|
492
|
+
end
|
|
427
493
|
|
|
428
494
|
# Track any raised exception to pass to the auditors.
|
|
429
495
|
provider_exception = nil
|
|
@@ -433,14 +499,18 @@ module Entitlements
|
|
|
433
499
|
# Sort the child classes by priority
|
|
434
500
|
begin
|
|
435
501
|
# Pre-apply changes for each class.
|
|
436
|
-
Entitlements.child_classes.each do |
|
|
437
|
-
|
|
502
|
+
Entitlements.child_classes.each do |group_name, obj|
|
|
503
|
+
provider = Entitlements.config["groups"].fetch(group_name).fetch("type")
|
|
504
|
+
timed_operation(phase: "preapply", provider: provider, target: group_name) { obj.preapply }
|
|
438
505
|
end
|
|
439
506
|
|
|
440
507
|
# Apply changes from all actions.
|
|
441
508
|
actions.each do |action|
|
|
442
509
|
obj = Entitlements.child_classes.fetch(action.ou)
|
|
443
|
-
|
|
510
|
+
provider = Entitlements.config["groups"].fetch(action.ou).fetch("type")
|
|
511
|
+
timed_operation(phase: "apply", provider: provider, target: action.ou, count: 1) do
|
|
512
|
+
obj.apply(action)
|
|
513
|
+
end
|
|
444
514
|
successful_actions.add(action.dn)
|
|
445
515
|
end
|
|
446
516
|
rescue => e
|
|
@@ -457,11 +527,13 @@ module Entitlements
|
|
|
457
527
|
logger.debug "Recording data to #{Entitlements.auditors.size} audit provider(s)"
|
|
458
528
|
Entitlements.auditors.each do |audit|
|
|
459
529
|
begin
|
|
460
|
-
audit.
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
530
|
+
timed_operation(phase: "audit_commit", provider: audit.provider_id) do
|
|
531
|
+
audit.commit(
|
|
532
|
+
actions: actions,
|
|
533
|
+
successful_actions: successful_actions,
|
|
534
|
+
provider_exception: provider_exception
|
|
535
|
+
)
|
|
536
|
+
end
|
|
465
537
|
logger.debug "Audit #{audit.description} completed successfully"
|
|
466
538
|
rescue => e
|
|
467
539
|
logger.error "Audit #{audit.description} failed: #{e.class} #{e.message}"
|
|
@@ -564,7 +636,9 @@ module Entitlements
|
|
|
564
636
|
|
|
565
637
|
objects = people_data_sources.map do |ds_name, ds_config|
|
|
566
638
|
people_obj = Entitlements::Data::People.new_from_config(ds_config)
|
|
567
|
-
|
|
639
|
+
timed_operation(phase: "prefetch_people_source", provider: ds_config.fetch("type"), target: ds_name) do
|
|
640
|
+
people_obj.read
|
|
641
|
+
end
|
|
568
642
|
[ds_name, people_obj]
|
|
569
643
|
end.to_h
|
|
570
644
|
|
data/lib/version.rb
CHANGED
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: entitlements-app
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 1.2.
|
|
4
|
+
version: 1.2.3
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- GitHub, Inc. Security Ops
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: bin
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date:
|
|
11
|
+
date: 2026-09-14 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: concurrent-ruby
|
|
@@ -30,6 +30,20 @@ dependencies:
|
|
|
30
30
|
- - ">="
|
|
31
31
|
- !ruby/object:Gem::Version
|
|
32
32
|
version: 1.3.1
|
|
33
|
+
- !ruby/object:Gem::Dependency
|
|
34
|
+
name: dogstatsd-ruby
|
|
35
|
+
requirement: !ruby/object:Gem::Requirement
|
|
36
|
+
requirements:
|
|
37
|
+
- - "~>"
|
|
38
|
+
- !ruby/object:Gem::Version
|
|
39
|
+
version: '5.7'
|
|
40
|
+
type: :runtime
|
|
41
|
+
prerelease: false
|
|
42
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
43
|
+
requirements:
|
|
44
|
+
- - "~>"
|
|
45
|
+
- !ruby/object:Gem::Version
|
|
46
|
+
version: '5.7'
|
|
33
47
|
- !ruby/object:Gem::Dependency
|
|
34
48
|
name: faraday
|
|
35
49
|
requirement: !ruby/object:Gem::Requirement
|