Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion Gemfile.lock
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
PATH
remote: .
specs:
entitlements-app (1.2.3)
entitlements-app (1.2.4)
concurrent-ruby (~> 1.3, >= 1.3.1)
dogstatsd-ruby (~> 5.7)
faraday (~> 2.0)
Expand Down
19 changes: 19 additions & 0 deletions lib/entitlements.rb
Original file line number Diff line number Diff line change
Expand Up @@ -90,13 +90,32 @@ def self.reset!
@config = nil
@config_file = nil
@config_path_override = nil
@evaluation_time = nil
@person_extra_methods = {}
@statsd = nil

reset_extras!
Entitlements::Data::Groups::Calculated.reset!
end

# Return the fixed time used for the current date-sensitive entitlement evaluation.
#
# Returns a Time.
Contract C::None => Time
def self.evaluation_time
@evaluation_time ||= Time.now
end

# Set the time used for date-sensitive entitlement evaluation.
#
# value - A Time.
#
# Returns the supplied Time.
Contract Time => Time
def self.evaluation_time=(value)
@evaluation_time = value
end

def self.reset_extras!
extras_loaded = @extras_loaded
if extras_loaded
Expand Down
4 changes: 2 additions & 2 deletions lib/entitlements/data/groups/calculated/base.rb
Original file line number Diff line number Diff line change
Expand Up @@ -217,7 +217,7 @@ def expired?(expiration, context)
return false if expiration.nil? || expiration.strip.empty?
if expiration =~ /\A(\d{4})-(\d{2})-(\d{2})\z/
year, month, day = Regexp.last_match(1).to_i, Regexp.last_match(2).to_i, Regexp.last_match(3).to_i
return Time.utc(year, month, day, 0, 0, 0) <= Time.now.utc
return Time.utc(year, month, day, 0, 0, 0) <= Entitlements.evaluation_time.utc
end
message = "Invalid expiration date #{expiration.inspect} in #{context} (expected format: YYYY-MM-DD)"
raise ArgumentError, message
Expand Down Expand Up @@ -340,7 +340,7 @@ def handle_or(rule)
# Returns C::SetOf[Entitlements::Models::Person] from a recursive call.
def handle_and(rule)
ensure_type!("and", rule, Array)
return result unless rule.any?
return Set.new unless rule.any?

first_rule = rule.shift
ensure_type!("and", first_rule, Hash)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ def modify(result)
end

# If the date is in the future, leave the entitlement unchanged.
return false if parse_date > Time.now.utc.to_date
return false if parse_date > Entitlements.evaluation_time.utc.to_date

# Empty the group. Set metadata allowing no members. Return true to indicate modification.
rs.metadata["no_members_ok"] = true
Expand Down
12 changes: 7 additions & 5 deletions lib/entitlements/data/groups/calculated/text.rb
Original file line number Diff line number Diff line change
Expand Up @@ -21,10 +21,12 @@ class Text < Entitlements::Data::Groups::Calculated::Base
# Returns a Set[String] with DN's of the people in the group.
Contract C::None => C::Or[:calculating, C::SetOf[Entitlements::Models::Person]]
def members
@members ||= begin
Entitlements.logger.debug "Calculating members from #{filename}"
members_from_rules(rules)
end
return @members if @members

Entitlements.logger.debug "Calculating members from #{filename}"
result = members_from_rules(rules)
@members = result unless result == :calculating
result
end

# Standard interface: Get the description of this group.
Expand Down Expand Up @@ -182,7 +184,7 @@ def rules
if parsed_data.key?("modifier_expiration") && affirmative.empty?
exp_date = parsed_data.fetch("modifier_expiration").fetch("=").first.fetch(:key)
date = Entitlements::Util::Util.parse_date(exp_date)
return {"always" => false} if date <= Time.now.utc.to_date
return {"always" => false} if date <= Entitlements.evaluation_time.utc.to_date
end

# There has to be at least one affirmative condition, not just all negative ones.
Expand Down
10 changes: 6 additions & 4 deletions lib/entitlements/data/groups/calculated/yaml.rb
Original file line number Diff line number Diff line change
Expand Up @@ -18,10 +18,12 @@ class YAML < Entitlements::Data::Groups::Calculated::Base
# Returns a Set[String] with DN's of the people in the group.
Contract C::None => C::Or[:calculating, C::SetOf[Entitlements::Models::Person]]
def members
@members ||= begin
Entitlements.logger.debug "Calculating members from #{filename}"
members_from_rules(rules)
end
return @members if @members

