diff --git a/elixir/apps/domain/lib/domain/flows.ex b/elixir/apps/domain/lib/domain/flows.ex index 4a22c99f5..8a6fd229b 100644 --- a/elixir/apps/domain/lib/domain/flows.ex +++ b/elixir/apps/domain/lib/domain/flows.ex @@ -32,7 +32,7 @@ defmodule Domain.Flows do with :ok <- Auth.ensure_has_permissions(subject, Authorizer.create_flows_permission()), {:ok, resource} <- Resources.fetch_and_authorize_resource_by_id(resource_id, subject, opts), - {:ok, policy} <- fetch_conforming_policy(resource, client) do + {:ok, policy, conformation_expires_at} <- fetch_conforming_policy(resource, client) do flow = Flow.Changeset.create(%{ token_id: token_id, @@ -44,7 +44,7 @@ defmodule Domain.Flows do client_remote_ip: client_remote_ip, client_user_agent: client_user_agent, gateway_remote_ip: gateway_remote_ip, - expires_at: expires_at + expires_at: conformation_expires_at || expires_at }) |> Repo.insert!() @@ -55,8 +55,8 @@ defmodule Domain.Flows do defp fetch_conforming_policy(%Resources.Resource{} = resource, client) do Enum.reduce_while(resource.authorized_by_policies, {:error, []}, fn policy, {:error, acc} -> case Policies.ensure_client_conforms_policy_conditions(client, policy) do - :ok -> - {:halt, {:ok, policy}} + {:ok, expires_at} -> + {:halt, {:ok, policy, expires_at}} {:error, {:forbidden, violated_properties: violated_properties}} -> {:cont, {:error, violated_properties ++ acc}} @@ -66,8 +66,8 @@ defmodule Domain.Flows do {:error, violated_properties} -> {:error, {:forbidden, violated_properties: violated_properties}} - {:ok, policy} -> - {:ok, policy} + {:ok, policy, expires_at} -> + {:ok, policy, expires_at} end end diff --git a/elixir/apps/domain/lib/domain/policies.ex b/elixir/apps/domain/lib/domain/policies.ex index e2e750b01..57c960614 100644 --- a/elixir/apps/domain/lib/domain/policies.ex +++ b/elixir/apps/domain/lib/domain/policies.ex @@ -167,8 +167,8 @@ defmodule Domain.Policies do def ensure_client_conforms_policy_conditions(%Clients.Client{} = client, %Policy{} = policy) do case Condition.Evaluator.ensure_conforms(policy.conditions, client) do - :ok -> - :ok + {:ok, expires_at} -> + {:ok, expires_at} {:error, violated_properties} -> {:error, {:forbidden, violated_properties: violated_properties}} diff --git a/elixir/apps/domain/lib/domain/policies/condition/evaluator.ex b/elixir/apps/domain/lib/domain/policies/condition/evaluator.ex index 29d2af61d..818a95651 100644 --- a/elixir/apps/domain/lib/domain/policies/condition/evaluator.ex +++ b/elixir/apps/domain/lib/domain/policies/condition/evaluator.ex @@ -6,79 +6,118 @@ defmodule Domain.Policies.Condition.Evaluator do @days_of_week ~w[M T W R F S U] def ensure_conforms([], %Clients.Client{}) do - :ok + {:ok, nil} end def ensure_conforms(conditions, %Clients.Client{} = client) when is_list(conditions) do client = Repo.preload(client, :identity) conditions - |> Enum.reduce([], fn condition, violated_properties -> - cond do - conforms?(condition, client) -> violated_properties - condition.property in violated_properties -> violated_properties - true -> [condition.property | violated_properties] + |> Enum.reduce({[], nil}, fn condition, {violated_properties, min_expires_at} -> + if condition.property in violated_properties do + {violated_properties, min_expires_at} + else + case fetch_conformation_expiration(condition, client) do + {:ok, expires_at} -> + {violated_properties, min_expires_at(expires_at, min_expires_at)} + + :error -> + {[condition.property | violated_properties], min_expires_at} + end end end) |> case do - [] -> :ok - violated_properties -> {:error, Enum.reverse(violated_properties)} + {[], expires_at} -> {:ok, expires_at} + {violated_properties, _expires_at} -> {:error, Enum.reverse(violated_properties)} end end - def conforms?( + defp min_expires_at(expires_at, nil), do: expires_at + + defp min_expires_at(expires_at, min_expires_at), + do: Enum.min([expires_at, min_expires_at], DateTime) + + def fetch_conformation_expiration( %Condition{property: :remote_ip_location_region, operator: :is_in, values: values}, %Clients.Client{} = client ) do - client.last_seen_remote_ip_location_region in values + if client.last_seen_remote_ip_location_region in values do + {:ok, nil} + else + :error + end end - def conforms?( + def fetch_conformation_expiration( %Condition{property: :remote_ip_location_region, operator: :is_not_in, values: values}, %Clients.Client{} = client ) do - client.last_seen_remote_ip_location_region not in values + if client.last_seen_remote_ip_location_region in values do + :error + else + {:ok, nil} + end end - def conforms?( + def fetch_conformation_expiration( %Condition{property: :remote_ip, operator: :is_in_cidr, values: values}, %Clients.Client{} = client ) do - Enum.any?(values, fn cidr -> + Enum.reduce_while(values, :error, fn cidr, :error -> {:ok, inet} = Domain.Types.INET.cast(cidr) cidr = %{inet | netmask: inet.netmask || Domain.Types.CIDR.max_netmask(inet)} - Domain.Types.CIDR.contains?(cidr, client.last_seen_remote_ip) + + if Domain.Types.CIDR.contains?(cidr, client.last_seen_remote_ip) do + {:halt, {:ok, nil}} + else + {:cont, :error} + end end) end - def conforms?( + def fetch_conformation_expiration( %Condition{property: :remote_ip, operator: :is_not_in_cidr, values: values}, %Clients.Client{} = client ) do - Enum.all?(values, fn cidr -> + Enum.reduce_while(values, {:ok, nil}, fn cidr, {:ok, nil} -> {:ok, inet} = Domain.Types.INET.cast(cidr) cidr = %{inet | netmask: inet.netmask || Domain.Types.CIDR.max_netmask(inet)} - not Domain.Types.CIDR.contains?(cidr, client.last_seen_remote_ip) + + if Domain.Types.CIDR.contains?(cidr, client.last_seen_remote_ip) do + {:halt, :error} + else + {:cont, {:ok, nil}} + end end) end - def conforms?( + def fetch_conformation_expiration( %Condition{property: :provider_id, operator: :is_in, values: values}, %Clients.Client{} = client ) do client = Repo.preload(client, :identity) - client.identity.provider_id in values + + if client.identity.provider_id in values do + {:ok, nil} + else + :error + end end - def conforms?( + def fetch_conformation_expiration( %Condition{property: :provider_id, operator: :is_not_in, values: values}, %Clients.Client{} = client ) do client = Repo.preload(client, :identity) - client.identity.provider_id not in values + + if client.identity.provider_id in values do + :error + else + {:ok, nil} + end end - def conforms?( + def fetch_conformation_expiration( %Condition{ property: :current_utc_datetime, operator: :is_in_day_of_week_time_ranges, @@ -86,33 +125,75 @@ defmodule Domain.Policies.Condition.Evaluator do }, %Clients.Client{} ) do - datetime_in_day_of_the_week_time_ranges?(DateTime.utc_now(), values) + case find_day_of_the_week_time_range(values, DateTime.utc_now()) do + nil -> :error + expires_at -> {:ok, expires_at} + end end - def datetime_in_day_of_the_week_time_ranges?(datetime, dow_time_ranges) do + def find_day_of_the_week_time_range(dow_time_ranges, datetime) do dow_time_ranges |> parse_days_of_week_time_ranges() |> case do {:ok, dow_time_ranges} -> - Enum.any?(dow_time_ranges, fn {day, time_ranges} -> + dow_time_ranges + |> Enum.find_value(fn {day, time_ranges} -> + time_ranges = merge_joint_time_ranges(time_ranges) datetime_in_time_ranges?(datetime, day, time_ranges) end) {:error, _reason} -> - false + nil end end + @doc false + # Merge ranges, eg. 4-11,11-22 = 4-22 + def merge_joint_time_ranges(time_ranges) do + merged_time_ranges = + Enum.reduce(time_ranges, [], fn {start_time, end_time, timezone}, acc -> + index = + Enum.find_index(acc, fn {acc_start_time, acc_end_time, acc_timezone} -> + acc_timezone == timezone and + (time_in_range?(start_time, acc_start_time, acc_end_time) or + time_in_range?(end_time, acc_start_time, acc_end_time) or + time_in_range?(acc_start_time, start_time, end_time) or + time_in_range?(acc_end_time, start_time, end_time)) + end) + + if index == nil do + [{start_time, end_time, timezone}] ++ acc + else + {{acc_start_time, acc_end_time, _timezone}, acc} = List.pop_at(acc, index) + start_time = Enum.min([start_time, acc_start_time], Time) + end_time = Enum.max([end_time, acc_end_time], Time) + [{start_time, end_time, timezone}] ++ acc + end + end) + |> Enum.reverse() + + if merged_time_ranges == time_ranges do + merged_time_ranges + else + merge_joint_time_ranges(merged_time_ranges) + end + end + + defp time_in_range?(time, range_start, range_end) do + Time.compare(range_start, time) in [:lt, :eq] and + Time.compare(time, range_end) in [:lt, :eq] + end + defp datetime_in_time_ranges?(datetime, day_of_the_week, time_ranges) do - Enum.any?(time_ranges, fn {start_time, end_time, timezone} -> - {:ok, datetime} = DateTime.shift_zone(datetime, timezone, Tzdata.TimeZoneDatabase) + Enum.find_value(time_ranges, fn {start_time, end_time, timezone} -> + datetime = DateTime.shift_zone!(datetime, timezone, Tzdata.TimeZoneDatabase) date = DateTime.to_date(datetime) time = DateTime.to_time(datetime) - if Enum.at(@days_of_week, Date.day_of_week(date) - 1) == day_of_the_week do - Time.compare(start_time, time) != :gt and Time.compare(time, end_time) != :gt - else - false + if Enum.at(@days_of_week, Date.day_of_week(date) - 1) == day_of_the_week and + Time.compare(start_time, time) != :gt and Time.compare(time, end_time) != :gt do + DateTime.new!(date, end_time, timezone, Tzdata.TimeZoneDatabase) + |> DateTime.shift_zone!("UTC", Tzdata.TimeZoneDatabase) end end) end diff --git a/elixir/apps/domain/test/domain/flows_test.exs b/elixir/apps/domain/test/domain/flows_test.exs index d9dcbde78..c900ee60f 100644 --- a/elixir/apps/domain/test/domain/flows_test.exs +++ b/elixir/apps/domain/test/domain/flows_test.exs @@ -172,6 +172,13 @@ defmodule Domain.FlowsTest do actor_group2 = Fixtures.Actors.create_group(account: account) Fixtures.Actors.create_membership(account: account, actor: actor, group: actor_group2) + time = Time.utc_now() + one_hour_ago = Time.add(time, -1, :hour) + one_hour_in_future = Time.add(time, 1, :hour) + + date = Date.utc_today() + day_of_week = Enum.at(~w[M T W R F S U], Date.day_of_week(date) - 1) + Fixtures.Policies.create_policy( account: account, actor_group: actor_group2, @@ -181,6 +188,13 @@ defmodule Domain.FlowsTest do property: :remote_ip_location_region, operator: :is_not_in, values: [client.last_seen_remote_ip_location_region] + }, + %{ + property: :current_utc_datetime, + operator: :is_in_day_of_week_time_ranges, + values: [ + "#{day_of_week}/#{one_hour_ago}-#{one_hour_in_future}/UTC" + ] } ] ) @@ -189,6 +203,7 @@ defmodule Domain.FlowsTest do authorize_flow(client, gateway, resource.id, subject) assert flow.policy_id == policy.id + assert DateTime.diff(flow.expires_at, DateTime.new!(date, one_hour_in_future)) < 5 end test "creates a flow when all conditions for at least one of the policies are satisfied", %{ @@ -223,8 +238,10 @@ defmodule Domain.FlowsTest do ] ) - assert {:ok, _fetched_resource, _flow} = + assert {:ok, _fetched_resource, flow} = authorize_flow(client, gateway, resource.id, subject) + + assert flow.expires_at == subject.expires_at end test "creates a network flow for users", %{ diff --git a/elixir/apps/domain/test/domain/policies/condition/evaluator_test.exs b/elixir/apps/domain/test/domain/policies/condition/evaluator_test.exs index 0be4291ed..c218379df 100644 --- a/elixir/apps/domain/test/domain/policies/condition/evaluator_test.exs +++ b/elixir/apps/domain/test/domain/policies/condition/evaluator_test.exs @@ -5,7 +5,7 @@ defmodule Domain.Policies.Condition.EvaluatorTest do describe "ensure_conforms/2" do test "returns ok when there are no conditions" do client = %Domain.Clients.Client{} - assert ensure_conforms([], client) == :ok + assert ensure_conforms([], client) == {:ok, nil} end test "returns ok when all conditions are met" do @@ -27,7 +27,7 @@ defmodule Domain.Policies.Condition.EvaluatorTest do } ] - assert ensure_conforms(conditions, client) == :ok + assert ensure_conforms(conditions, client) == {:ok, nil} end test "returns error when all conditions are not met" do @@ -77,7 +77,7 @@ defmodule Domain.Policies.Condition.EvaluatorTest do end end - describe "conforms?/2" do + describe "fetch_conformation_expiration/2" do test "when client last seen remote ip location region is in or not in the values" do condition = %Domain.Policies.Condition{ property: :remote_ip_location_region, @@ -88,15 +88,17 @@ defmodule Domain.Policies.Condition.EvaluatorTest do last_seen_remote_ip_location_region: "US" } - assert conforms?(%{condition | operator: :is_in}, client) == true - assert conforms?(%{condition | operator: :is_not_in}, client) == false + assert fetch_conformation_expiration(%{condition | operator: :is_in}, client) == {:ok, nil} + assert fetch_conformation_expiration(%{condition | operator: :is_not_in}, client) == :error client = %Domain.Clients.Client{ last_seen_remote_ip_location_region: "CA" } - assert conforms?(%{condition | operator: :is_in}, client) == false - assert conforms?(%{condition | operator: :is_not_in}, client) == true + assert fetch_conformation_expiration(%{condition | operator: :is_in}, client) == :error + + assert fetch_conformation_expiration(%{condition | operator: :is_not_in}, client) == + {:ok, nil} end test "when client last seen remote ip is in or not in the CIDR values" do @@ -109,15 +111,20 @@ defmodule Domain.Policies.Condition.EvaluatorTest do last_seen_remote_ip: %Postgrex.INET{address: {192, 168, 0, 1}} } - assert conforms?(%{condition | operator: :is_in_cidr}, client) == true - assert conforms?(%{condition | operator: :is_not_in_cidr}, client) == false + assert fetch_conformation_expiration(%{condition | operator: :is_in_cidr}, client) == + {:ok, nil} + + assert fetch_conformation_expiration(%{condition | operator: :is_not_in_cidr}, client) == + :error client = %Domain.Clients.Client{ last_seen_remote_ip: %Postgrex.INET{address: {10, 168, 0, 1}} } - assert conforms?(%{condition | operator: :is_in_cidr}, client) == false - assert conforms?(%{condition | operator: :is_not_in_cidr}, client) == true + assert fetch_conformation_expiration(%{condition | operator: :is_in_cidr}, client) == :error + + assert fetch_conformation_expiration(%{condition | operator: :is_not_in_cidr}, client) == + {:ok, nil} end test "when client last seen remote ip is in or not in the IP values" do @@ -130,15 +137,20 @@ defmodule Domain.Policies.Condition.EvaluatorTest do last_seen_remote_ip: %Postgrex.INET{address: {192, 168, 0, 1}} } - assert conforms?(%{condition | operator: :is_in_cidr}, client) == true - assert conforms?(%{condition | operator: :is_not_in_cidr}, client) == false + assert fetch_conformation_expiration(%{condition | operator: :is_in_cidr}, client) == + {:ok, nil} + + assert fetch_conformation_expiration(%{condition | operator: :is_not_in_cidr}, client) == + :error client = %Domain.Clients.Client{ last_seen_remote_ip: %Postgrex.INET{address: {10, 168, 0, 1}} } - assert conforms?(%{condition | operator: :is_in_cidr}, client) == false - assert conforms?(%{condition | operator: :is_not_in_cidr}, client) == true + assert fetch_conformation_expiration(%{condition | operator: :is_in_cidr}, client) == :error + + assert fetch_conformation_expiration(%{condition | operator: :is_not_in_cidr}, client) == + {:ok, nil} end test "when client identity provider id is in or not in the values" do @@ -153,8 +165,8 @@ defmodule Domain.Policies.Condition.EvaluatorTest do } } - assert conforms?(%{condition | operator: :is_in}, client) == true - assert conforms?(%{condition | operator: :is_not_in}, client) == false + assert fetch_conformation_expiration(%{condition | operator: :is_in}, client) == {:ok, nil} + assert fetch_conformation_expiration(%{condition | operator: :is_not_in}, client) == :error client = %Domain.Clients.Client{ identity: %Domain.Auth.Identity{ @@ -162,12 +174,14 @@ defmodule Domain.Policies.Condition.EvaluatorTest do } } - assert conforms?(%{condition | operator: :is_in}, client) == false - assert conforms?(%{condition | operator: :is_not_in}, client) == true + assert fetch_conformation_expiration(%{condition | operator: :is_in}, client) == :error + + assert fetch_conformation_expiration(%{condition | operator: :is_not_in}, client) == + {:ok, nil} end test "when client current UTC datetime is in the day of the week time ranges" do - # this is tested separately in datetime_in_day_of_the_week_time_ranges?/2 + # this is deeply tested separately in find_day_of_the_week_time_range/2 condition = %Domain.Policies.Condition{ property: :current_utc_datetime, values: [] @@ -175,26 +189,57 @@ defmodule Domain.Policies.Condition.EvaluatorTest do client = %Domain.Clients.Client{} - assert conforms?(%{condition | operator: :is_in_day_of_week_time_ranges}, client) == false + assert fetch_conformation_expiration( + %{condition | operator: :is_in_day_of_week_time_ranges}, + client + ) == :error end end - describe "datetime_in_day_of_the_week_time_ranges?/2" do + describe "find_day_of_the_week_time_range/2" do test "returns true when datetime is in the day of the week time ranges" do # Friday datetime = ~U[2021-01-01 10:00:00Z] + # Exact match dow_time_ranges = ["F/10:00:00-10:00:00/UTC"] - assert datetime_in_day_of_the_week_time_ranges?(datetime, dow_time_ranges) == true - dow_time_ranges = ["F/10:00:00-11:00:00/UTC"] - assert datetime_in_day_of_the_week_time_ranges?(datetime, dow_time_ranges) == true + assert DateTime.compare( + find_day_of_the_week_time_range(dow_time_ranges, datetime), + ~U[2021-01-01 10:00:00Z] + ) == :eq - dow_time_ranges = ["F/09:00:00-10:00:00/UTC"] - assert datetime_in_day_of_the_week_time_ranges?(datetime, dow_time_ranges) == true + # Range start match + dow_time_ranges = ["F/10:00:00-11:00:00,20:00-22:00/UTC"] + assert DateTime.compare( + find_day_of_the_week_time_range(dow_time_ranges, datetime), + ~U[2021-01-01 11:00:00Z] + ) == :eq + + # Range end match + dow_time_ranges = ["F/09:00:00-10:00:00,11-22/UTC"] + + assert DateTime.compare( + find_day_of_the_week_time_range(dow_time_ranges, datetime), + ~U[2021-01-01 10:00:00Z] + ) == :eq + + # Entire day match dow_time_ranges = ["F/true/UTC"] - assert datetime_in_day_of_the_week_time_ranges?(datetime, dow_time_ranges) == true + + assert DateTime.compare( + find_day_of_the_week_time_range(dow_time_ranges, datetime), + ~U[2021-01-01 23:59:59Z] + ) == :eq + + # Finds greatest expiration time + dow_time_ranges = ["F/09:00:00-11:00:00,11-15,14-22/UTC"] + + assert DateTime.compare( + find_day_of_the_week_time_range(dow_time_ranges, datetime), + ~U[2021-01-01 22:00:00Z] + ) == :eq end test "returns false when datetime is not in the day of the week time ranges" do @@ -202,55 +247,209 @@ defmodule Domain.Policies.Condition.EvaluatorTest do datetime = ~U[2021-01-01 10:00:00Z] dow_time_ranges = ["F/09:00:00-09:59:59/UTC"] - assert datetime_in_day_of_the_week_time_ranges?(datetime, dow_time_ranges) == false + assert find_day_of_the_week_time_range(dow_time_ranges, datetime) == nil dow_time_ranges = ["F/10:00:01-11:00:00/UTC"] - assert datetime_in_day_of_the_week_time_ranges?(datetime, dow_time_ranges) == false + assert find_day_of_the_week_time_range(dow_time_ranges, datetime) == nil dow_time_ranges = ["M/09:00:00-11:00:00/UTC"] - assert datetime_in_day_of_the_week_time_ranges?(datetime, dow_time_ranges) == false + assert find_day_of_the_week_time_range(dow_time_ranges, datetime) == nil dow_time_ranges = ["U/true/UTC"] - assert datetime_in_day_of_the_week_time_ranges?(datetime, dow_time_ranges) == false + assert find_day_of_the_week_time_range(dow_time_ranges, datetime) == nil end test "handles different timezones" do - # Friday in UTC, Thursday in US/Pacific (UTC-8) + # 01:00 Friday in UTC, 07:00 Thursday in US/Pacific (UTC-8) datetime = ~U[2021-01-01 01:00:00Z] + # Thursday in US/Pacific ends at 07:59:59 UTC dow_time_ranges = ["R/true/US/Pacific"] - assert datetime_in_day_of_the_week_time_ranges?(datetime, dow_time_ranges) == true + assert DateTime.compare( + find_day_of_the_week_time_range(dow_time_ranges, datetime), + ~U[2021-01-01 07:59:59Z] + ) == :eq + + # Friday in UTC dow_time_ranges = ["F/true/UTC"] - assert datetime_in_day_of_the_week_time_ranges?(datetime, dow_time_ranges) == true + assert DateTime.compare( + find_day_of_the_week_time_range(dow_time_ranges, datetime), + ~U[2021-01-01 23:59:59Z] + ) == :eq + + # 19:00 Thursday in US/Pacific (UTC-8) = 03:00 Friday in UTC dow_time_ranges = ["R/15:00:00-19:00:00/US/Pacific"] - assert datetime_in_day_of_the_week_time_ranges?(datetime, dow_time_ranges) == true + assert DateTime.compare( + find_day_of_the_week_time_range(dow_time_ranges, datetime), + ~U[2021-01-01 03:00:00Z] + ) == :eq + + # given datetime is 07:00 Thursday in US/Pacific (UTC-8), so Friday in UTC should not match dow_time_ranges = ["R/00:00:00-02:00:00/US/Pacific"] - assert datetime_in_day_of_the_week_time_ranges?(datetime, dow_time_ranges) == false + assert find_day_of_the_week_time_range(dow_time_ranges, datetime) == nil + # Poland timezone is UTC+1, given datetime in UTC is 02:00 Friday in Poland dow_time_ranges = ["F/02:00:00-04:00:00/Poland"] - assert datetime_in_day_of_the_week_time_ranges?(datetime, dow_time_ranges) == true + + assert DateTime.compare( + find_day_of_the_week_time_range(dow_time_ranges, datetime), + ~U[2021-01-01 03:00:00Z] + ) == :eq end test "returns false when ranges are invalid" do datetime = ~U[2021-01-01 10:00:00Z] dow_time_ranges = ["F/10:00:00-09:59:59/UTC"] - assert datetime_in_day_of_the_week_time_ranges?(datetime, dow_time_ranges) == false + assert find_day_of_the_week_time_range(dow_time_ranges, datetime) == nil dow_time_ranges = ["F/11:00:00-12:00:00-/US/Pacific"] - assert datetime_in_day_of_the_week_time_ranges?(datetime, dow_time_ranges) == false + assert find_day_of_the_week_time_range(dow_time_ranges, datetime) == nil dow_time_ranges = ["F/false/UTC"] - assert datetime_in_day_of_the_week_time_ranges?(datetime, dow_time_ranges) == false + assert find_day_of_the_week_time_range(dow_time_ranges, datetime) == nil dow_time_ranges = ["F/10-11"] - assert datetime_in_day_of_the_week_time_ranges?(datetime, dow_time_ranges) == false + assert find_day_of_the_week_time_range(dow_time_ranges, datetime) == nil dow_time_ranges = ["F/10-11/"] - assert datetime_in_day_of_the_week_time_ranges?(datetime, dow_time_ranges) == false + assert find_day_of_the_week_time_range(dow_time_ranges, datetime) == nil + end + end + + describe "merge_joint_time_ranges/1" do + test "does nothing on empty time ranges" do + time_ranges = [] + assert merge_joint_time_ranges(time_ranges) == time_ranges + end + + test "does nothing on single time range" do + time_ranges = [{~T[10:00:00], ~T[11:00:00], "UTC"}] + assert merge_joint_time_ranges(time_ranges) == time_ranges + end + + test "does not merge overlapping time ranges that do not overlap" do + time_ranges = [ + {~T[10:00:00], ~T[11:00:00], "UTC"}, + {~T[11:00:01], ~T[12:00:00], "UTC"} + ] + + assert merge_joint_time_ranges(time_ranges) == time_ranges + + time_ranges = [ + {~T[09:00:00], ~T[10:00:00], "UTC"}, + {~T[11:00:00], ~T[12:00:00], "UTC"}, + {~T[10:00:01], ~T[10:00:02], "UTC"} + ] + + assert merge_joint_time_ranges(time_ranges) == time_ranges + end + + test "does not merge overlapping time ranges that have different timezones" do + time_ranges = [ + {~T[10:00:00], ~T[11:00:00], "UTC"}, + {~T[10:30:00], ~T[12:00:00], "PDT"} + ] + + assert merge_joint_time_ranges(time_ranges) == time_ranges + end + + test "merges overlapping time ranges" do + time_ranges = [ + {~T[10:00:00], ~T[11:00:00], "UTC"}, + {~T[10:30:00], ~T[12:00:00], "UTC"} + ] + + assert merge_joint_time_ranges(time_ranges) == [ + {~T[10:00:00], ~T[12:00:00], "UTC"} + ] + + time_ranges = [ + {~T[10:00:00], ~T[11:00:00], "UTC"}, + {~T[11:00:00], ~T[12:00:00], "UTC"} + ] + + assert merge_joint_time_ranges(time_ranges) == [ + {~T[10:00:00], ~T[12:00:00], "UTC"} + ] + + time_ranges = [ + {~T[10:00:00], ~T[11:00:00], "UTC"}, + {~T[09:00:00], ~T[10:00:00], "UTC"} + ] + + assert merge_joint_time_ranges(time_ranges) == [ + {~T[09:00:00], ~T[11:00:00], "UTC"} + ] + + time_ranges = [ + {~T[10:00:00], ~T[11:00:00], "UTC"}, + {~T[10:00:00], ~T[12:00:00], "UTC"} + ] + + assert merge_joint_time_ranges(time_ranges) == [ + {~T[10:00:00], ~T[12:00:00], "UTC"} + ] + + time_ranges = [ + {~T[10:00:00], ~T[11:00:00], "UTC"}, + {~T[09:00:00], ~T[12:00:00], "UTC"} + ] + + assert merge_joint_time_ranges(time_ranges) == [ + {~T[09:00:00], ~T[12:00:00], "UTC"} + ] + + time_ranges = [ + {~T[09:00:00], ~T[12:00:00], "UTC"}, + {~T[10:00:00], ~T[11:00:00], "UTC"} + ] + + assert merge_joint_time_ranges(time_ranges) == [ + {~T[09:00:00], ~T[12:00:00], "UTC"} + ] + end + + test "merges multiple overlapping time ranges" do + time_ranges = [ + {~T[09:00:00], ~T[10:00:00], "UTC"}, + {~T[11:00:00], ~T[12:00:00], "UTC"}, + {~T[10:00:00], ~T[11:00:00], "UTC"} + ] + + assert merge_joint_time_ranges(time_ranges) == [ + {~T[09:00:00], ~T[12:00:00], "UTC"} + ] + + time_ranges = [ + {~T[09:00:00], ~T[12:00:00], "UTC"}, + {~T[11:00:00], ~T[12:00:00], "UTC"}, + {~T[10:00:00], ~T[11:00:00], "UTC"}, + {~T[09:00:00], ~T[10:00:00], "UTC"}, + {~T[01:00:00], ~T[10:00:00], "UTC"} + ] + + assert merge_joint_time_ranges(time_ranges) == [ + {~T[01:00:00], ~T[12:00:00], "UTC"} + ] + end + + test "merges two sets of overlapping time ranges" do + time_ranges = [ + {~T[09:00:00], ~T[12:00:00], "UTC"}, + {~T[11:00:00], ~T[13:00:00], "UTC"}, + {~T[10:00:00], ~T[11:00:00], "UTC"}, + {~T[02:00:00], ~T[05:00:00], "UTC"}, + {~T[01:00:00], ~T[08:00:00], "UTC"} + ] + + assert merge_joint_time_ranges(time_ranges) == [ + {~T[09:00:00], ~T[13:00:00], "UTC"}, + {~T[01:00:00], ~T[08:00:00], "UTC"} + ] end end diff --git a/elixir/apps/domain/test/domain/policies_test.exs b/elixir/apps/domain/test/domain/policies_test.exs index fbd17fc2a..63780e9e6 100644 --- a/elixir/apps/domain/test/domain/policies_test.exs +++ b/elixir/apps/domain/test/domain/policies_test.exs @@ -979,7 +979,7 @@ defmodule Domain.PoliciesTest do ] } - assert ensure_client_conforms_policy_conditions(client, policy) == :ok + assert ensure_client_conforms_policy_conditions(client, policy) == {:ok, nil} end test "returns error when client conforms to policy conditions", %{} do diff --git a/elixir/apps/web/lib/web/live/policies/components.ex b/elixir/apps/web/lib/web/live/policies/components.ex index 8a1f823af..955b17ad1 100644 --- a/elixir/apps/web/lib/web/live/policies/components.ex +++ b/elixir/apps/web/lib/web/live/policies/components.ex @@ -256,6 +256,11 @@ defmodule Web.Policies.Components do providers={@providers} disabled={@policy_conditions_enabled? == false} /> + <.current_utc_datetime_condition_form + form={@form} + timezone={@timezone} + disabled={@policy_conditions_enabled? == false} + /> """ @@ -518,95 +523,93 @@ defmodule Web.Policies.Components do """ end - # TODO: Uncomment this once policy time conditions are finished - # defp current_utc_datetime_condition_form(assigns) do - # assigns = assign_new(assigns, :days_of_week, fn -> @days_of_week end) + defp current_utc_datetime_condition_form(assigns) do + assigns = assign_new(assigns, :days_of_week, fn -> @days_of_week end) - # ~H""" - #