Entitlements.logger.debug "Calculating members from #{filename}"
result = members_from_rules(rules)
@members = result unless result == :calculating
result
end

# Standard interface: Get the description of this group.
Expand Down
2 changes: 1 addition & 1 deletion lib/version.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,6 @@

module Entitlements
module Version
VERSION = "1.2.3"
VERSION = "1.2.4"
end
end
9 changes: 9 additions & 0 deletions spec/unit/entitlements/data/groups/calculated/base_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -158,6 +158,15 @@
end
end

context "with an empty 'and' rule set" do
let(:file) { fixture("ldap-config/logic_tests/simple_and.yaml") }
let(:obj) { Entitlements::Data::Groups::Calculated::YAML.new(filename: file, config: config) }

it "returns an empty set" do
expect(obj.send(:handle_and, [])).to eq(Set.new)
end
end

context "with a simple 'or' rule set" do
let(:file) { fixture("ldap-config/logic_tests/simple_or.yaml") }
let(:obj) { Entitlements::Data::Groups::Calculated::YAML.new(filename: file, config: config) }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,15 @@
obj = Entitlements::Data::Groups::Calculated.read("cn=expired-text-empty,ou=Felines,ou=Groups,dc=example,dc=net")
expect(obj.members).to eq(Set.new)
end

it "uses the configured evaluation time" do
Entitlements.evaluation_time = Time.utc(2000, 1, 1)
allow(Entitlements::Util::Util).to receive(:path_for_group).with(ou_key).and_return(fixture("ldap-config/#{ou_key}"))
Entitlements::Data::Groups::Calculated.read_all(ou_key, cfg_obj)
obj = Entitlements::Data::Groups::Calculated.read("cn=expired-text-empty,ou=Felines,ou=Groups,dc=example,dc=net")
expected_result = %w[russianblue mainecoon]
expect(obj.members).to eq(Set.new(expected_result.map { |name| people_obj.read[name] }))
end
end

context "non-expired non-expired yaml file (date as date)" do
Expand Down
8 changes: 8 additions & 0 deletions spec/unit/entitlements/data/groups/calculated/text_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,14 @@
answer_set = Set.new(answer_array)
expect(result_set).to eq(answer_set)
end

it "does not cache the calculating sentinel" do
members = Set.new([people_obj.read["blackmanx"]])
allow(subject).to receive(:members_from_rules).and_return(:calculating, members)

expect(subject.members).to eq(:calculating)
expect(subject.members).to eq(members)
end
end

describe "#description" do
Expand Down
10 changes: 10 additions & 0 deletions spec/unit/entitlements/data/groups/calculated/yaml_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,16 @@
expect(result.size).to eq(2)
expect(result.map { |i| i.uid }.sort).to eq(answer)
end

it "does not cache the calculating sentinel" do
filename = fixture("ldap-config/filters/no-filters.yaml")
subject = described_class.new(filename: filename)
members = Set.new([people_obj.read["blackmanx"]])
allow(subject).to receive(:members_from_rules).and_return(:calculating, members)

expect(subject.members).to eq(:calculating)
expect(subject.members).to eq(members)
end
end

describe "#description" do
Expand Down
25 changes: 25 additions & 0 deletions spec/unit/entitlements_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,31 @@
describe Entitlements do
let(:subject) { Entitlements }

describe "#evaluation_time" do
it "uses one timestamp until Entitlements state is reset" do
first_time = Time.utc(2026, 9, 16, 23, 59, 59)
next_time = Time.utc(2026, 9, 17, 0, 0, 0)
allow(Time).to receive(:now).and_return(first_time, next_time)

expect(subject.evaluation_time).to eq(first_time)
expect(subject.evaluation_time).to eq(first_time)

subject.reset!

expect(subject.evaluation_time).to eq(next_time)
end

it "preserves an explicitly configured evaluation timestamp" do
configured_time = Time.utc(2026, 9, 16, 12, 0, 0)

subject.evaluation_time = configured_time

expect(subject.evaluation_time).to eq(configured_time)
expect(Time).not_to receive(:now)
expect(subject.evaluation_time).to eq(configured_time)
end
end

describe "#config" do
before(:each) do
ENV["TEST_ERB_VARIABLE"] = "Hello, ERB world!"
Expand Down
Loading