mirror of
https://github.com/outbackdingo/firezone.git
synced 2026-01-28 02:18:50 +00:00
feat(portal): Add Policy conditions (#5144)
Now policies can have additional conditions based on Client location (country or IP range), IdP provider used for sign in or the current time of the day at a given timezone. This covers use cases where employees can access the production system only from certain countries (states can be added later) or when contractors can only access internal tools during working hours. Closes https://github.com/firezone/firezone/issues/4743 Closes #4742 Closes #4741 Closes #4740 <img width="1728" alt="Screenshot 2024-05-31 at 13 50 53" src="https://github.com/firezone/firezone/assets/1877644/55f509f2-0f49-4edb-8c03-7a5a6d884ccc"> <img width="1728" alt="Screenshot 2024-05-31 at 13 50 56" src="https://github.com/firezone/firezone/assets/1877644/756bb03f-4024-4978-ac85-6daa918ae037"> <img width="1728" alt="Screenshot 2024-05-31 at 13 51 01" src="https://github.com/firezone/firezone/assets/1877644/cf159a86-077f-4ada-9952-9e8d399d0dc1"> <img width="1728" alt="Screenshot 2024-05-31 at 13 51 03" src="https://github.com/firezone/firezone/assets/1877644/c070719e-2d4b-41bd-ad03-430baf2dbe9b"> <img width="676" alt="Screenshot 2024-05-31 at 14 56 06" src="https://github.com/firezone/firezone/assets/1877644/435a4951-479d-4371-99c4-29a055348175">
This commit is contained in:
@@ -97,6 +97,7 @@ services:
|
||||
STATIC_SEEDS: "true"
|
||||
# Feature flags
|
||||
FLOW_ACTIVITIES_ENABLED: "true"
|
||||
FEATURE_POLICY_CONDITIONS_ENABLED: "true"
|
||||
MULTI_SITE_RESOURCES_ENABLED: "true"
|
||||
SELF_HOSTED_RELAYS_ENABLED: "true"
|
||||
IDP_SYNC_ENABLED: "true"
|
||||
@@ -165,6 +166,7 @@ services:
|
||||
STATIC_SEEDS: "true"
|
||||
# Feature flags
|
||||
FLOW_ACTIVITIES_ENABLED: "true"
|
||||
FEATURE_POLICY_CONDITIONS_ENABLED: "true"
|
||||
MULTI_SITE_RESOURCES_ENABLED: "true"
|
||||
SELF_HOSTED_RELAYS_ENABLED: "true"
|
||||
IDP_SYNC_ENABLED: "true"
|
||||
@@ -227,6 +229,7 @@ services:
|
||||
STATIC_SEEDS: "true"
|
||||
# Feature flags
|
||||
FLOW_ACTIVITIES_ENABLED: "true"
|
||||
FEATURE_POLICY_CONDITIONS_ENABLED: "true"
|
||||
MULTI_SITE_RESOURCES_ENABLED: "true"
|
||||
SELF_HOSTED_RELAYS_ENABLED: "true"
|
||||
IDP_SYNC_ENABLED: "true"
|
||||
@@ -295,6 +298,7 @@ services:
|
||||
STATIC_SEEDS: "true"
|
||||
# Feature flags
|
||||
FLOW_ACTIVITIES_ENABLED: "true"
|
||||
FEATURE_POLICY_CONDITIONS_ENABLED: "true"
|
||||
MULTI_SITE_RESOURCES_ENABLED: "true"
|
||||
SELF_HOSTED_RELAYS_ENABLED: "true"
|
||||
IDP_SYNC_ENABLED: "true"
|
||||
|
||||
@@ -465,6 +465,12 @@ defmodule API.Client.Channel do
|
||||
OpenTelemetry.Tracer.set_status(:error, "not_found")
|
||||
{:reply, {:error, %{reason: :not_found}}, socket}
|
||||
|
||||
{:error, {:forbidden, violated_properties: violated_properties}} ->
|
||||
OpenTelemetry.Tracer.set_status(:error, "forbidden")
|
||||
|
||||
{:reply, {:error, %{reason: :forbidden, violated_properties: violated_properties}},
|
||||
socket}
|
||||
|
||||
false ->
|
||||
OpenTelemetry.Tracer.set_status(:error, "offline")
|
||||
{:reply, {:error, %{reason: :offline}}, socket}
|
||||
@@ -521,6 +527,12 @@ defmodule API.Client.Channel do
|
||||
OpenTelemetry.Tracer.set_status(:error, "not_found")
|
||||
{:reply, {:error, %{reason: :not_found}}, socket}
|
||||
|
||||
{:error, {:forbidden, violated_properties: violated_properties}} ->
|
||||
OpenTelemetry.Tracer.set_status(:error, "forbidden")
|
||||
|
||||
{:reply, {:error, %{reason: :forbidden, violated_properties: violated_properties}},
|
||||
socket}
|
||||
|
||||
false ->
|
||||
OpenTelemetry.Tracer.set_status(:error, "offline")
|
||||
{:reply, {:error, %{reason: :offline}}, socket}
|
||||
|
||||
@@ -936,6 +936,49 @@ defmodule API.Client.ChannelTest do
|
||||
assert_reply ref, :error, %{reason: :offline}
|
||||
end
|
||||
|
||||
test "returns error when flow is not authorized due to failing conditions", %{
|
||||
account: account,
|
||||
client: client,
|
||||
actor_group: actor_group,
|
||||
gateway_group: gateway_group,
|
||||
gateway: gateway,
|
||||
socket: socket
|
||||
} do
|
||||
resource =
|
||||
Fixtures.Resources.create_resource(
|
||||
account: account,
|
||||
connections: [%{gateway_group_id: gateway_group.id}]
|
||||
)
|
||||
|
||||
Fixtures.Policies.create_policy(
|
||||
account: account,
|
||||
actor_group: actor_group,
|
||||
resource: resource,
|
||||
conditions: [
|
||||
%{
|
||||
property: :remote_ip_location_region,
|
||||
operator: :is_not_in,
|
||||
values: [client.last_seen_remote_ip_location_region]
|
||||
}
|
||||
]
|
||||
)
|
||||
|
||||
attrs = %{
|
||||
"resource_id" => resource.id,
|
||||
"gateway_id" => gateway.id,
|
||||
"payload" => "DNS_Q"
|
||||
}
|
||||
|
||||
:ok = Domain.Gateways.connect_gateway(gateway)
|
||||
|
||||
ref = push(socket, "reuse_connection", attrs)
|
||||
|
||||
assert_reply ref, :error, %{
|
||||
reason: :forbidden,
|
||||
violated_properties: [:remote_ip_location_region]
|
||||
}
|
||||
end
|
||||
|
||||
test "returns error when client has no policy allowing access to resource", %{
|
||||
account: account,
|
||||
socket: socket
|
||||
@@ -1117,6 +1160,50 @@ defmodule API.Client.ChannelTest do
|
||||
assert_reply ref, :error, %{reason: :not_found}
|
||||
end
|
||||
|
||||
test "returns error when flow is not authorized due to failing conditions", %{
|
||||
account: account,
|
||||
client: client,
|
||||
actor_group: actor_group,
|
||||
gateway_group: gateway_group,
|
||||
gateway: gateway,
|
||||
socket: socket
|
||||
} do
|
||||
resource =
|
||||
Fixtures.Resources.create_resource(
|
||||
account: account,
|
||||
connections: [%{gateway_group_id: gateway_group.id}]
|
||||
)
|
||||
|
||||
Fixtures.Policies.create_policy(
|
||||
account: account,
|
||||
actor_group: actor_group,
|
||||
resource: resource,
|
||||
conditions: [
|
||||
%{
|
||||
property: :remote_ip_location_region,
|
||||
operator: :is_not_in,
|
||||
values: [client.last_seen_remote_ip_location_region]
|
||||
}
|
||||
]
|
||||
)
|
||||
|
||||
attrs = %{
|
||||
"resource_id" => resource.id,
|
||||
"gateway_id" => gateway.id,
|
||||
"client_payload" => "RTC_SD",
|
||||
"client_preshared_key" => "PSK"
|
||||
}
|
||||
|
||||
:ok = Domain.Gateways.connect_gateway(gateway)
|
||||
|
||||
ref = push(socket, "request_connection", attrs)
|
||||
|
||||
assert_reply ref, :error, %{
|
||||
reason: :forbidden,
|
||||
violated_properties: [:remote_ip_location_region]
|
||||
}
|
||||
end
|
||||
|
||||
test "returns error when gateway is offline", %{
|
||||
dns_resource: resource,
|
||||
gateway: gateway,
|
||||
|
||||
@@ -4,6 +4,7 @@ defmodule Domain.Accounts.Features do
|
||||
@primary_key false
|
||||
embedded_schema do
|
||||
field :flow_activities, :boolean
|
||||
field :policy_conditions, :boolean
|
||||
field :multi_site_resources, :boolean
|
||||
field :traffic_filters, :boolean
|
||||
field :self_hosted_relays, :boolean
|
||||
|
||||
@@ -2,7 +2,15 @@ defmodule Domain.Accounts.Features.Changeset do
|
||||
use Domain, :changeset
|
||||
alias Domain.Accounts.Features
|
||||
|
||||
@fields ~w[flow_activities multi_site_resources traffic_filters self_hosted_relays idp_sync rest_api]a
|
||||
@fields ~w[
|
||||
flow_activities
|
||||
policy_conditions
|
||||
multi_site_resources
|
||||
traffic_filters
|
||||
self_hosted_relays
|
||||
idp_sync
|
||||
rest_api
|
||||
]a
|
||||
|
||||
def changeset(features \\ %Features{}, attrs) do
|
||||
features
|
||||
|
||||
@@ -646,6 +646,11 @@ defmodule Domain.Config.Definitions do
|
||||
"""
|
||||
defconfig(:feature_self_hosted_relays_enabled, :boolean, default: false)
|
||||
|
||||
@doc """
|
||||
Boolean flag to turn Policy Conditions functionality on/off for all accounts.
|
||||
"""
|
||||
defconfig(:feature_policy_conditions_enabled, :boolean, default: false)
|
||||
|
||||
@doc """
|
||||
Boolean flag to turn Multi-Site resources functionality on/off for all accounts.
|
||||
"""
|
||||
|
||||
@@ -5,14 +5,12 @@ defmodule Domain.Flows do
|
||||
require Ecto.Query
|
||||
require Logger
|
||||
|
||||
def authorize_flow(client, gateway, id, subject, opts \\ [])
|
||||
|
||||
def authorize_flow(
|
||||
%Clients.Client{
|
||||
id: client_id,
|
||||
account_id: account_id,
|
||||
actor_id: actor_id
|
||||
},
|
||||
} = client,
|
||||
%Gateways.Gateway{
|
||||
id: gateway_id,
|
||||
last_seen_remote_ip: gateway_remote_ip,
|
||||
@@ -29,15 +27,16 @@ defmodule Domain.Flows do
|
||||
user_agent: client_user_agent
|
||||
}
|
||||
} = subject,
|
||||
opts
|
||||
opts \\ []
|
||||
) 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) do
|
||||
Resources.fetch_and_authorize_resource_by_id(resource_id, subject, opts),
|
||||
{:ok, policy} <- fetch_conforming_policy(resource, client) do
|
||||
flow =
|
||||
Flow.Changeset.create(%{
|
||||
token_id: token_id,
|
||||
policy_id: resource.authorized_by_policy.id,
|
||||
policy_id: policy.id,
|
||||
client_id: client_id,
|
||||
gateway_id: gateway_id,
|
||||
resource_id: resource.id,
|
||||
@@ -53,27 +52,23 @@ defmodule Domain.Flows do
|
||||
end
|
||||
end
|
||||
|
||||
def authorize_flow(client, gateway, id, subject, _opts) do
|
||||
Logger.error("authorize_flow/4 called with invalid arguments",
|
||||
id: id,
|
||||
client: %{
|
||||
id: client.id,
|
||||
account_id: client.account_id,
|
||||
actor_id: client.actor_id,
|
||||
identity_id: client.identity_id
|
||||
},
|
||||
gateway: %{
|
||||
id: gateway.id,
|
||||
account_id: gateway.account_id
|
||||
},
|
||||
subject: %{
|
||||
account: %{id: subject.account.id, slug: subject.account.slug},
|
||||
actor: %{id: subject.actor.id, type: subject.actor.type},
|
||||
identity: %{id: subject.identity.id}
|
||||
}
|
||||
)
|
||||
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}}
|
||||
|
||||
{:error, :internal_error}
|
||||
{:error, {:forbidden, violated_properties: violated_properties}} ->
|
||||
{:cont, {:error, violated_properties ++ acc}}
|
||||
end
|
||||
end)
|
||||
|> case do
|
||||
{:error, violated_properties} ->
|
||||
{:error, {:forbidden, violated_properties: violated_properties}}
|
||||
|
||||
{:ok, policy} ->
|
||||
{:ok, policy}
|
||||
end
|
||||
end
|
||||
|
||||
def fetch_flow_by_id(id, %Auth.Subject{} = subject, opts \\ []) do
|
||||
|
||||
@@ -1,259 +1,268 @@
|
||||
defmodule Domain.Geo do
|
||||
@radius_of_earth_km 6371.0
|
||||
|
||||
@coordinates_by_code %{
|
||||
"AF" => {33, 65},
|
||||
"AX" => {60.116667, 19.9},
|
||||
"AL" => {41, 20},
|
||||
"DZ" => {28, 3},
|
||||
"AS" => {-14.33333333, -170},
|
||||
"AD" => {42.5, 1.5},
|
||||
"AO" => {-12.5, 18.5},
|
||||
"AI" => {18.25, -63.16666666},
|
||||
"AQ" => {-74.65, 4.48},
|
||||
"AG" => {17.05, -61.8},
|
||||
"AR" => {-34, -64},
|
||||
"AM" => {40, 45},
|
||||
"AW" => {12.5, -69.96666666},
|
||||
"AU" => {-27, 133},
|
||||
"AT" => {47.33333333, 13.33333333},
|
||||
"AZ" => {40.5, 47.5},
|
||||
"BS" => {24.25, -76},
|
||||
"BH" => {26, 50.55},
|
||||
"BD" => {24, 90},
|
||||
"BB" => {13.16666666, -59.53333333},
|
||||
"BY" => {53, 28},
|
||||
"BE" => {50.83333333, 4},
|
||||
"BZ" => {17.25, -88.75},
|
||||
"BJ" => {9.5, 2.25},
|
||||
"BM" => {32.33333333, -64.75},
|
||||
"BT" => {27.5, 90.5},
|
||||
"BO" => {-17, -65},
|
||||
"BQ" => {12.15, -68.266667},
|
||||
"BA" => {44, 18},
|
||||
"BW" => {-22, 24},
|
||||
"BV" => {-54.43333333, 3.4},
|
||||
"BR" => {-10, -55},
|
||||
"IO" => {-6, 71.5},
|
||||
"VG" => {18.431383, -64.62305},
|
||||
"VI" => {18.34, -64.93},
|
||||
"BN" => {4.5, 114.66666666},
|
||||
"BG" => {43, 25},
|
||||
"BF" => {13, -2},
|
||||
"BI" => {-3.5, 30},
|
||||
"KH" => {13, 105},
|
||||
"CM" => {6, 12},
|
||||
"CA" => {60, -95},
|
||||
"CV" => {16, -24},
|
||||
"KY" => {19.5, -80.5},
|
||||
"CF" => {7, 21},
|
||||
"TD" => {15, 19},
|
||||
"CL" => {-30, -71},
|
||||
"CN" => {35, 105},
|
||||
"CX" => {-10.5, 105.66666666},
|
||||
"CC" => {-12.5, 96.83333333},
|
||||
"CO" => {4, -72},
|
||||
"KM" => {-12.16666666, 44.25},
|
||||
"CG" => {-1, 15},
|
||||
"CD" => {0, 25},
|
||||
"CK" => {-21.23333333, -159.76666666},
|
||||
"CR" => {10, -84},
|
||||
"HR" => {45.16666666, 15.5},
|
||||
"CU" => {21.5, -80},
|
||||
"CW" => {12.116667, -68.933333},
|
||||
"CY" => {35, 33},
|
||||
"CZ" => {49.75, 15.5},
|
||||
"DK" => {56, 10},
|
||||
"DJ" => {11.5, 43},
|
||||
"DM" => {15.41666666, -61.33333333},
|
||||
"DO" => {19, -70.66666666},
|
||||
"EC" => {-2, -77.5},
|
||||
"EG" => {27, 30},
|
||||
"SV" => {13.83333333, -88.91666666},
|
||||
"GQ" => {2, 10},
|
||||
"ER" => {15, 39},
|
||||
"EE" => {59, 26},
|
||||
"ET" => {8, 38},
|
||||
"FK" => {-51.75, -59},
|
||||
"FO" => {62, -7},
|
||||
"FJ" => {-18, 175},
|
||||
"FI" => {64, 26},
|
||||
"FR" => {46, 2},
|
||||
"GF" => {4, -53},
|
||||
"PF" => {-15, -140},
|
||||
"TF" => {-49.25, 69.167},
|
||||
"GA" => {-1, 11.75},
|
||||
"GM" => {13.46666666, -16.56666666},
|
||||
"GE" => {42, 43.5},
|
||||
"DE" => {51, 9},
|
||||
"GH" => {8, -2},
|
||||
"GI" => {36.13333333, -5.35},
|
||||
"GR" => {39, 22},
|
||||
"GL" => {72, -40},
|
||||
"GD" => {12.11666666, -61.66666666},
|
||||
"GP" => {16.25, -61.583333},
|
||||
"GU" => {13.46666666, 144.78333333},
|
||||
"GT" => {15.5, -90.25},
|
||||
"GG" => {49.46666666, -2.58333333},
|
||||
"GN" => {11, -10},
|
||||
"GW" => {12, -15},
|
||||
"GY" => {5, -59},
|
||||
"HT" => {19, -72.41666666},
|
||||
"HM" => {-53.1, 72.51666666},
|
||||
"VA" => {41.9, 12.45},
|
||||
"HN" => {15, -86.5},
|
||||
"HU" => {47, 20},
|
||||
"HK" => {22.25, 114.16666666},
|
||||
"IS" => {65, -18},
|
||||
"IN" => {20, 77},
|
||||
"ID" => {-5, 120},
|
||||
"CI" => {8, -5},
|
||||
"IR" => {32, 53},
|
||||
"IQ" => {33, 44},
|
||||
"IE" => {53, -8},
|
||||
"IM" => {54.25, -4.5},
|
||||
"IL" => {31.5, 34.75},
|
||||
"IT" => {42.83333333, 12.83333333},
|
||||
"JM" => {18.25, -77.5},
|
||||
"JP" => {36, 138},
|
||||
"JE" => {49.25, -2.16666666},
|
||||
"JO" => {31, 36},
|
||||
"KZ" => {48, 68},
|
||||
"KE" => {1, 38},
|
||||
"KI" => {1.41666666, 173},
|
||||
"KW" => {29.5, 45.75},
|
||||
"KG" => {41, 75},
|
||||
"LA" => {18, 105},
|
||||
"LV" => {57, 25},
|
||||
"LB" => {33.83333333, 35.83333333},
|
||||
"LS" => {-29.5, 28.5},
|
||||
"LR" => {6.5, -9.5},
|
||||
"LY" => {25, 17},
|
||||
"LI" => {47.26666666, 9.53333333},
|
||||
"LT" => {56, 24},
|
||||
"LU" => {49.75, 6.16666666},
|
||||
"MO" => {22.16666666, 113.55},
|
||||
"MK" => {41.83333333, 22},
|
||||
"MG" => {-20, 47},
|
||||
"MW" => {-13.5, 34},
|
||||
"MY" => {2.5, 112.5},
|
||||
"MV" => {3.25, 73},
|
||||
"ML" => {17, -4},
|
||||
"MT" => {35.83333333, 14.58333333},
|
||||
"MH" => {9, 168},
|
||||
"MQ" => {14.666667, -61},
|
||||
"MR" => {20, -12},
|
||||
"MU" => {-20.28333333, 57.55},
|
||||
"YT" => {-12.83333333, 45.16666666},
|
||||
"MX" => {23, -102},
|
||||
"FM" => {6.91666666, 158.25},
|
||||
"MD" => {47, 29},
|
||||
"MC" => {43.73333333, 7.4},
|
||||
"MN" => {46, 105},
|
||||
"ME" => {42.5, 19.3},
|
||||
"MS" => {16.75, -62.2},
|
||||
"MA" => {32, -5},
|
||||
"MZ" => {-18.25, 35},
|
||||
"MM" => {22, 98},
|
||||
"NA" => {-22, 17},
|
||||
"NR" => {-0.53333333, 166.91666666},
|
||||
"NP" => {28, 84},
|
||||
"NL" => {52.5, 5.75},
|
||||
"NC" => {-21.5, 165.5},
|
||||
"NZ" => {-41, 174},
|
||||
"NI" => {13, -85},
|
||||
"NE" => {16, 8},
|
||||
"NG" => {10, 8},
|
||||
"NU" => {-19.03333333, -169.86666666},
|
||||
"NF" => {-29.03333333, 167.95},
|
||||
"KP" => {40, 127},
|
||||
"MP" => {15.2, 145.75},
|
||||
"NO" => {62, 10},
|
||||
"OM" => {21, 57},
|
||||
"PK" => {30, 70},
|
||||
"PW" => {7.5, 134.5},
|
||||
"PS" => {31.9, 35.2},
|
||||
"PA" => {9, -80},
|
||||
"PG" => {-6, 147},
|
||||
"PY" => {-23, -58},
|
||||
"PE" => {-10, -76},
|
||||
"PH" => {13, 122},
|
||||
"PN" => {-25.06666666, -130.1},
|
||||
"PL" => {52, 20},
|
||||
"PT" => {39.5, -8},
|
||||
"PR" => {18.25, -66.5},
|
||||
"QA" => {25.5, 51.25},
|
||||
"XK" => {42.666667, 21.166667},
|
||||
"RE" => {-21.15, 55.5},
|
||||
"RO" => {46, 25},
|
||||
"RU" => {60, 100},
|
||||
"RW" => {-2, 30},
|
||||
"BL" => {18.5, -63.41666666},
|
||||
"SH" => {-15.95, -5.7},
|
||||
"KN" => {17.33333333, -62.75},
|
||||
"LC" => {13.88333333, -60.96666666},
|
||||
"MF" => {18.08333333, -63.95},
|
||||
"PM" => {46.83333333, -56.33333333},
|
||||
"VC" => {13.25, -61.2},
|
||||
"WS" => {-13.58333333, -172.33333333},
|
||||
"SM" => {43.76666666, 12.41666666},
|
||||
"ST" => {1, 7},
|
||||
"SA" => {25, 45},
|
||||
"SN" => {14, -14},
|
||||
"RS" => {44, 21},
|
||||
"SC" => {-4.58333333, 55.66666666},
|
||||
"SL" => {8.5, -11.5},
|
||||
"SG" => {1.36666666, 103.8},
|
||||
"SX" => {18.033333, -63.05},
|
||||
"SK" => {48.66666666, 19.5},
|
||||
"SI" => {46.11666666, 14.81666666},
|
||||
"SB" => {-8, 159},
|
||||
"SO" => {10, 49},
|
||||
"ZA" => {-29, 24},
|
||||
"GS" => {-54.5, -37},
|
||||
"KR" => {37, 127.5},
|
||||
"ES" => {40, -4},
|
||||
"LK" => {7, 81},
|
||||
"SD" => {15, 30},
|
||||
"SS" => {7, 30},
|
||||
"SR" => {4, -56},
|
||||
"SJ" => {78, 20},
|
||||
"SZ" => {-26.5, 31.5},
|
||||
"SE" => {62, 15},
|
||||
"CH" => {47, 8},
|
||||
"SY" => {35, 38},
|
||||
"TW" => {23.5, 121},
|
||||
"TJ" => {39, 71},
|
||||
"TZ" => {-6, 35},
|
||||
"TH" => {15, 100},
|
||||
"TL" => {-8.83333333, 125.91666666},
|
||||
"TG" => {8, 1.16666666},
|
||||
"TK" => {-9, -172},
|
||||
"TO" => {-20, -175},
|
||||
"TT" => {11, -61},
|
||||
"TN" => {34, 9},
|
||||
"TR" => {39, 35},
|
||||
"TM" => {40, 60},
|
||||
"TC" => {21.75, -71.58333333},
|
||||
"TV" => {-8, 178},
|
||||
"UG" => {1, 32},
|
||||
"UA" => {49, 32},
|
||||
"AE" => {24, 54},
|
||||
"GB" => {54, -2},
|
||||
"US" => {38, -97},
|
||||
"UY" => {-33, -56},
|
||||
"UZ" => {41, 64},
|
||||
"VU" => {-16, 167},
|
||||
"VE" => {8, -66},
|
||||
"VN" => {16.16666666, 107.83333333},
|
||||
"WF" => {-13.3, -176.2},
|
||||
"EH" => {24.5, -13},
|
||||
"YE" => {15, 48},
|
||||
"ZM" => {-15, 30},
|
||||
"ZW" => {-20, 30}
|
||||
}
|
||||
|> Enum.map(fn {code, {lat, lon}} -> {code, {lat * 1.0, lon * 1.0}} end)
|
||||
|> Map.new()
|
||||
# ISO 3166-1 alpha-2
|
||||
# https://en.wikipedia.org/wiki/ISO_3166-1_alpha-2
|
||||
@counties %{
|
||||
"LT" => %{common_name: "Lithuania", coordinates: {56.0, 24.0}},
|
||||
"GU" => %{common_name: "Guam", coordinates: {13.46666666, 144.78333333}},
|
||||
"BA" => %{common_name: "Bosnia and Herzegovina", coordinates: {44.0, 18.0}},
|
||||
"LI" => %{common_name: "Liechtenstein", coordinates: {47.26666666, 9.53333333}},
|
||||
"CZ" => %{common_name: "Czechia", coordinates: {49.75, 15.5}},
|
||||
"AD" => %{common_name: "Andorra", coordinates: {42.5, 1.5}},
|
||||
"MT" => %{common_name: "Malta", coordinates: {35.83333333, 14.58333333}},
|
||||
"MX" => %{common_name: "Mexico", coordinates: {23.0, -102.0}},
|
||||
"TH" => %{common_name: "Thailand", coordinates: {15.0, 100.0}},
|
||||
"EH" => %{common_name: "Western Sahara", coordinates: {24.5, -13.0}},
|
||||
"LR" => %{common_name: "Liberia", coordinates: {6.5, -9.5}},
|
||||
"HK" => %{common_name: "Hong Kong", coordinates: {22.25, 114.16666666}},
|
||||
"SZ" => %{common_name: "Eswatini", coordinates: {-26.5, 31.5}},
|
||||
"TD" => %{common_name: "Chad", coordinates: {15.0, 19.0}},
|
||||
"TR" => %{common_name: "Türkiye", coordinates: {39.0, 35.0}},
|
||||
"KG" => %{common_name: "Kyrgyzstan", coordinates: {41.0, 75.0}},
|
||||
"YE" => %{common_name: "Yemen", coordinates: {15.0, 48.0}},
|
||||
"SJ" => %{common_name: "Svalbard and Jan Mayen", coordinates: {78.0, 20.0}},
|
||||
"IN" => %{common_name: "India", coordinates: {20.0, 77.0}},
|
||||
"FO" => %{common_name: "Faroe Islands", coordinates: {62.0, -7.0}},
|
||||
"US" => %{common_name: "United States of America", coordinates: {38.0, -97.0}},
|
||||
"SD" => %{common_name: "Sudan", coordinates: {15.0, 30.0}},
|
||||
"IR" => %{common_name: "Iran", coordinates: {32.0, 53.0}},
|
||||
"CW" => %{common_name: "Curaçao", coordinates: {12.116667, -68.933333}},
|
||||
"SE" => %{common_name: "Sweden", coordinates: {62.0, 15.0}},
|
||||
"LK" => %{common_name: "Sri Lanka", coordinates: {7.0, 81.0}},
|
||||
"KH" => %{common_name: "Cambodia", coordinates: {13.0, 105.0}},
|
||||
"CN" => %{common_name: "China", coordinates: {35.0, 105.0}},
|
||||
"SA" => %{common_name: "Saudi Arabia", coordinates: {25.0, 45.0}},
|
||||
"IM" => %{common_name: "Isle of Man", coordinates: {54.25, -4.5}},
|
||||
"GY" => %{common_name: "Guyana", coordinates: {5.0, -59.0}},
|
||||
"ST" => %{common_name: "Sao Tome and Principe", coordinates: {1.0, 7.0}},
|
||||
"AL" => %{common_name: "Albania", coordinates: {41.0, 20.0}},
|
||||
"SO" => %{common_name: "Somalia", coordinates: {10.0, 49.0}},
|
||||
"BS" => %{common_name: "Bahamas", coordinates: {24.25, -76.0}},
|
||||
"GM" => %{common_name: "Gambia", coordinates: {13.46666666, -16.56666666}},
|
||||
"ES" => %{common_name: "Spain", coordinates: {40.0, -4.0}},
|
||||
"RW" => %{common_name: "Rwanda", coordinates: {-2.0, 30.0}},
|
||||
"EE" => %{common_name: "Estonia", coordinates: {59.0, 26.0}},
|
||||
"HN" => %{common_name: "Honduras", coordinates: {15.0, -86.5}},
|
||||
"MQ" => %{common_name: "Martinique", coordinates: {14.666667, -61.0}},
|
||||
"EG" => %{common_name: "Egypt", coordinates: {27.0, 30.0}},
|
||||
"GI" => %{common_name: "Gibraltar", coordinates: {36.13333333, -5.35}},
|
||||
"CC" => %{common_name: "Cocos (Keeling) Islands", coordinates: {-12.5, 96.83333333}},
|
||||
"MA" => %{common_name: "Morocco", coordinates: {32.0, -5.0}},
|
||||
"MC" => %{common_name: "Monaco", coordinates: {43.73333333, 7.4}},
|
||||
"DE" => %{common_name: "Germany", coordinates: {51.0, 9.0}},
|
||||
"YT" => %{common_name: "Mayotte", coordinates: {-12.83333333, 45.16666666}},
|
||||
"KI" => %{common_name: "Kiribati", coordinates: {1.41666666, 173.0}},
|
||||
"CU" => %{common_name: "Cuba", coordinates: {21.5, -80.0}},
|
||||
"GL" => %{common_name: "Greenland", coordinates: {72.0, -40.0}},
|
||||
"CH" => %{common_name: "Switzerland", coordinates: {47.0, 8.0}},
|
||||
"BY" => %{common_name: "Belarus", coordinates: {53.0, 28.0}},
|
||||
"NF" => %{common_name: "Norfolk Island", coordinates: {-29.03333333, 167.95}},
|
||||
"SS" => %{common_name: "South Sudan", coordinates: {7.0, 30.0}},
|
||||
"GR" => %{common_name: "Greece", coordinates: {39.0, 22.0}},
|
||||
"DO" => %{common_name: "Dominican Republic", coordinates: {19.0, -70.66666666}},
|
||||
"CI" => %{common_name: "Côte d'Ivoire", coordinates: {8.0, -5.0}},
|
||||
"BQ" => %{common_name: "Bonaire", coordinates: {12.15, -68.266667}},
|
||||
"KN" => %{common_name: "Saint Kitts and Nevis", coordinates: {17.33333333, -62.75}},
|
||||
"KE" => %{common_name: "Kenya", coordinates: {1.0, 38.0}},
|
||||
"PL" => %{common_name: "Poland", coordinates: {52.0, 20.0}},
|
||||
"RO" => %{common_name: "Romania", coordinates: {46.0, 25.0}},
|
||||
"BI" => %{common_name: "Burundi", coordinates: {-3.5, 30.0}},
|
||||
"BO" => %{common_name: "Bolivia", coordinates: {-17.0, -65.0}},
|
||||
"SX" => %{common_name: "Sint Maarten (Dutch part)", coordinates: {18.033333, -63.05}},
|
||||
"CY" => %{common_name: "Cyprus", coordinates: {35.0, 33.0}},
|
||||
"CL" => %{common_name: "Chile", coordinates: {-30.0, -71.0}},
|
||||
"TL" => %{common_name: "Timor-Leste", coordinates: {-8.83333333, 125.91666666}},
|
||||
"AU" => %{common_name: "Australia", coordinates: {-27.0, 133.0}},
|
||||
"KP" => %{common_name: "North Korea", coordinates: {40.0, 127.0}},
|
||||
"WF" => %{common_name: "Wallis and Futuna", coordinates: {-13.3, -176.2}},
|
||||
"MY" => %{common_name: "Malaysia", coordinates: {2.5, 112.5}},
|
||||
"MV" => %{common_name: "Maldives", coordinates: {3.25, 73.0}},
|
||||
"TG" => %{common_name: "Togo", coordinates: {8.0, 1.16666666}},
|
||||
"FI" => %{common_name: "Finland", coordinates: {64.0, 26.0}},
|
||||
"MP" => %{common_name: "Northern Mariana Islands", coordinates: {15.2, 145.75}},
|
||||
"RS" => %{common_name: "Serbia", coordinates: {44.0, 21.0}},
|
||||
"NA" => %{common_name: "Namibia", coordinates: {-22.0, 17.0}},
|
||||
"SI" => %{common_name: "Slovenia", coordinates: {46.11666666, 14.81666666}},
|
||||
"GD" => %{common_name: "Grenada", coordinates: {12.11666666, -61.66666666}},
|
||||
"VU" => %{common_name: "Vanuatu", coordinates: {-16.0, 167.0}},
|
||||
"GW" => %{common_name: "Guinea-Bissau", coordinates: {12.0, -15.0}},
|
||||
"GT" => %{common_name: "Guatemala", coordinates: {15.5, -90.25}},
|
||||
"IQ" => %{common_name: "Iraq", coordinates: {33.0, 44.0}},
|
||||
"BJ" => %{common_name: "Benin", coordinates: {9.5, 2.25}},
|
||||
"BZ" => %{common_name: "Belize", coordinates: {17.25, -88.75}},
|
||||
"GQ" => %{common_name: "Equatorial Guinea", coordinates: {2.0, 10.0}},
|
||||
"MN" => %{common_name: "Mongolia", coordinates: {46.0, 105.0}},
|
||||
"CX" => %{common_name: "Christmas Island", coordinates: {-10.5, 105.66666666}},
|
||||
"MZ" => %{common_name: "Mozambique", coordinates: {-18.25, 35.0}},
|
||||
"JM" => %{common_name: "Jamaica", coordinates: {18.25, -77.5}},
|
||||
"UM" => %{
|
||||
common_name: "United States Minor Outlying Islands",
|
||||
coordinates: {13.9172255, -134.1859535}
|
||||
},
|
||||
"IE" => %{common_name: "Ireland", coordinates: {53.0, -8.0}},
|
||||
"CR" => %{common_name: "Costa Rica", coordinates: {10.0, -84.0}},
|
||||
"PM" => %{common_name: "Saint Pierre and Miquelon", coordinates: {46.83333333, -56.33333333}},
|
||||
"MD" => %{common_name: "Moldova", coordinates: {47.0, 29.0}},
|
||||
"PR" => %{common_name: "Puerto Rico", coordinates: {18.25, -66.5}},
|
||||
"MO" => %{common_name: "Macao", coordinates: {22.16666666, 113.55}},
|
||||
"TO" => %{common_name: "Tonga", coordinates: {-20.0, -175.0}},
|
||||
"AO" => %{common_name: "Angola", coordinates: {-12.5, 18.5}},
|
||||
"AQ" => %{common_name: "Antarctica", coordinates: {-74.65, 4.48}},
|
||||
"IT" => %{common_name: "Italy", coordinates: {42.83333333, 12.83333333}},
|
||||
"TV" => %{common_name: "Tuvalu", coordinates: {-8.0, 178.0}},
|
||||
"SH" => %{common_name: "Saint Helena", coordinates: {-15.95, -5.7}},
|
||||
"ME" => %{common_name: "Montenegro", coordinates: {42.5, 19.3}},
|
||||
"GB" => %{
|
||||
common_name: "United Kingdom of Great Britain and Northern Ireland",
|
||||
coordinates: {54.0, -2.0}
|
||||
},
|
||||
"FK" => %{common_name: "Falkland Islands (Malvinas)", coordinates: {-51.75, -59.0}},
|
||||
"NO" => %{common_name: "Norway", coordinates: {62.0, 10.0}},
|
||||
"DM" => %{common_name: "Dominica", coordinates: {15.41666666, -61.33333333}},
|
||||
"PE" => %{common_name: "Peru", coordinates: {-10.0, -76.0}},
|
||||
"NR" => %{common_name: "Nauru", coordinates: {-0.53333333, 166.91666666}},
|
||||
"MS" => %{common_name: "Montserrat", coordinates: {16.75, -62.2}},
|
||||
"PW" => %{common_name: "Palau", coordinates: {7.5, 134.5}},
|
||||
"KM" => %{common_name: "Comoros", coordinates: {-12.16666666, 44.25}},
|
||||
"AF" => %{common_name: "Afghanistan", coordinates: {33.0, 65.0}},
|
||||
"MM" => %{common_name: "Myanmar", coordinates: {22.0, 98.0}},
|
||||
"CK" => %{common_name: "Cook Islands", coordinates: {-21.23333333, -159.76666666}},
|
||||
"MU" => %{common_name: "Mauritius", coordinates: {-20.28333333, 57.55}},
|
||||
"BD" => %{common_name: "Bangladesh", coordinates: {24.0, 90.0}},
|
||||
"FJ" => %{common_name: "Fiji", coordinates: {-18.0, 175.0}},
|
||||
"UG" => %{common_name: "Uganda", coordinates: {1.0, 32.0}},
|
||||
"RU" => %{common_name: "Russia", coordinates: {60.0, 100.0}},
|
||||
"GN" => %{common_name: "Guinea", coordinates: {11.0, -10.0}},
|
||||
"BM" => %{common_name: "Bermuda", coordinates: {32.33333333, -64.75}},
|
||||
"JP" => %{common_name: "Japan", coordinates: {36.0, 138.0}},
|
||||
"TT" => %{common_name: "Trinidad and Tobago", coordinates: {11.0, -61.0}},
|
||||
"IS" => %{common_name: "Iceland", coordinates: {65.0, -18.0}},
|
||||
"FM" => %{common_name: "Micronesia", coordinates: {6.91666666, 158.25}},
|
||||
"KY" => %{common_name: "Cayman Islands", coordinates: {19.5, -80.5}},
|
||||
"MH" => %{common_name: "Marshall Islands", coordinates: {9.0, 168.0}},
|
||||
"UA" => %{common_name: "Ukraine", coordinates: {49.0, 32.0}},
|
||||
"NC" => %{common_name: "New Caledonia", coordinates: {-21.5, 165.5}},
|
||||
"MW" => %{common_name: "Malawi", coordinates: {-13.5, 34.0}},
|
||||
"TF" => %{common_name: "French Southern Territories", coordinates: {-49.25, 69.167}},
|
||||
"LS" => %{common_name: "Lesotho", coordinates: {-29.5, 28.5}},
|
||||
"IO" => %{common_name: "British Indian Ocean Territory", coordinates: {-6.0, 71.5}},
|
||||
"SN" => %{common_name: "Senegal", coordinates: {14.0, -14.0}},
|
||||
"DZ" => %{common_name: "Algeria", coordinates: {28.0, 3.0}},
|
||||
"AW" => %{common_name: "Aruba", coordinates: {12.5, -69.96666666}},
|
||||
"GS" => %{
|
||||
common_name: "South Georgia and the South Sandwich Islands",
|
||||
coordinates: {-54.5, -37.0}
|
||||
},
|
||||
"SM" => %{common_name: "San Marino", coordinates: {43.76666666, 12.41666666}},
|
||||
"PA" => %{common_name: "Panama", coordinates: {9.0, -80.0}},
|
||||
"JO" => %{common_name: "Jordan", coordinates: {31.0, 36.0}},
|
||||
"VE" => %{common_name: "Venezuela", coordinates: {8.0, -66.0}},
|
||||
"AE" => %{common_name: "United Arab Emirates", coordinates: {24.0, 54.0}},
|
||||
"TJ" => %{common_name: "Tajikistan", coordinates: {39.0, 71.0}},
|
||||
"BT" => %{common_name: "Bhutan", coordinates: {27.5, 90.5}},
|
||||
"TC" => %{common_name: "Turks and Caicos Islands", coordinates: {21.75, -71.58333333}},
|
||||
"NP" => %{common_name: "Nepal", coordinates: {28.0, 84.0}},
|
||||
"LB" => %{common_name: "Lebanon", coordinates: {33.83333333, 35.83333333}},
|
||||
"HU" => %{common_name: "Hungary", coordinates: {47.0, 20.0}},
|
||||
"LU" => %{common_name: "Luxembourg", coordinates: {49.75, 6.16666666}},
|
||||
"DK" => %{common_name: "Denmark", coordinates: {56.0, 10.0}},
|
||||
"BF" => %{common_name: "Burkina Faso", coordinates: {13.0, -2.0}},
|
||||
"VA" => %{common_name: "Holy See", coordinates: {41.9, 12.45}},
|
||||
"SK" => %{common_name: "Slovakia", coordinates: {48.66666666, 19.5}},
|
||||
"UZ" => %{common_name: "Uzbekistan", coordinates: {41.0, 64.0}},
|
||||
"NG" => %{common_name: "Nigeria", coordinates: {10.0, 8.0}},
|
||||
"AG" => %{common_name: "Antigua and Barbuda", coordinates: {17.05, -61.8}},
|
||||
"EC" => %{common_name: "Ecuador", coordinates: {-2.0, -77.5}},
|
||||
"SC" => %{common_name: "Seychelles", coordinates: {-4.58333333, 55.66666666}},
|
||||
"AR" => %{common_name: "Argentina", coordinates: {-34.0, -64.0}},
|
||||
"BW" => %{common_name: "Botswana", coordinates: {-22.0, 24.0}},
|
||||
"BE" => %{common_name: "Belgium", coordinates: {50.83333333, 4.0}},
|
||||
"TM" => %{common_name: "Turkmenistan", coordinates: {40.0, 60.0}},
|
||||
"VN" => %{common_name: "Vietnam", coordinates: {16.16666666, 107.83333333}},
|
||||
"PH" => %{common_name: "Philippines", coordinates: {13.0, 122.0}},
|
||||
"SG" => %{common_name: "Singapore", coordinates: {1.36666666, 103.8}},
|
||||
"TK" => %{common_name: "Tokelau", coordinates: {-9.0, -172.0}},
|
||||
"BV" => %{common_name: "Bouvet Island", coordinates: {-54.43333333, 3.4}},
|
||||
"MG" => %{common_name: "Madagascar", coordinates: {-20.0, 47.0}},
|
||||
"GP" => %{common_name: "Guadeloupe", coordinates: {16.25, -61.583333}},
|
||||
"ET" => %{common_name: "Ethiopia", coordinates: {8.0, 38.0}},
|
||||
"CM" => %{common_name: "Cameroon", coordinates: {6.0, 12.0}},
|
||||
"AX" => %{common_name: "Åland Islands", coordinates: {60.116667, 19.9}},
|
||||
"BL" => %{common_name: "Saint Barthélemy", coordinates: {18.5, -63.41666666}},
|
||||
"SY" => %{common_name: "Syria", coordinates: {35.0, 38.0}},
|
||||
"MR" => %{common_name: "Mauritania", coordinates: {20.0, -12.0}},
|
||||
"LY" => %{common_name: "Libya", coordinates: {25.0, 17.0}},
|
||||
"RE" => %{common_name: "Réunion", coordinates: {-21.15, 55.5}},
|
||||
"HM" => %{common_name: "Heard Island and McDonald Islands", coordinates: {-53.1, 72.51666666}},
|
||||
"VG" => %{common_name: "Virgin Islands (British)", coordinates: {18.431383, -64.62305}},
|
||||
"AZ" => %{common_name: "Azerbaijan", coordinates: {40.5, 47.5}},
|
||||
"CF" => %{common_name: "Central African Republic", coordinates: {7.0, 21.0}},
|
||||
"AT" => %{common_name: "Austria", coordinates: {47.33333333, 13.33333333}},
|
||||
"BH" => %{common_name: "Bahrain", coordinates: {26.0, 50.55}},
|
||||
"PT" => %{common_name: "Portugal", coordinates: {39.5, -8.0}},
|
||||
"TN" => %{common_name: "Tunisia", coordinates: {34.0, 9.0}},
|
||||
"TZ" => %{common_name: "Tanzania", coordinates: {-6.0, 35.0}},
|
||||
"ZA" => %{common_name: "South Africa", coordinates: {-29.0, 24.0}},
|
||||
"VC" => %{common_name: "Saint Vincent and the Grenadines", coordinates: {13.25, -61.2}},
|
||||
"PK" => %{common_name: "Pakistan", coordinates: {30.0, 70.0}},
|
||||
"PY" => %{common_name: "Paraguay", coordinates: {-23.0, -58.0}},
|
||||
"CD" => %{common_name: "Congo", coordinates: {0.0, 25.0}},
|
||||
"WS" => %{common_name: "Samoa", coordinates: {-13.58333333, -172.33333333}},
|
||||
"UY" => %{common_name: "Uruguay", coordinates: {-33.0, -56.0}},
|
||||
"SB" => %{common_name: "Solomon Islands", coordinates: {-8.0, 159.0}},
|
||||
"GE" => %{common_name: "Georgia", coordinates: {42.0, 43.5}},
|
||||
"GA" => %{common_name: "Gabon", coordinates: {-1.0, 11.75}},
|
||||
"HT" => %{common_name: "Haiti", coordinates: {19.0, -72.41666666}},
|
||||
"BG" => %{common_name: "Bulgaria", coordinates: {43.0, 25.0}},
|
||||
"OM" => %{common_name: "Oman", coordinates: {21.0, 57.0}},
|
||||
"CA" => %{common_name: "Canada", coordinates: {60.0, -95.0}},
|
||||
"KR" => %{common_name: "South Korea", coordinates: {37.0, 127.5}},
|
||||
"ER" => %{common_name: "Eritrea", coordinates: {15.0, 39.0}},
|
||||
"GF" => %{common_name: "French Guiana", coordinates: {4.0, -53.0}},
|
||||
"LV" => %{common_name: "Latvia", coordinates: {57.0, 25.0}},
|
||||
"AI" => %{common_name: "Anguilla", coordinates: {18.25, -63.16666666}},
|
||||
"ZW" => %{common_name: "Zimbabwe", coordinates: {-20.0, 30.0}},
|
||||
"BN" => %{common_name: "Brunei Darussalam", coordinates: {4.5, 114.66666666}},
|
||||
"SV" => %{common_name: "El Salvador", coordinates: {13.83333333, -88.91666666}},
|
||||
"NL" => %{common_name: "Netherlands", coordinates: {52.5, 5.75}},
|
||||
"PG" => %{common_name: "Papua New Guinea", coordinates: {-6.0, 147.0}},
|
||||
"GG" => %{common_name: "Guernsey", coordinates: {49.46666666, -2.58333333}},
|
||||
"TW" => %{common_name: "Taiwan", coordinates: {23.5, 121.0}},
|
||||
"CO" => %{common_name: "Colombia", coordinates: {4.0, -72.0}},
|
||||
"SR" => %{common_name: "Suriname", coordinates: {4.0, -56.0}},
|
||||
"HR" => %{common_name: "Croatia", coordinates: {45.16666666, 15.5}},
|
||||
"JE" => %{common_name: "Jersey", coordinates: {49.25, -2.16666666}},
|
||||
"LA" => %{common_name: "Laos", coordinates: {18.0, 105.0}},
|
||||
"KW" => %{common_name: "Kuwait", coordinates: {29.5, 45.75}},
|
||||
"PS" => %{common_name: "Palestine", coordinates: {31.9, 35.2}},
|
||||
"ZM" => %{common_name: "Zambia", coordinates: {-15.0, 30.0}},
|
||||
"NI" => %{common_name: "Nicaragua", coordinates: {13.0, -85.0}},
|
||||
"SL" => %{common_name: "Sierra Leone", coordinates: {8.5, -11.5}},
|
||||
"GH" => %{common_name: "Ghana", coordinates: {8.0, -2.0}},
|
||||
"IL" => %{common_name: "Israel", coordinates: {31.5, 34.75}},
|
||||
"AS" => %{common_name: "American Samoa", coordinates: {-14.33333333, -170.0}},
|
||||
"MF" => %{common_name: "Saint Martin (French part)", coordinates: {18.08333333, -63.95}},
|
||||
"NZ" => %{common_name: "New Zealand", coordinates: {-41.0, 174.0}},
|
||||
"BB" => %{common_name: "Barbados", coordinates: {13.16666666, -59.53333333}},
|
||||
"NU" => %{common_name: "Niue", coordinates: {-19.03333333, -169.86666666}},
|
||||
"DJ" => %{common_name: "Djibouti", coordinates: {11.5, 43.0}},
|
||||
"BR" => %{common_name: "Brazil", coordinates: {-10.0, -55.0}},
|
||||
"LC" => %{common_name: "Saint Lucia", coordinates: {13.88333333, -60.96666666}},
|
||||
"QA" => %{common_name: "Qatar", coordinates: {25.5, 51.25}},
|
||||
"ID" => %{common_name: "Indonesia", coordinates: {-5.0, 120.0}},
|
||||
"NE" => %{common_name: "Niger", coordinates: {16.0, 8.0}},
|
||||
"MK" => %{common_name: "North Macedonia", coordinates: {41.83333333, 22.0}},
|
||||
"FR" => %{common_name: "France", coordinates: {46.0, 2.0}},
|
||||
"PN" => %{common_name: "Pitcairn", coordinates: {-25.06666666, -130.1}},
|
||||
"AM" => %{common_name: "Armenia", coordinates: {40.0, 45.0}},
|
||||
"ML" => %{common_name: "Mali", coordinates: {17.0, -4.0}},
|
||||
"VI" => %{common_name: "Virgin Islands (U.S.)", coordinates: {18.34, -64.93}},
|
||||
"KZ" => %{common_name: "Kazakhstan", coordinates: {48.0, 68.0}},
|
||||
"CG" => %{common_name: "Congo", coordinates: {-1.0, 15.0}},
|
||||
"CV" => %{common_name: "Cabo Verde", coordinates: {16.0, -24.0}},
|
||||
"PF" => %{common_name: "French Polynesia", coordinates: {-15.0, -140.0}}
|
||||
}
|
||||
|
||||
def distance({lat1, lon1}, {lat2, lon2}) do
|
||||
d_lat = degrees_to_radians(lat2 - lat1)
|
||||
@@ -274,13 +283,32 @@ defmodule Domain.Geo do
|
||||
end
|
||||
|
||||
def maybe_put_default_coordinates(country_code, {nil, nil}) do
|
||||
case Map.get(@coordinates_by_code, country_code) do
|
||||
{lat, lon} -> {lat, lon}
|
||||
nil -> {nil, nil}
|
||||
with {:ok, country} <- Map.fetch(@counties, country_code),
|
||||
{:ok, {lat, lon}} <- Map.fetch(country, :coordinates) do
|
||||
{lat, lon}
|
||||
else
|
||||
_other -> {nil, nil}
|
||||
end
|
||||
end
|
||||
|
||||
def maybe_put_default_coordinates(_country_code, {lat, lon}) do
|
||||
{lat, lon}
|
||||
end
|
||||
|
||||
def all_country_codes! do
|
||||
Enum.map(@counties, fn {code, _} -> code end)
|
||||
end
|
||||
|
||||
def all_country_options! do
|
||||
@counties
|
||||
|> Enum.map(fn {code, %{common_name: common_name}} -> {common_name, code} end)
|
||||
|> Enum.sort_by(fn {common_name, _} -> common_name end)
|
||||
end
|
||||
|
||||
def country_common_name!(country_code) do
|
||||
case Map.fetch(@counties, country_code) do
|
||||
{:ok, %{common_name: common_name}} -> common_name
|
||||
:error -> country_code
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
defmodule Domain.Policies do
|
||||
alias Domain.{Repo, PubSub}
|
||||
alias Domain.{Auth, Accounts, Actors, Resources, Flows}
|
||||
alias Domain.Policies.{Authorizer, Policy}
|
||||
alias Domain.{Auth, Accounts, Actors, Clients, Resources, Flows}
|
||||
alias Domain.Policies.{Authorizer, Policy, Condition}
|
||||
|
||||
def fetch_policy_by_id(id, %Auth.Subject{} = subject, opts \\ []) do
|
||||
required_permissions =
|
||||
@@ -165,6 +165,16 @@ defmodule Domain.Policies do
|
||||
{:ok, policies}
|
||||
end
|
||||
|
||||
def ensure_client_conforms_policy_conditions(%Clients.Client{} = client, %Policy{} = policy) do
|
||||
case Condition.Evaluator.ensure_conforms(policy.conditions, client) do
|
||||
:ok ->
|
||||
:ok
|
||||
|
||||
{:error, violated_properties} ->
|
||||
{:error, {:forbidden, violated_properties: violated_properties}}
|
||||
end
|
||||
end
|
||||
|
||||
def ensure_has_access_to(%Auth.Subject{} = subject, %Policy{} = policy) do
|
||||
if subject.account.id == policy.account_id do
|
||||
:ok
|
||||
|
||||
18
elixir/apps/domain/lib/domain/policies/condition.ex
Normal file
18
elixir/apps/domain/lib/domain/policies/condition.ex
Normal file
@@ -0,0 +1,18 @@
|
||||
defmodule Domain.Policies.Condition do
|
||||
use Domain, :schema
|
||||
|
||||
@primary_key false
|
||||
embedded_schema do
|
||||
field :property, Ecto.Enum,
|
||||
values: ~w[remote_ip_location_region remote_ip provider_id current_utc_datetime]a
|
||||
|
||||
field :operator, Ecto.Enum, values: ~w[
|
||||
contains does_not_contain
|
||||
is_in is_not_in
|
||||
is_in_day_of_week_time_ranges
|
||||
is_in_cidr is_not_in_cidr
|
||||
]a
|
||||
|
||||
field :values, {:array, :string}
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,66 @@
|
||||
defmodule Domain.Policies.Condition.Changeset do
|
||||
use Domain, :changeset
|
||||
alias Domain.Policies.Condition
|
||||
|
||||
def changeset(%Condition{} = condition, attrs, _position) do
|
||||
condition
|
||||
|> cast(attrs, [:property, :operator, :values])
|
||||
|> put_default_value(:property, :remote_ip_location_region)
|
||||
|> put_default_value(:operator, :is_in)
|
||||
|> validate_required([:property, :operator])
|
||||
|> validate_operator()
|
||||
end
|
||||
|
||||
def valid_operators_for_property(:remote_ip_location_region), do: [:is_in, :is_not_in]
|
||||
def valid_operators_for_property(:remote_ip), do: [:is_in_cidr, :is_not_in_cidr]
|
||||
def valid_operators_for_property(:provider_id), do: [:is_in, :is_not_in]
|
||||
def valid_operators_for_property(:current_utc_datetime), do: [:is_in_day_of_week_time_ranges]
|
||||
|
||||
defp validate_operator(changeset) do
|
||||
case fetch_field(changeset, :property) do
|
||||
{_data_or_changes, :remote_ip_location_region} ->
|
||||
changeset
|
||||
|> validate_required(:operator)
|
||||
|> validate_inclusion(:operator, valid_operators_for_property(:remote_ip_location_region))
|
||||
|> validate_subset(:values, Domain.Geo.all_country_codes!())
|
||||
|
||||
{_data_or_changes, :remote_ip} ->
|
||||
changeset
|
||||
|> validate_required(:operator)
|
||||
|> validate_inclusion(:operator, valid_operators_for_property(:remote_ip))
|
||||
|> validate_list(:values, Domain.Types.INET)
|
||||
|
||||
{_data_or_changes, :provider_id} ->
|
||||
changeset
|
||||
|> validate_required(:operator)
|
||||
|> validate_inclusion(:operator, valid_operators_for_property(:provider_id))
|
||||
|> validate_list(:values, Ecto.UUID)
|
||||
|
||||
{_data_or_changes, :current_utc_datetime} ->
|
||||
changeset
|
||||
|> validate_required(:operator)
|
||||
|> validate_inclusion(:operator, valid_operators_for_property(:current_utc_datetime))
|
||||
|> validate_list(:values, :string, fn changeset, field ->
|
||||
validate_day_of_week_time_ranges(changeset, field)
|
||||
end)
|
||||
|
||||
{_data_or_changes, nil} ->
|
||||
changeset
|
||||
|
||||
:error ->
|
||||
add_error(changeset, :property, "is not supported")
|
||||
end
|
||||
end
|
||||
|
||||
def validate_day_of_week_time_ranges(changeset, field) do
|
||||
validate_change(changeset, field, fn field, value ->
|
||||
case Condition.Evaluator.parse_day_of_week_time_ranges(value) do
|
||||
{:ok, _dow_time_ranges} ->
|
||||
[]
|
||||
|
||||
{:error, reason} ->
|
||||
[{field, reason}]
|
||||
end
|
||||
end)
|
||||
end
|
||||
end
|
||||
237
elixir/apps/domain/lib/domain/policies/condition/evaluator.ex
Normal file
237
elixir/apps/domain/lib/domain/policies/condition/evaluator.ex
Normal file
@@ -0,0 +1,237 @@
|
||||
defmodule Domain.Policies.Condition.Evaluator do
|
||||
alias Domain.Repo
|
||||
alias Domain.Clients
|
||||
alias Domain.Policies.Condition
|
||||
|
||||
@days_of_week ~w[M T W R F S U]
|
||||
|
||||
def ensure_conforms([], %Clients.Client{}) do
|
||||
:ok
|
||||
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]
|
||||
end
|
||||
end)
|
||||
|> case do
|
||||
[] -> :ok
|
||||
violated_properties -> {:error, Enum.reverse(violated_properties)}
|
||||
end
|
||||
end
|
||||
|
||||
def conforms?(
|
||||
%Condition{property: :remote_ip_location_region, operator: :is_in, values: values},
|
||||
%Clients.Client{} = client
|
||||
) do
|
||||
client.last_seen_remote_ip_location_region in values
|
||||
end
|
||||
|
||||
def conforms?(
|
||||
%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
|
||||
end
|
||||
|
||||
def conforms?(
|
||||
%Condition{property: :remote_ip, operator: :is_in_cidr, values: values},
|
||||
%Clients.Client{} = client
|
||||
) do
|
||||
Enum.any?(values, fn cidr ->
|
||||
{: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)
|
||||
end)
|
||||
end
|
||||
|
||||
def conforms?(
|
||||
%Condition{property: :remote_ip, operator: :is_not_in_cidr, values: values},
|
||||
%Clients.Client{} = client
|
||||
) do
|
||||
Enum.all?(values, fn cidr ->
|
||||
{: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)
|
||||
end)
|
||||
end
|
||||
|
||||
def conforms?(
|
||||
%Condition{property: :provider_id, operator: :is_in, values: values},
|
||||
%Clients.Client{} = client
|
||||
) do
|
||||
client = Repo.preload(client, :identity)
|
||||
client.identity.provider_id in values
|
||||
end
|
||||
|
||||
def conforms?(
|
||||
%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
|
||||
end
|
||||
|
||||
def conforms?(
|
||||
%Condition{
|
||||
property: :current_utc_datetime,
|
||||
operator: :is_in_day_of_week_time_ranges,
|
||||
values: values
|
||||
},
|
||||
%Clients.Client{}
|
||||
) do
|
||||
datetime_in_day_of_the_week_time_ranges?(DateTime.utc_now(), values)
|
||||
end
|
||||
|
||||
def datetime_in_day_of_the_week_time_ranges?(datetime, dow_time_ranges) 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} ->
|
||||
datetime_in_time_ranges?(datetime, day, time_ranges)
|
||||
end)
|
||||
|
||||
{:error, _reason} ->
|
||||
false
|
||||
end
|
||||
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)
|
||||
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
|
||||
end
|
||||
end)
|
||||
end
|
||||
|
||||
def parse_days_of_week_time_ranges(dows_time_ranges) do
|
||||
Enum.reduce_while(dows_time_ranges, {:ok, %{}}, fn dow_time_ranges, {:ok, acc} ->
|
||||
case parse_day_of_week_time_ranges(dow_time_ranges) do
|
||||
{:ok, {day, dow_time_ranges}} ->
|
||||
{_current_value, acc} =
|
||||
Map.get_and_update(acc, day, fn
|
||||
nil -> {nil, dow_time_ranges}
|
||||
current_value -> {current_value, current_value ++ dow_time_ranges}
|
||||
end)
|
||||
|
||||
{:cont, {:ok, acc}}
|
||||
|
||||
{:error, reason} ->
|
||||
{:halt, {:error, reason}}
|
||||
end
|
||||
end)
|
||||
end
|
||||
|
||||
def parse_day_of_week_time_ranges(dow_time_ranges) do
|
||||
case String.split(dow_time_ranges, "/", parts: 3) do
|
||||
[day, time_ranges, timezone] when day in @days_of_week ->
|
||||
with {:ok, time_ranges} <- parse_time_ranges(time_ranges, timezone) do
|
||||
{:ok, {day, time_ranges}}
|
||||
end
|
||||
|
||||
[_day, _time_ranges] ->
|
||||
{:error, "timezone is required"}
|
||||
|
||||
_ ->
|
||||
{:error, "invalid day of the week, must be one of #{Enum.join(@days_of_week, ", ")}"}
|
||||
end
|
||||
end
|
||||
|
||||
def parse_time_ranges(time_ranges, timezone) do
|
||||
with true <- Tzdata.zone_exists?(timezone),
|
||||
{:ok, time_ranges} <- parse_time_ranges(time_ranges) do
|
||||
time_ranges =
|
||||
Enum.map(time_ranges, fn {start_time, end_time} ->
|
||||
{start_time, end_time, timezone}
|
||||
end)
|
||||
|
||||
{:ok, time_ranges}
|
||||
else
|
||||
false ->
|
||||
{:error, "invalid timezone"}
|
||||
|
||||
{:error, reason} ->
|
||||
{:error, reason}
|
||||
end
|
||||
end
|
||||
|
||||
def parse_time_ranges(nil) do
|
||||
{:ok, []}
|
||||
end
|
||||
|
||||
def parse_time_ranges(time_ranges) do
|
||||
String.split(time_ranges, ",", trim: true)
|
||||
|> Enum.reduce_while({:ok, []}, fn time_range, {:ok, acc} ->
|
||||
time_range
|
||||
|> String.trim()
|
||||
|> parse_time_range()
|
||||
|> case do
|
||||
{:ok, time} ->
|
||||
{:cont, {:ok, [time | acc]}}
|
||||
|
||||
{:error, reason} ->
|
||||
{:halt, {:error, reason}}
|
||||
end
|
||||
end)
|
||||
|> case do
|
||||
{:ok, time_ranges} ->
|
||||
{:ok, Enum.reverse(time_ranges)}
|
||||
|
||||
{:error, reason} ->
|
||||
{:error, reason}
|
||||
end
|
||||
end
|
||||
|
||||
def parse_time_range("true") do
|
||||
{:ok, {~T[00:00:00], ~T[23:59:59]}}
|
||||
end
|
||||
|
||||
def parse_time_range(time_range) do
|
||||
with [start_time, end_time] <- String.split(time_range, "-", parts: 2, trim: true),
|
||||
{:ok, start_time} <- parse_time(start_time),
|
||||
{:ok, end_time} <- parse_time(end_time),
|
||||
true <- Time.compare(start_time, end_time) != :gt do
|
||||
{:ok, {start_time, end_time}}
|
||||
else
|
||||
false ->
|
||||
{:error, "start of the time range must be less than or equal to the end of it"}
|
||||
|
||||
_ ->
|
||||
{:error, "invalid time range: #{time_range}"}
|
||||
end
|
||||
end
|
||||
|
||||
defp parse_time(time) do
|
||||
[time | _tail] = String.split(time, ".", parts: 2)
|
||||
|
||||
case String.split(time, ":", parts: 3) do
|
||||
[hours] ->
|
||||
Time.from_iso8601(pad2(hours) <> ":00:00")
|
||||
|
||||
[hours, minutes] ->
|
||||
Time.from_iso8601(pad2(hours) <> ":" <> pad2(minutes) <> ":00")
|
||||
|
||||
[hours, minutes, seconds] ->
|
||||
Time.from_iso8601(pad2(hours) <> ":" <> pad2(minutes) <> ":" <> pad2(seconds))
|
||||
|
||||
_ ->
|
||||
{:error, "invalid time: #{time}"}
|
||||
end
|
||||
end
|
||||
|
||||
defp pad2(str_int) when byte_size(str_int) == 1, do: "0#{str_int}"
|
||||
defp pad2(str_int), do: str_int
|
||||
end
|
||||
@@ -4,6 +4,8 @@ defmodule Domain.Policies.Policy do
|
||||
schema "policies" do
|
||||
field :description, :string
|
||||
|
||||
embeds_many :conditions, Domain.Policies.Condition, on_replace: :delete
|
||||
|
||||
belongs_to :actor_group, Domain.Actors.Group
|
||||
belongs_to :resource, Domain.Resources.Resource
|
||||
belongs_to :account, Domain.Accounts.Account
|
||||
|
||||
@@ -11,6 +11,7 @@ defmodule Domain.Policies.Policy.Changeset do
|
||||
%Policy{}
|
||||
|> cast(attrs, @fields)
|
||||
|> validate_required(@required_fields)
|
||||
|> cast_embed(:conditions, with: &Domain.Policies.Condition.Changeset.changeset/3)
|
||||
|> changeset()
|
||||
|> put_change(:account_id, subject.account.id)
|
||||
|> put_change(:created_by, :identity)
|
||||
@@ -21,6 +22,7 @@ defmodule Domain.Policies.Policy.Changeset do
|
||||
policy
|
||||
|> cast(attrs, @update_fields)
|
||||
|> validate_required(@required_fields)
|
||||
|> cast_embed(:conditions, with: &Domain.Policies.Condition.Changeset.changeset/3)
|
||||
|> changeset()
|
||||
end
|
||||
|
||||
|
||||
@@ -7,6 +7,34 @@ defmodule Domain.Repo.Changeset do
|
||||
|
||||
# Helpers
|
||||
|
||||
def set_action(changesets, action) when is_list(changesets) do
|
||||
Enum.map(changesets, &set_action(&1, action))
|
||||
end
|
||||
|
||||
def set_action(%Ecto.Changeset{} = changeset, action) do
|
||||
assocs =
|
||||
Enum.flat_map(changeset.types, fn
|
||||
{field, {:assoc, _params}} -> [field]
|
||||
{field, {:embed, _params}} -> [field]
|
||||
_ -> []
|
||||
end)
|
||||
|
||||
changeset =
|
||||
Enum.reduce(changeset.changes, changeset, fn {key, value}, changeset ->
|
||||
if key in assocs do
|
||||
%{changeset | changes: Map.put(changeset.changes, key, set_action(value, action))}
|
||||
else
|
||||
changeset
|
||||
end
|
||||
end)
|
||||
|
||||
%{changeset | action: action}
|
||||
end
|
||||
|
||||
def set_action(other, _action) do
|
||||
other
|
||||
end
|
||||
|
||||
def has_errors?(%Ecto.Changeset{} = changeset, field) do
|
||||
Keyword.has_key?(changeset.errors, field)
|
||||
end
|
||||
@@ -113,6 +141,43 @@ defmodule Domain.Repo.Changeset do
|
||||
|
||||
# Validations
|
||||
|
||||
def validate_list(
|
||||
%Ecto.Changeset{} = changeset,
|
||||
field,
|
||||
element_type,
|
||||
fun \\ fn changeset, _field -> changeset end
|
||||
) do
|
||||
validate_change(changeset, field, fn _current_field, value ->
|
||||
cond do
|
||||
not is_list(value) ->
|
||||
[{field, "must be a list"}]
|
||||
|
||||
Enum.empty?(value) ->
|
||||
[]
|
||||
|
||||
true ->
|
||||
value
|
||||
|> Enum.with_index()
|
||||
|> Enum.flat_map(&validate_list_element(field, fun, element_type, &1))
|
||||
end
|
||||
end)
|
||||
end
|
||||
|
||||
defp validate_list_element(field, fun, element_type, {value, index}) do
|
||||
{%{}, %{value: element_type}}
|
||||
|> Ecto.Changeset.cast(%{value: value}, [:value])
|
||||
|> fun.(:value)
|
||||
|> Ecto.Changeset.apply_action(:insert)
|
||||
|> case do
|
||||
{:ok, _} ->
|
||||
[]
|
||||
|
||||
{:error, %{errors: errors}} ->
|
||||
{error, meta} = errors[:value]
|
||||
[{field, {error, meta ++ [validated_as: :list, at: index]}}]
|
||||
end
|
||||
end
|
||||
|
||||
def validate_email(%Ecto.Changeset{} = changeset, field) do
|
||||
changeset
|
||||
|> validate_format(field, ~r/^[^\s]+@[^\s]+$/, message: "is an invalid email address")
|
||||
|
||||
@@ -21,7 +21,10 @@ defmodule Domain.Resources.Resource do
|
||||
|
||||
has_many :policies, Domain.Policies.Policy, where: [deleted_at: nil]
|
||||
has_many :actor_groups, through: [:policies, :actor_group]
|
||||
field :authorized_by_policy, :map, virtual: true
|
||||
|
||||
# Warning: do not do Repo.preload/2 for this field, it will not work intentionally,
|
||||
# because the actual preload query should also use joins and process policy conditions
|
||||
has_many :authorized_by_policies, Domain.Policies.Policy, where: [id: {:fragment, "FALSE"}]
|
||||
|
||||
field :created_by, Ecto.Enum, values: ~w[identity]a
|
||||
belongs_to :created_by_identity, Domain.Auth.Identity
|
||||
|
||||
@@ -23,24 +23,18 @@ defmodule Domain.Resources.Resource.Query do
|
||||
end
|
||||
|
||||
def by_authorized_actor_id(queryable, actor_id) do
|
||||
subquery =
|
||||
Domain.Policies.Policy.Query.not_disabled()
|
||||
|> Domain.Policies.Policy.Query.by_actor_id(actor_id)
|
||||
|> where([policies: policies], policies.resource_id == parent_as(:resources).id)
|
||||
|> limit(1)
|
||||
|
||||
queryable
|
||||
|> join(
|
||||
:inner_lateral,
|
||||
:inner,
|
||||
[resources: resources],
|
||||
policies in subquery(subquery),
|
||||
on: true,
|
||||
as: :authorized_by_policies
|
||||
policies in ^Domain.Policies.Policy.Query.not_disabled(),
|
||||
on: policies.resource_id == resources.id,
|
||||
as: :policies
|
||||
)
|
||||
|> Domain.Policies.Policy.Query.by_actor_id(actor_id)
|
||||
|> preload([resources: resources, policies: policies],
|
||||
authorized_by_policies: policies
|
||||
)
|
||||
# Note: this will only write one of policies to a map, which means that
|
||||
# when a client has access to a resource using multiple policies (eg. being a member in multiple groups),
|
||||
# the policy used will be not deterministic
|
||||
|> select_merge([authorized_by_policies: policies], %{authorized_by_policy: policies})
|
||||
end
|
||||
|
||||
def with_at_least_one_gateway_group(queryable) do
|
||||
|
||||
@@ -120,6 +120,7 @@ defmodule Domain.Types.CIDR do
|
||||
:ok <- validate_netmask(address, netmask) do
|
||||
{:ok, %Postgrex.INET{address: address, netmask: netmask}}
|
||||
else
|
||||
:invalid_netmask -> {:error, message: "CIDR netmask is invalid or missing"}
|
||||
_error -> {:error, message: "is invalid"}
|
||||
end
|
||||
end
|
||||
@@ -132,6 +133,7 @@ defmodule Domain.Types.CIDR do
|
||||
with [binary_address, binary_netmask] <- String.split(binary, "/", parts: 2) do
|
||||
{:ok, {binary_address, binary_netmask}}
|
||||
else
|
||||
[_binary_address] -> :invalid_netmask
|
||||
_other -> :error
|
||||
end
|
||||
end
|
||||
@@ -145,7 +147,7 @@ defmodule Domain.Types.CIDR do
|
||||
defp cast_netmask(binary) when is_binary(binary) do
|
||||
case Integer.parse(binary) do
|
||||
{netmask, ""} -> {:ok, netmask}
|
||||
_other -> :error
|
||||
_other -> :invalid_netmask
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
@@ -34,6 +34,9 @@ defmodule Domain.Types.IP do
|
||||
def load(%Postgrex.INET{} = inet), do: {:ok, inet}
|
||||
def load(_), do: :error
|
||||
|
||||
def type(address) when tuple_size(address) == 4, do: :ipv4
|
||||
def type(address) when tuple_size(address) == 8, do: :ipv6
|
||||
|
||||
def to_string(ip) when is_binary(ip), do: ip
|
||||
def to_string(%Postgrex.INET{} = inet), do: Domain.Types.INET.to_string(inet)
|
||||
end
|
||||
|
||||
@@ -17,7 +17,8 @@ defmodule Domain.Types.IPPort do
|
||||
with {:ok, {binary_address, binary_port}} <- parse_binary(binary),
|
||||
{:ok, address} <- cast_address(binary_address),
|
||||
{:ok, port} <- cast_port(binary_port) do
|
||||
{:ok, %__MODULE__{address_type: type(address), address: address, port: port}}
|
||||
address_type = Domain.Types.IP.type(address)
|
||||
{:ok, %__MODULE__{address_type: address_type, address: address, port: port}}
|
||||
else
|
||||
_error -> {:error, message: "is invalid"}
|
||||
end
|
||||
@@ -56,9 +57,6 @@ defmodule Domain.Types.IPPort do
|
||||
end
|
||||
end
|
||||
|
||||
defp type(address) when tuple_size(address) == 4, do: :ipv4
|
||||
defp type(address) when tuple_size(address) == 8, do: :ipv6
|
||||
|
||||
def dump(%__MODULE__{} = ip) do
|
||||
{:ok, __MODULE__.to_string(ip)}
|
||||
end
|
||||
|
||||
@@ -75,6 +75,9 @@ defmodule Domain.MixProject do
|
||||
{:opentelemetry_ecto, "~> 1.2"},
|
||||
{:opentelemetry_finch, "~> 0.2.0"},
|
||||
|
||||
# Other application deps
|
||||
{:tzdata, "~> 1.1"},
|
||||
|
||||
# Test and dev deps
|
||||
{:bypass, "~> 2.1", only: :test},
|
||||
{:credo, "~> 1.5", only: [:dev, :test], runtime: false},
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
defmodule Domain.Repo.Migrations.AddPoliciesConditions do
|
||||
use Ecto.Migration
|
||||
|
||||
def change do
|
||||
alter table(:policies) do
|
||||
add(:conditions, {:array, :map}, default: [])
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -25,6 +25,7 @@ account
|
||||
|> Ecto.Changeset.change(
|
||||
features: %{
|
||||
flow_activities: true,
|
||||
policy_conditions: true,
|
||||
multi_site_resources: true,
|
||||
traffic_filters: true,
|
||||
self_hosted_relays: true,
|
||||
|
||||
@@ -47,7 +47,11 @@ defmodule Domain.Config.ValidatorTest do
|
||||
|
||||
assert validate(:key, "invalid", type, []) ==
|
||||
{:error,
|
||||
{"invalid", ["must be one of: Elixir.Domain.Types.IP, Elixir.Domain.Types.CIDR"]}}
|
||||
{"invalid",
|
||||
[
|
||||
"must be one of: Elixir.Domain.Types.IP, Elixir.Domain.Types.CIDR",
|
||||
"CIDR netmask is invalid or missing"
|
||||
]}}
|
||||
|
||||
type = {:json_array, {:one_of, [:integer, :boolean]}}
|
||||
|
||||
|
||||
@@ -80,7 +80,151 @@ defmodule Domain.FlowsTest do
|
||||
authorize_flow(client, gateway, resource.id, subject)
|
||||
|
||||
assert fetched_resource.id == resource.id
|
||||
assert fetched_resource.authorized_by_policy.id == policy.id
|
||||
assert hd(fetched_resource.authorized_by_policies).id == policy.id
|
||||
end
|
||||
|
||||
test "returns error when some conditions are not satisfied", %{
|
||||
account: account,
|
||||
actor_group: actor_group,
|
||||
client: client,
|
||||
gateway_group: gateway_group,
|
||||
gateway: gateway,
|
||||
subject: subject
|
||||
} do
|
||||
resource =
|
||||
Fixtures.Resources.create_resource(
|
||||
account: account,
|
||||
connections: [%{gateway_group_id: gateway_group.id}]
|
||||
)
|
||||
|
||||
Fixtures.Policies.create_policy(
|
||||
account: account,
|
||||
actor_group: actor_group,
|
||||
resource: resource,
|
||||
conditions: [
|
||||
%{
|
||||
property: :remote_ip_location_region,
|
||||
operator: :is_in,
|
||||
values: ["AU"]
|
||||
},
|
||||
%{
|
||||
property: :remote_ip_location_region,
|
||||
operator: :is_not_in,
|
||||
values: [client.last_seen_remote_ip_location_region]
|
||||
},
|
||||
%{
|
||||
property: :remote_ip,
|
||||
operator: :is_in_cidr,
|
||||
values: ["0.0.0.0/0", "0::/0"]
|
||||
}
|
||||
]
|
||||
)
|
||||
|
||||
assert authorize_flow(client, gateway, resource.id, subject) ==
|
||||
{:error, {:forbidden, violated_properties: [:remote_ip_location_region]}}
|
||||
end
|
||||
|
||||
test "returns error when all conditions are not satisfied", %{
|
||||
account: account,
|
||||
actor_group: actor_group,
|
||||
client: client,
|
||||
gateway_group: gateway_group,
|
||||
gateway: gateway,
|
||||
subject: subject
|
||||
} do
|
||||
resource =
|
||||
Fixtures.Resources.create_resource(
|
||||
account: account,
|
||||
connections: [%{gateway_group_id: gateway_group.id}]
|
||||
)
|
||||
|
||||
Fixtures.Policies.create_policy(
|
||||
account: account,
|
||||
actor_group: actor_group,
|
||||
resource: resource,
|
||||
conditions: [
|
||||
%{
|
||||
property: :remote_ip_location_region,
|
||||
operator: :is_in,
|
||||
values: ["AU"]
|
||||
},
|
||||
%{
|
||||
property: :remote_ip_location_region,
|
||||
operator: :is_not_in,
|
||||
values: [client.last_seen_remote_ip_location_region]
|
||||
}
|
||||
]
|
||||
)
|
||||
|
||||
assert authorize_flow(client, gateway, resource.id, subject) ==
|
||||
{:error, {:forbidden, violated_properties: [:remote_ip_location_region]}}
|
||||
end
|
||||
|
||||
test "creates a flow when the only policy conditions are satisfied", %{
|
||||
account: account,
|
||||
actor: actor,
|
||||
resource: resource,
|
||||
client: client,
|
||||
policy: policy,
|
||||
gateway: gateway,
|
||||
subject: subject
|
||||
} do
|
||||
actor_group2 = Fixtures.Actors.create_group(account: account)
|
||||
Fixtures.Actors.create_membership(account: account, actor: actor, group: actor_group2)
|
||||
|
||||
Fixtures.Policies.create_policy(
|
||||
account: account,
|
||||
actor_group: actor_group2,
|
||||
resource: resource,
|
||||
conditions: [
|
||||
%{
|
||||
property: :remote_ip_location_region,
|
||||
operator: :is_not_in,
|
||||
values: [client.last_seen_remote_ip_location_region]
|
||||
}
|
||||
]
|
||||
)
|
||||
|
||||
assert {:ok, _fetched_resource, flow} =
|
||||
authorize_flow(client, gateway, resource.id, subject)
|
||||
|
||||
assert flow.policy_id == policy.id
|
||||
end
|
||||
|
||||
test "creates a flow when all conditions for at least one of the policies are satisfied", %{
|
||||
account: account,
|
||||
actor_group: actor_group,
|
||||
client: client,
|
||||
gateway_group: gateway_group,
|
||||
gateway: gateway,
|
||||
subject: subject
|
||||
} do
|
||||
resource =
|
||||
Fixtures.Resources.create_resource(
|
||||
account: account,
|
||||
connections: [%{gateway_group_id: gateway_group.id}]
|
||||
)
|
||||
|
||||
Fixtures.Policies.create_policy(
|
||||
account: account,
|
||||
actor_group: actor_group,
|
||||
resource: resource,
|
||||
conditions: [
|
||||
%{
|
||||
property: :remote_ip_location_region,
|
||||
operator: :is_in,
|
||||
values: [client.last_seen_remote_ip_location_region]
|
||||
},
|
||||
%{
|
||||
property: :remote_ip,
|
||||
operator: :is_in_cidr,
|
||||
values: ["0.0.0.0/0", "0::/0"]
|
||||
}
|
||||
]
|
||||
)
|
||||
|
||||
assert {:ok, _fetched_resource, _flow} =
|
||||
authorize_flow(client, gateway, resource.id, subject)
|
||||
end
|
||||
|
||||
test "creates a network flow for users", %{
|
||||
@@ -169,14 +313,17 @@ defmodule Domain.FlowsTest do
|
||||
other_client = Fixtures.Clients.create_client()
|
||||
other_gateway = Fixtures.Gateways.create_gateway()
|
||||
|
||||
assert authorize_flow(client, gateway, resource.id, other_subject) ==
|
||||
{:error, :internal_error}
|
||||
assert_raise FunctionClauseError, fn ->
|
||||
assert authorize_flow(client, gateway, resource.id, other_subject)
|
||||
end
|
||||
|
||||
assert authorize_flow(client, other_gateway, resource.id, subject) ==
|
||||
{:error, :internal_error}
|
||||
assert_raise FunctionClauseError, fn ->
|
||||
assert authorize_flow(client, other_gateway, resource.id, subject)
|
||||
end
|
||||
|
||||
assert authorize_flow(other_client, gateway, resource.id, subject) ==
|
||||
{:error, :internal_error}
|
||||
assert_raise FunctionClauseError, fn ->
|
||||
assert authorize_flow(other_client, gateway, resource.id, subject)
|
||||
end
|
||||
end
|
||||
|
||||
test "returns error when subject has no permission to create flows", %{
|
||||
|
||||
@@ -0,0 +1,480 @@
|
||||
defmodule Domain.Policies.Condition.EvaluatorTest do
|
||||
use Domain.DataCase, async: true
|
||||
import Domain.Policies.Condition.Evaluator
|
||||
|
||||
describe "ensure_conforms/2" do
|
||||
test "returns ok when there are no conditions" do
|
||||
client = %Domain.Clients.Client{}
|
||||
assert ensure_conforms([], client) == :ok
|
||||
end
|
||||
|
||||
test "returns ok when all conditions are met" do
|
||||
client = %Domain.Clients.Client{
|
||||
last_seen_remote_ip_location_region: "US",
|
||||
last_seen_remote_ip: %Postgrex.INET{address: {192, 168, 0, 1}}
|
||||
}
|
||||
|
||||
conditions = [
|
||||
%Domain.Policies.Condition{
|
||||
property: :remote_ip_location_region,
|
||||
operator: :is_in,
|
||||
values: ["US"]
|
||||
},
|
||||
%Domain.Policies.Condition{
|
||||
property: :remote_ip,
|
||||
operator: :is_in_cidr,
|
||||
values: ["192.168.0.1/24"]
|
||||
}
|
||||
]
|
||||
|
||||
assert ensure_conforms(conditions, client) == :ok
|
||||
end
|
||||
|
||||
test "returns error when all conditions are not met" do
|
||||
client = %Domain.Clients.Client{
|
||||
last_seen_remote_ip_location_region: "US",
|
||||
last_seen_remote_ip: %Postgrex.INET{address: {192, 168, 0, 1}}
|
||||
}
|
||||
|
||||
conditions = [
|
||||
%Domain.Policies.Condition{
|
||||
property: :remote_ip_location_region,
|
||||
operator: :is_in,
|
||||
values: ["CN"]
|
||||
},
|
||||
%Domain.Policies.Condition{
|
||||
property: :remote_ip,
|
||||
operator: :is_in_cidr,
|
||||
values: ["10.10.0.1/24"]
|
||||
}
|
||||
]
|
||||
|
||||
assert ensure_conforms(conditions, client) ==
|
||||
{:error, [:remote_ip_location_region, :remote_ip]}
|
||||
end
|
||||
|
||||
test "returns error when one of the conditions is not met" do
|
||||
client = %Domain.Clients.Client{
|
||||
last_seen_remote_ip_location_region: "US",
|
||||
last_seen_remote_ip: %Postgrex.INET{address: {192, 168, 0, 1}}
|
||||
}
|
||||
|
||||
conditions = [
|
||||
%Domain.Policies.Condition{
|
||||
property: :remote_ip_location_region,
|
||||
operator: :is_in,
|
||||
values: ["CN"]
|
||||
},
|
||||
%Domain.Policies.Condition{
|
||||
property: :remote_ip,
|
||||
operator: :is_in_cidr,
|
||||
values: ["192.168.0.1/24"]
|
||||
}
|
||||
]
|
||||
|
||||
assert ensure_conforms(conditions, client) ==
|
||||
{:error, [:remote_ip_location_region]}
|
||||
end
|
||||
end
|
||||
|
||||
describe "conforms?/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,
|
||||
values: ["US"]
|
||||
}
|
||||
|
||||
client = %Domain.Clients.Client{
|
||||
last_seen_remote_ip_location_region: "US"
|
||||
}
|
||||
|
||||
assert conforms?(%{condition | operator: :is_in}, client) == true
|
||||
assert conforms?(%{condition | operator: :is_not_in}, client) == false
|
||||
|
||||
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
|
||||
end
|
||||
|
||||
test "when client last seen remote ip is in or not in the CIDR values" do
|
||||
condition = %Domain.Policies.Condition{
|
||||
property: :remote_ip,
|
||||
values: ["192.168.0.1/24"]
|
||||
}
|
||||
|
||||
client = %Domain.Clients.Client{
|
||||
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
|
||||
|
||||
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
|
||||
end
|
||||
|
||||
test "when client last seen remote ip is in or not in the IP values" do
|
||||
condition = %Domain.Policies.Condition{
|
||||
property: :remote_ip,
|
||||
values: ["192.168.0.1", "2001:0000:130F:0000:0000:09C0:876A:130B"]
|
||||
}
|
||||
|
||||
client = %Domain.Clients.Client{
|
||||
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
|
||||
|
||||
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
|
||||
end
|
||||
|
||||
test "when client identity provider id is in or not in the values" do
|
||||
condition = %Domain.Policies.Condition{
|
||||
property: :provider_id,
|
||||
values: ["00000000-0000-0000-0000-000000000000"]
|
||||
}
|
||||
|
||||
client = %Domain.Clients.Client{
|
||||
identity: %Domain.Auth.Identity{
|
||||
provider_id: "00000000-0000-0000-0000-000000000000"
|
||||
}
|
||||
}
|
||||
|
||||
assert conforms?(%{condition | operator: :is_in}, client) == true
|
||||
assert conforms?(%{condition | operator: :is_not_in}, client) == false
|
||||
|
||||
client = %Domain.Clients.Client{
|
||||
identity: %Domain.Auth.Identity{
|
||||
provider_id: "11111111-1111-1111-1111-111111111111"
|
||||
}
|
||||
}
|
||||
|
||||
assert conforms?(%{condition | operator: :is_in}, client) == false
|
||||
assert conforms?(%{condition | operator: :is_not_in}, client) == true
|
||||
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
|
||||
condition = %Domain.Policies.Condition{
|
||||
property: :current_utc_datetime,
|
||||
values: []
|
||||
}
|
||||
|
||||
client = %Domain.Clients.Client{}
|
||||
|
||||
assert conforms?(%{condition | operator: :is_in_day_of_week_time_ranges}, client) == false
|
||||
end
|
||||
end
|
||||
|
||||
describe "datetime_in_day_of_the_week_time_ranges?/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]
|
||||
|
||||
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
|
||||
|
||||
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
|
||||
|
||||
dow_time_ranges = ["F/true/UTC"]
|
||||
assert datetime_in_day_of_the_week_time_ranges?(datetime, dow_time_ranges) == true
|
||||
end
|
||||
|
||||
test "returns false when datetime is not in the day of the week time ranges" do
|
||||
# Friday
|
||||
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
|
||||
|
||||
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
|
||||
|
||||
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
|
||||
|
||||
dow_time_ranges = ["U/true/UTC"]
|
||||
assert datetime_in_day_of_the_week_time_ranges?(datetime, dow_time_ranges) == false
|
||||
end
|
||||
|
||||
test "handles different timezones" do
|
||||
# Friday in UTC, Thursday in US/Pacific (UTC-8)
|
||||
datetime = ~U[2021-01-01 01:00:00Z]
|
||||
|
||||
dow_time_ranges = ["R/true/US/Pacific"]
|
||||
assert datetime_in_day_of_the_week_time_ranges?(datetime, dow_time_ranges) == true
|
||||
|
||||
dow_time_ranges = ["F/true/UTC"]
|
||||
assert datetime_in_day_of_the_week_time_ranges?(datetime, dow_time_ranges) == true
|
||||
|
||||
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
|
||||
|
||||
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
|
||||
|
||||
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
|
||||
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
|
||||
|
||||
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
|
||||
|
||||
dow_time_ranges = ["F/false/UTC"]
|
||||
assert datetime_in_day_of_the_week_time_ranges?(datetime, dow_time_ranges) == false
|
||||
|
||||
dow_time_ranges = ["F/10-11"]
|
||||
assert datetime_in_day_of_the_week_time_ranges?(datetime, dow_time_ranges) == false
|
||||
|
||||
dow_time_ranges = ["F/10-11/"]
|
||||
assert datetime_in_day_of_the_week_time_ranges?(datetime, dow_time_ranges) == false
|
||||
end
|
||||
end
|
||||
|
||||
describe "parse_days_of_week_time_ranges/1" do
|
||||
test "parses list of days of the week time ranges" do
|
||||
assert parse_days_of_week_time_ranges(["M/true/UTC"]) ==
|
||||
{:ok, %{"M" => [{~T[00:00:00], ~T[23:59:59], "UTC"}]}}
|
||||
|
||||
assert parse_days_of_week_time_ranges(["M/true/UTC", "W/19:00:00-22:00:00,22-23/US/Pacific"]) ==
|
||||
{:ok,
|
||||
%{
|
||||
"M" => [
|
||||
{~T[00:00:00], ~T[23:59:59], "UTC"}
|
||||
],
|
||||
"W" => [
|
||||
{~T[19:00:00], ~T[22:00:00], "US/Pacific"},
|
||||
{~T[22:00:00], ~T[23:00:00], "US/Pacific"}
|
||||
]
|
||||
}}
|
||||
end
|
||||
|
||||
test "merges list of days of the week time ranges" do
|
||||
assert parse_days_of_week_time_ranges([
|
||||
"M/true,10:00:00-11:00:00/UTC",
|
||||
"W/19:00:00-22:00:00/US/Pacific"
|
||||
]) ==
|
||||
{:ok,
|
||||
%{
|
||||
"M" => [
|
||||
{~T[00:00:00], ~T[23:59:59], "UTC"},
|
||||
{~T[10:00:00], ~T[11:00:00], "UTC"}
|
||||
],
|
||||
"W" => [
|
||||
{~T[19:00:00], ~T[22:00:00], "US/Pacific"}
|
||||
]
|
||||
}}
|
||||
|
||||
assert parse_days_of_week_time_ranges([
|
||||
"M/true/UTC",
|
||||
"W/19:00:00-22:00:00/UTC",
|
||||
"M/10:00:00-11:00:00/UTC"
|
||||
]) ==
|
||||
{:ok,
|
||||
%{
|
||||
"M" => [
|
||||
{~T[00:00:00], ~T[23:59:59], "UTC"},
|
||||
{~T[10:00:00], ~T[11:00:00], "UTC"}
|
||||
],
|
||||
"W" => [
|
||||
{~T[19:00:00], ~T[22:00:00], "UTC"}
|
||||
]
|
||||
}}
|
||||
|
||||
assert parse_days_of_week_time_ranges([
|
||||
"M/22:00:00-22:30/UTC",
|
||||
"M/19-22:00:00/UTC",
|
||||
"M/true/UTC"
|
||||
]) ==
|
||||
{:ok,
|
||||
%{
|
||||
"M" => [
|
||||
{~T[22:00:00], ~T[22:30:00], "UTC"},
|
||||
{~T[19:00:00], ~T[22:00:00], "UTC"},
|
||||
{~T[00:00:00], ~T[23:59:59], "UTC"}
|
||||
]
|
||||
}}
|
||||
|
||||
assert parse_days_of_week_time_ranges([
|
||||
"M/09:00:00-10:00:00/UTC",
|
||||
"W/19:00:00-22:00:00/UTC",
|
||||
"M/10:00:00-11:00:00/UTC"
|
||||
]) ==
|
||||
{:ok,
|
||||
%{
|
||||
"M" => [
|
||||
{~T[09:00:00], ~T[10:00:00], "UTC"},
|
||||
{~T[10:00:00], ~T[11:00:00], "UTC"}
|
||||
],
|
||||
"W" => [
|
||||
{~T[19:00:00], ~T[22:00:00], "UTC"}
|
||||
]
|
||||
}}
|
||||
end
|
||||
|
||||
test "returns error on invalid timezone" do
|
||||
assert parse_days_of_week_time_ranges(["M/true"]) ==
|
||||
{:error, "timezone is required"}
|
||||
|
||||
assert parse_days_of_week_time_ranges(["M/true/invalid"]) ==
|
||||
{:error, "invalid timezone"}
|
||||
end
|
||||
end
|
||||
|
||||
describe "parse_day_of_week_time_ranges/1" do
|
||||
test "parses 7 days of the week" do
|
||||
for day <- ~w[M T W R F S U] do
|
||||
assert {:ok,
|
||||
{^day,
|
||||
[
|
||||
{~T[00:00:00], ~T[23:59:59], "US/Pacific"}
|
||||
]}} = parse_day_of_week_time_ranges("#{day}/true/US/Pacific")
|
||||
end
|
||||
end
|
||||
|
||||
test "parses day of week time ranges" do
|
||||
assert parse_day_of_week_time_ranges("M/08:00:00-17:00:00,22:00:00-23:59:59/America/Merida") ==
|
||||
{:ok,
|
||||
{"M",
|
||||
[
|
||||
{~T[08:00:00], ~T[17:00:00], "America/Merida"},
|
||||
{~T[22:00:00], ~T[23:59:59], "America/Merida"}
|
||||
]}}
|
||||
|
||||
assert parse_day_of_week_time_ranges("U/08:00:00-17:00:00/UTC") ==
|
||||
{:ok, {"U", [{~T[08:00:00], ~T[17:00:00], "UTC"}]}}
|
||||
|
||||
assert parse_day_of_week_time_ranges("U/08:00-17:00:00/US/Pacific") ==
|
||||
{:ok, {"U", [{~T[08:00:00], ~T[17:00:00], "US/Pacific"}]}}
|
||||
end
|
||||
|
||||
test "returns error when invalid day of week is provided" do
|
||||
assert parse_day_of_week_time_ranges("X/08:00:00-17:00:00/UTC") ==
|
||||
{:error, "invalid day of the week, must be one of M, T, W, R, F, S, U"}
|
||||
end
|
||||
|
||||
test "returns error when invalid time range is provided" do
|
||||
assert parse_day_of_week_time_ranges("M/08:00:00-17:00:00-/UTC") ==
|
||||
{:error, "invalid time range: 08:00:00-17:00:00-"}
|
||||
end
|
||||
|
||||
test "returns error when invalid time is provided" do
|
||||
assert parse_day_of_week_time_ranges("M/25-17:00:00/UTC") ==
|
||||
{:error, "invalid time range: 25-17:00:00"}
|
||||
|
||||
assert parse_day_of_week_time_ranges("M/08:00:00-25/UTC") ==
|
||||
{:error, "invalid time range: 08:00:00-25"}
|
||||
end
|
||||
|
||||
test "returns error when start of the time range is greater than the end of it" do
|
||||
assert {:error, "start of the time range must be less than or equal to the end of it"} =
|
||||
parse_day_of_week_time_ranges("M/17:00:00-08:00:00/UTC")
|
||||
end
|
||||
end
|
||||
|
||||
describe "parse_time_ranges/1" do
|
||||
test "parses time ranges" do
|
||||
assert parse_time_ranges("true") ==
|
||||
{:ok, [{~T[00:00:00], ~T[23:59:59]}]}
|
||||
|
||||
assert parse_time_ranges("08:00:00-17:00:00") ==
|
||||
{:ok, [{~T[08:00:00], ~T[17:00:00]}]}
|
||||
|
||||
assert parse_time_ranges("08:00-17:00:00") ==
|
||||
{:ok, [{~T[08:00:00], ~T[17:00:00]}]}
|
||||
|
||||
assert parse_time_ranges("08:00:00-17:00") ==
|
||||
{:ok, [{~T[08:00:00], ~T[17:00:00]}]}
|
||||
|
||||
assert parse_time_ranges("08-17:00:00") ==
|
||||
{:ok, [{~T[08:00:00], ~T[17:00:00]}]}
|
||||
|
||||
assert parse_time_ranges("08:00:00-17") ==
|
||||
{:ok, [{~T[08:00:00], ~T[17:00:00]}]}
|
||||
|
||||
assert parse_time_ranges("08:00:00-17:00:00,09:00:00-10:00:00") ==
|
||||
{:ok, [{~T[08:00:00], ~T[17:00:00]}, {~T[09:00:00], ~T[10:00:00]}]}
|
||||
end
|
||||
|
||||
test "returns error when invalid time range is provided" do
|
||||
assert parse_time_ranges("08:00:00-17:00:00-") ==
|
||||
{:error, "invalid time range: 08:00:00-17:00:00-"}
|
||||
end
|
||||
|
||||
test "returns error when invalid time is provided" do
|
||||
assert parse_time_ranges("25:00:00-17:00:00") ==
|
||||
{:error, "invalid time range: 25:00:00-17:00:00"}
|
||||
|
||||
assert parse_time_ranges("08:00:00-25:00:00") ==
|
||||
{:error, "invalid time range: 08:00:00-25:00:00"}
|
||||
|
||||
assert parse_time_ranges("25-17:00:00") ==
|
||||
{:error, "invalid time range: 25-17:00:00"}
|
||||
|
||||
assert parse_time_ranges("08:00:00-25") ==
|
||||
{:error, "invalid time range: 08:00:00-25"}
|
||||
end
|
||||
|
||||
test "returns error when start of the time range is greater than the end of it" do
|
||||
assert {:error, "start of the time range must be less than or equal to the end of it"} =
|
||||
parse_time_ranges("17:00:00-08:00:00")
|
||||
end
|
||||
end
|
||||
|
||||
describe "parse_time_range/1" do
|
||||
test "parses time range" do
|
||||
assert parse_time_range("08:00:00-17:00:00") ==
|
||||
{:ok, {~T[08:00:00], ~T[17:00:00]}}
|
||||
|
||||
assert parse_time_range("08:00-17:00:00") ==
|
||||
{:ok, {~T[08:00:00], ~T[17:00:00]}}
|
||||
|
||||
assert parse_time_range("08:00:00-17:00") ==
|
||||
{:ok, {~T[08:00:00], ~T[17:00:00]}}
|
||||
|
||||
assert parse_time_range("true") ==
|
||||
{:ok, {~T[00:00:00], ~T[23:59:59]}}
|
||||
end
|
||||
|
||||
test "returns error when invalid time range is provided" do
|
||||
assert parse_time_range("08:00:00-17:00:00-") ==
|
||||
{:error, "invalid time range: 08:00:00-17:00:00-"}
|
||||
end
|
||||
|
||||
test "returns error when invalid time is provided" do
|
||||
assert parse_time_range("25-17:00:00") ==
|
||||
{:error, "invalid time range: 25-17:00:00"}
|
||||
|
||||
assert parse_time_range("08:00:00-33") ==
|
||||
{:error, "invalid time range: 08:00:00-33"}
|
||||
end
|
||||
|
||||
test "returns error when start of the time range is greater than the end of it" do
|
||||
assert {:error, "start of the time range must be less than or equal to the end of it"} =
|
||||
parse_time_range("17:00:00-08:00:00")
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -1,5 +1,4 @@
|
||||
defmodule Domain.PoliciesTest do
|
||||
alias Web.Policies
|
||||
use Domain.DataCase, async: true
|
||||
import Domain.Policies
|
||||
alias Domain.Policies
|
||||
@@ -232,6 +231,74 @@ defmodule Domain.PoliciesTest do
|
||||
assert policy.resource_id == resource.id
|
||||
end
|
||||
|
||||
test "creates a policy with conditions", %{
|
||||
account: account,
|
||||
subject: subject
|
||||
} do
|
||||
resource = Fixtures.Resources.create_resource(account: account)
|
||||
actor_group = Fixtures.Actors.create_group(account: account)
|
||||
|
||||
attrs = %{
|
||||
actor_group_id: actor_group.id,
|
||||
resource_id: resource.id,
|
||||
conditions: [
|
||||
%{
|
||||
property: :remote_ip,
|
||||
operator: :is_in_cidr,
|
||||
values: ["10.10.0.0/24"]
|
||||
},
|
||||
%{
|
||||
property: :remote_ip_location_region,
|
||||
operator: :is_in,
|
||||
values: ["US"]
|
||||
},
|
||||
%{
|
||||
property: :provider_id,
|
||||
operator: :is_not_in,
|
||||
values: ["3c712b5d-b1af-4b5a-9f33-aa3d1a4dc296"]
|
||||
},
|
||||
%{
|
||||
property: :current_utc_datetime,
|
||||
operator: :is_in_day_of_week_time_ranges,
|
||||
values: [
|
||||
"M/13:00:00-15:00:00,19:00:00-22:00:00/Poland",
|
||||
"F/08:00:00-20:00:00/UTC",
|
||||
"S/true/US/Pacific"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
assert {:ok, policy} = create_policy(attrs, subject)
|
||||
|
||||
assert policy.conditions == [
|
||||
%Policies.Condition{
|
||||
property: :remote_ip,
|
||||
operator: :is_in_cidr,
|
||||
values: ["10.10.0.0/24"]
|
||||
},
|
||||
%Policies.Condition{
|
||||
property: :remote_ip_location_region,
|
||||
operator: :is_in,
|
||||
values: ["US"]
|
||||
},
|
||||
%Policies.Condition{
|
||||
property: :provider_id,
|
||||
operator: :is_not_in,
|
||||
values: ["3c712b5d-b1af-4b5a-9f33-aa3d1a4dc296"]
|
||||
},
|
||||
%Policies.Condition{
|
||||
property: :current_utc_datetime,
|
||||
operator: :is_in_day_of_week_time_ranges,
|
||||
values: [
|
||||
"M/13:00:00-15:00:00,19:00:00-22:00:00/Poland",
|
||||
"F/08:00:00-20:00:00/UTC",
|
||||
"S/true/US/Pacific"
|
||||
]
|
||||
}
|
||||
]
|
||||
end
|
||||
|
||||
test "broadcasts an account message when policy is created", %{
|
||||
account: account,
|
||||
subject: subject
|
||||
@@ -895,4 +962,43 @@ defmodule Domain.PoliciesTest do
|
||||
assert delete_policies_for(resource, subject) == {:ok, []}
|
||||
end
|
||||
end
|
||||
|
||||
describe "ensure_client_conforms_policy_conditions/2" do
|
||||
test "returns :ok when client conforms to policy conditions", %{} do
|
||||
client = %Domain.Clients.Client{
|
||||
last_seen_remote_ip_location_region: "US"
|
||||
}
|
||||
|
||||
policy = %Policies.Policy{
|
||||
conditions: [
|
||||
%Policies.Condition{
|
||||
property: :remote_ip_location_region,
|
||||
operator: :is_in,
|
||||
values: ["US"]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
assert ensure_client_conforms_policy_conditions(client, policy) == :ok
|
||||
end
|
||||
|
||||
test "returns error when client conforms to policy conditions", %{} do
|
||||
client = %Domain.Clients.Client{
|
||||
last_seen_remote_ip_location_region: "US"
|
||||
}
|
||||
|
||||
policy = %Policies.Policy{
|
||||
conditions: [
|
||||
%Policies.Condition{
|
||||
property: :remote_ip_location_region,
|
||||
operator: :is_in,
|
||||
values: ["CA"]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
assert ensure_client_conforms_policy_conditions(client, policy) ==
|
||||
{:error, {:forbidden, [violated_properties: [:remote_ip_location_region]]}}
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -32,7 +32,6 @@ defmodule Domain.ResourcesTest do
|
||||
|
||||
assert {:ok, fetched_resource} = fetch_resource_by_id(resource.id, subject)
|
||||
assert fetched_resource.id == resource.id
|
||||
assert is_nil(fetched_resource.authorized_by_policy)
|
||||
end
|
||||
|
||||
test "returns authorized resource for account user", %{
|
||||
@@ -58,7 +57,7 @@ defmodule Domain.ResourcesTest do
|
||||
|
||||
assert {:ok, fetched_resource} = fetch_resource_by_id(resource.id, subject)
|
||||
assert fetched_resource.id == resource.id
|
||||
assert fetched_resource.authorized_by_policy.id == policy.id
|
||||
assert Enum.map(fetched_resource.authorized_by_policies, & &1.id) == [policy.id]
|
||||
end
|
||||
|
||||
test "returns deleted resources", %{account: account, subject: subject} do
|
||||
@@ -135,7 +134,7 @@ defmodule Domain.ResourcesTest do
|
||||
|
||||
assert {:ok, fetched_resource} = fetch_and_authorize_resource_by_id(resource.id, subject)
|
||||
assert fetched_resource.id == resource.id
|
||||
assert fetched_resource.authorized_by_policy.id == policy.id
|
||||
assert Enum.map(fetched_resource.authorized_by_policies, & &1.id) == [policy.id]
|
||||
end
|
||||
|
||||
test "returns authorized resource for account user", %{
|
||||
@@ -161,7 +160,7 @@ defmodule Domain.ResourcesTest do
|
||||
|
||||
assert {:ok, fetched_resource} = fetch_and_authorize_resource_by_id(resource.id, subject)
|
||||
assert fetched_resource.id == resource.id
|
||||
assert fetched_resource.authorized_by_policy.id == policy.id
|
||||
assert Enum.map(fetched_resource.authorized_by_policies, & &1.id) == [policy.id]
|
||||
end
|
||||
|
||||
test "returns authorized resource using one of multiple policies for account user", %{
|
||||
@@ -194,7 +193,10 @@ defmodule Domain.ResourcesTest do
|
||||
|
||||
assert {:ok, fetched_resource} = fetch_and_authorize_resource_by_id(resource.id, subject)
|
||||
assert fetched_resource.id == resource.id
|
||||
assert fetched_resource.authorized_by_policy.id in [policy1.id, policy2.id]
|
||||
|
||||
authorized_by_policy_ids = Enum.map(fetched_resource.authorized_by_policies, & &1.id)
|
||||
policy_ids = [policy1.id, policy2.id]
|
||||
assert Enum.sort(authorized_by_policy_ids) == Enum.sort(policy_ids)
|
||||
end
|
||||
|
||||
test "does not return deleted resources", %{account: account, actor: actor, subject: subject} do
|
||||
@@ -418,7 +420,7 @@ defmodule Domain.ResourcesTest do
|
||||
|
||||
assert {:ok, resources} = all_authorized_resources(subject)
|
||||
assert length(resources) == 1
|
||||
assert hd(resources).authorized_by_policy.id == policy.id
|
||||
assert Enum.map(hd(resources).authorized_by_policies, & &1.id) == [policy.id]
|
||||
|
||||
policy2 =
|
||||
Fixtures.Policies.create_policy(
|
||||
@@ -430,7 +432,7 @@ defmodule Domain.ResourcesTest do
|
||||
assert {:ok, resources2} = all_authorized_resources(subject)
|
||||
assert length(resources2) == 2
|
||||
|
||||
assert hd(resources2 -- resources).authorized_by_policy.id == policy2.id
|
||||
assert hd(hd(resources2 -- resources).authorized_by_policies).id == policy2.id
|
||||
end
|
||||
|
||||
test "returns authorized resources for account admin subject", %{
|
||||
@@ -472,7 +474,7 @@ defmodule Domain.ResourcesTest do
|
||||
|
||||
assert {:ok, resources} = all_authorized_resources(subject)
|
||||
assert length(resources) == 1
|
||||
assert hd(resources).authorized_by_policy.id == policy.id
|
||||
assert Enum.map(hd(resources).authorized_by_policies, & &1.id) == [policy.id]
|
||||
|
||||
Fixtures.Policies.create_policy(
|
||||
account: account,
|
||||
|
||||
@@ -19,6 +19,7 @@ defmodule Domain.Fixtures.Accounts do
|
||||
},
|
||||
features: %{
|
||||
flow_activities: true,
|
||||
policy_conditions: true,
|
||||
multi_site_resources: true,
|
||||
traffic_filters: true,
|
||||
self_hosted_relays: true,
|
||||
|
||||
@@ -1158,6 +1158,12 @@ defmodule Web.CoreComponents do
|
||||
"""
|
||||
end
|
||||
|
||||
def feature_name(%{feature: :policy_conditions} = assigns) do
|
||||
~H"""
|
||||
Define Policy Conditions
|
||||
"""
|
||||
end
|
||||
|
||||
def feature_name(%{feature: :multi_site_resources} = assigns) do
|
||||
~H"""
|
||||
Define globally-distributed resources
|
||||
|
||||
@@ -39,6 +39,7 @@ defmodule Web.FormComponents do
|
||||
doc: "a form field struct retrieved from the form, for example: @form[:email]"
|
||||
|
||||
attr :errors, :list, default: []
|
||||
attr :value_index, :integer, default: nil
|
||||
|
||||
attr :inline_errors, :boolean,
|
||||
default: false,
|
||||
@@ -58,9 +59,20 @@ defmodule Web.FormComponents do
|
||||
slot :inner_block
|
||||
|
||||
def input(%{field: %Phoenix.HTML.FormField{} = field} = assigns) do
|
||||
errors =
|
||||
if assigns.value_index do
|
||||
Enum.filter(field.errors, fn {_error, meta} ->
|
||||
Keyword.get(meta, :validated_as) == :list and
|
||||
Keyword.get(meta, :at) == assigns.value_index
|
||||
end)
|
||||
else
|
||||
field.errors
|
||||
end
|
||||
|> Enum.map(&translate_error(&1))
|
||||
|
||||
assigns
|
||||
|> assign(field: nil, id: assigns.id || field.id)
|
||||
|> assign(:errors, Enum.map(field.errors, &translate_error(&1)))
|
||||
|> assign(:errors, errors)
|
||||
|> assign_new(:name, fn -> if assigns.multiple, do: field.name <> "[]", else: field.name end)
|
||||
|> assign_new(:value, fn ->
|
||||
if assigns.value_id do
|
||||
@@ -144,7 +156,7 @@ defmodule Web.FormComponents do
|
||||
<div phx-feedback-for={@name}>
|
||||
<.label :if={@label} for={@id}><%= @label %></.label>
|
||||
<input
|
||||
:if={not is_nil(@value) and not is_nil(@rest[:disabled])}
|
||||
:if={not is_nil(@value) and @rest[:disabled] == true}
|
||||
type="hidden"
|
||||
name={@name}
|
||||
value={@value}
|
||||
@@ -185,7 +197,7 @@ defmodule Web.FormComponents do
|
||||
<div phx-feedback-for={@name}>
|
||||
<.label for={@id}><%= @label %></.label>
|
||||
<input
|
||||
:if={not is_nil(@value) and not is_nil(@rest[:disabled])}
|
||||
:if={@rest[:disabled] in [true, "true"] and not is_nil(@value)}
|
||||
type="hidden"
|
||||
name={@name}
|
||||
value={@value}
|
||||
@@ -542,7 +554,7 @@ defmodule Web.FormComponents do
|
||||
[text[size], spacing[size]]
|
||||
end
|
||||
|
||||
defp icon_size(size) do
|
||||
def icon_size(size) do
|
||||
icon_size = %{
|
||||
"xs" => "w-3 h-3",
|
||||
"sm" => "w-3 h-3",
|
||||
|
||||
@@ -405,9 +405,9 @@ defmodule Web.Actors.Show do
|
||||
</.section>
|
||||
|
||||
<.section>
|
||||
<:title>Activity</:title>
|
||||
<:title>Authorized Activity</:title>
|
||||
<:help>
|
||||
Attempts to access resources by this actor.
|
||||
Authorized attempts by actors to access the resource governed by this policy.
|
||||
</:help>
|
||||
<:content>
|
||||
<.live_table
|
||||
|
||||
@@ -156,9 +156,9 @@ defmodule Web.Clients.Show do
|
||||
</.section>
|
||||
|
||||
<.section>
|
||||
<:title>Activity</:title>
|
||||
<:title>Authorized Activity</:title>
|
||||
<:help>
|
||||
Attempts by the actor using this client to access resources.
|
||||
Authorized attempts by actors to access the resource governed by this policy.
|
||||
</:help>
|
||||
<:content>
|
||||
<.live_table
|
||||
|
||||
@@ -1,9 +1,619 @@
|
||||
defmodule Web.Policies.Components do
|
||||
use Web, :component_library
|
||||
alias Domain.Policies
|
||||
|
||||
@days_of_week [
|
||||
{"M", "Monday"},
|
||||
{"T", "Tuesday"},
|
||||
{"W", "Wednesday"},
|
||||
{"R", "Thursday"},
|
||||
{"F", "Friday"},
|
||||
{"S", "Saturday"},
|
||||
{"U", "Sunday"}
|
||||
]
|
||||
|
||||
attr :policy, :map, required: true
|
||||
|
||||
def policy_name(assigns) do
|
||||
~H"<%= @policy.actor_group.name %> → <%= @policy.resource.name %>"
|
||||
end
|
||||
|
||||
def map_condition_params(attrs, opts) do
|
||||
Map.update(attrs, "conditions", %{}, fn conditions ->
|
||||
for {property, condition_attrs} <- conditions,
|
||||
maybe_filter(condition_attrs, opts),
|
||||
condition_attrs = map_condition_values(condition_attrs),
|
||||
into: %{} do
|
||||
{property, condition_attrs}
|
||||
end
|
||||
end)
|
||||
end
|
||||
|
||||
defp maybe_filter(%{"values" => values}, empty_values: :drop) when is_list(values) do
|
||||
not (values
|
||||
|> List.wrap()
|
||||
|> Enum.reject(fn value -> value in [nil, ""] end)
|
||||
|> Enum.empty?())
|
||||
end
|
||||
|
||||
defp maybe_filter(%{"values" => values}, empty_values: :drop) when is_map(values) do
|
||||
not (values
|
||||
|> Enum.reject(fn {_key, value} -> value in [nil, ""] end)
|
||||
|> Enum.empty?())
|
||||
end
|
||||
|
||||
defp maybe_filter(_condition_attrs, _opts) do
|
||||
true
|
||||
end
|
||||
|
||||
defp map_condition_values(
|
||||
%{
|
||||
"operator" => "is_in_day_of_week_time_ranges",
|
||||
"timezone" => timezone
|
||||
} = condition_attrs
|
||||
) do
|
||||
Map.update(condition_attrs, "values", [], fn values ->
|
||||
values
|
||||
|> Enum.sort_by(fn {dow, _} -> day_of_week_index(dow) end)
|
||||
|> Enum.map(fn {dow, time_ranges} ->
|
||||
"#{dow}/#{time_ranges}/#{timezone}"
|
||||
end)
|
||||
end)
|
||||
end
|
||||
|
||||
defp map_condition_values(condition_attrs) do
|
||||
condition_attrs
|
||||
end
|
||||
|
||||
defp condition_values_empty?(%{
|
||||
params: %{
|
||||
"operator" => "is_in_day_of_week_time_ranges",
|
||||
"values" => values
|
||||
}
|
||||
}) do
|
||||
values
|
||||
|> Enum.reject(fn value ->
|
||||
case String.split(value, "/") do
|
||||
[_, ranges, _] -> ranges == ""
|
||||
_ -> true
|
||||
end
|
||||
end)
|
||||
|> Enum.empty?()
|
||||
end
|
||||
|
||||
defp condition_values_empty?(%{
|
||||
params: %{"values" => values}
|
||||
}) do
|
||||
values
|
||||
|> List.wrap()
|
||||
|> Enum.reject(fn value -> value in [nil, ""] end)
|
||||
|> Enum.empty?()
|
||||
end
|
||||
|
||||
defp condition_values_empty?(%{}) do
|
||||
true
|
||||
end
|
||||
|
||||
def conditions(assigns) do
|
||||
~H"""
|
||||
<span :if={@conditions == []} class="text-neutral-500">
|
||||
There are no conditions defined for this policy.
|
||||
</span>
|
||||
<span :if={@conditions != []} class="flex flex-wrap">
|
||||
<span class="mr-1">This policy can be used</span>
|
||||
<.condition
|
||||
:for={condition <- @conditions}
|
||||
providers={@providers}
|
||||
property={condition.property}
|
||||
operator={condition.operator}
|
||||
values={condition.values}
|
||||
/>
|
||||
</span>
|
||||
"""
|
||||
end
|
||||
|
||||
defp condition(%{property: :remote_ip_location_region} = assigns) do
|
||||
~H"""
|
||||
<span :if={@values != []} class="mr-1">
|
||||
<span :if={@operator == :is_in}>from</span>
|
||||
<span :if={@operator == :is_not_in}>from any counties except</span>
|
||||
<span class="font-medium">
|
||||
<%= @values |> Enum.map(&Domain.Geo.country_common_name!/1) |> Enum.join(", ") %>
|
||||
</span>
|
||||
</span>
|
||||
"""
|
||||
end
|
||||
|
||||
defp condition(%{property: :remote_ip} = assigns) do
|
||||
~H"""
|
||||
<span :if={@values != []} class="mr-1">
|
||||
<span>from IP addresses that are</span> <span :if={@operator == :is_in_cidr}>in</span>
|
||||
<span :if={@operator == :is_not_in_cidr}>not in</span>
|
||||
<span class="font-medium"><%= Enum.join(@values, ", ") %></span>
|
||||
</span>
|
||||
"""
|
||||
end
|
||||
|
||||
defp condition(%{property: :provider_id} = assigns) do
|
||||
assigns =
|
||||
assign(
|
||||
assigns,
|
||||
:providers,
|
||||
assigns.values
|
||||
|> Enum.map(fn provider_id ->
|
||||
Enum.find(assigns.providers, fn provider ->
|
||||
provider.id == provider_id
|
||||
end)
|
||||
end)
|
||||
|> Enum.reject(&is_nil/1)
|
||||
)
|
||||
|
||||
~H"""
|
||||
<span :if={@providers != []} class="flex flex-wrap space-x-1 mr-1">
|
||||
<span>when signed in</span>
|
||||
<span :if={@operator == :is_in}>with</span>
|
||||
<span :if={@operator == :is_not_in}>not with</span>
|
||||
<.intersperse_blocks>
|
||||
<:separator>,</:separator>
|
||||
|
||||
<:item :for={provider <- @providers}>
|
||||
<.link navigate={"/providers/#{provider.id}"} class={[link_style(), "font-medium"]}>
|
||||
<%= provider.name %>
|
||||
</.link>
|
||||
</:item>
|
||||
</.intersperse_blocks>
|
||||
<span>provider(s)</span>
|
||||
</span>
|
||||
"""
|
||||
end
|
||||
|
||||
defp condition(%{property: :current_utc_datetime, values: values} = assigns) do
|
||||
assigns =
|
||||
assign_new(assigns, :tz_time_ranges_by_dow, fn ->
|
||||
{:ok, ranges} = Policies.Condition.Evaluator.parse_days_of_week_time_ranges(values)
|
||||
|
||||
ranges
|
||||
|> Enum.reject(fn {_dow, time_ranges} -> time_ranges == [] end)
|
||||
|> Enum.map(fn {dow, time_ranges} ->
|
||||
time_ranges_by_timezone =
|
||||
time_ranges
|
||||
|> Enum.reduce(%{}, fn {starts_at, ends_at, timezone}, acc ->
|
||||
range = {starts_at, ends_at}
|
||||
Map.update(acc, timezone, [range], fn ranges -> [range | ranges] end)
|
||||
end)
|
||||
|
||||
{dow, time_ranges_by_timezone}
|
||||
end)
|
||||
|> Enum.sort_by(fn {dow, _time_ranges_by_timezone} -> day_of_week_index(dow) end)
|
||||
end)
|
||||
|
||||
~H"""
|
||||
<span class="flex flex-wrap space-x-1 mr-1">
|
||||
on
|
||||
<.intersperse_blocks>
|
||||
<:separator>,</:separator>
|
||||
|
||||
<:item :for={{day_of_week, tz_time_ranges} <- @tz_time_ranges_by_dow}>
|
||||
<span class="ml-1 font-medium">
|
||||
<%= day_of_week_name(day_of_week) <> "s" %>
|
||||
<span :for={{timezone, time_ranges} <- tz_time_ranges}>
|
||||
<%= "(" <>
|
||||
Enum.map_join(time_ranges, ", ", fn {from, to} ->
|
||||
"#{from} - #{to}"
|
||||
end) <> " #{timezone})" %>
|
||||
</span>
|
||||
</span>
|
||||
</:item>
|
||||
</.intersperse_blocks>
|
||||
</span>
|
||||
"""
|
||||
end
|
||||
|
||||
for {code, name} <- @days_of_week do
|
||||
defp day_of_week_name(unquote(code)), do: unquote(name)
|
||||
end
|
||||
|
||||
for {{code, _name}, index} <- Enum.with_index(@days_of_week) do
|
||||
def day_of_week_index(unquote(code)), do: unquote(index)
|
||||
end
|
||||
|
||||
defp condition_operator_option_name(:contains), do: "contains"
|
||||
defp condition_operator_option_name(:does_not_contain), do: "does not contain"
|
||||
defp condition_operator_option_name(:is_in), do: "is in"
|
||||
defp condition_operator_option_name(:is_not_in), do: "is not in"
|
||||
defp condition_operator_option_name(:is_in_day_of_week_time_ranges), do: ""
|
||||
defp condition_operator_option_name(:is_in_cidr), do: "is in"
|
||||
defp condition_operator_option_name(:is_not_in_cidr), do: "is not in"
|
||||
|
||||
def condition_form(assigns) do
|
||||
assigns =
|
||||
assign_new(assigns, :policy_conditions_enabled?, fn ->
|
||||
Domain.Accounts.policy_conditions_enabled?(assigns.account)
|
||||
end)
|
||||
|
||||
~H"""
|
||||
<fieldset class="flex flex-col gap-2">
|
||||
<div class="flex items-center justify-between">
|
||||
<legend class="text-lg mb-4">Conditions</legend>
|
||||
<%= if @policy_conditions_enabled? == false do %>
|
||||
<.link navigate={~p"/#{@account}/settings/billing"} class="text-sm text-primary-500">
|
||||
<.badge type="primary" title="Feature available on a higher pricing plan">
|
||||
<.icon name="hero-lock-closed" class="w-3.5 h-3.5 mr-1" /> UPGRADE TO UNLOCK
|
||||
</.badge>
|
||||
</.link>
|
||||
<% end %>
|
||||
</div>
|
||||
|
||||
<.remote_ip_location_region_condition_form
|
||||
form={@form}
|
||||
disabled={@policy_conditions_enabled? == false}
|
||||
/>
|
||||
<.remote_ip_condition_form form={@form} disabled={@policy_conditions_enabled? == false} />
|
||||
<.provider_id_condition_form
|
||||
form={@form}
|
||||
providers={@providers}
|
||||
disabled={@policy_conditions_enabled? == false}
|
||||
/>
|
||||
<.current_utc_datetime_condition_form
|
||||
form={@form}
|
||||
timezone={@timezone}
|
||||
disabled={@policy_conditions_enabled? == false}
|
||||
/>
|
||||
</fieldset>
|
||||
"""
|
||||
end
|
||||
|
||||
defp remote_ip_location_region_condition_form(assigns) do
|
||||
~H"""
|
||||
<fieldset class="mb-2">
|
||||
<% condition_form = find_condition_form(@form[:conditions], :remote_ip_location_region) %>
|
||||
|
||||
<.input
|
||||
type="hidden"
|
||||
field={condition_form[:property]}
|
||||
name="policy[conditions][remote_ip_location_region][property]"
|
||||
id="policy_conditions_remote_ip_location_region_property"
|
||||
value="remote_ip_location_region"
|
||||
/>
|
||||
|
||||
<div
|
||||
class="cursor-pointer"
|
||||
phx-click={
|
||||
JS.toggle_class("hidden",
|
||||
to: "#policy_conditions_remote_ip_location_region_condition"
|
||||
)
|
||||
|> JS.toggle_class("hero-chevron-down",
|
||||
to: "#policy_conditions_remote_ip_location_region_chevron"
|
||||
)
|
||||
|> JS.toggle_class("hero-chevron-up",
|
||||
to: "#policy_conditions_remote_ip_location_region_chevron"
|
||||
)
|
||||
}
|
||||
>
|
||||
<legend>
|
||||
<.icon id="policy_conditions_remote_ip_location_region_chevron" name="hero-chevron-down" />
|
||||
Client location
|
||||
</legend>
|
||||
|
||||
<p class="text-sm text-neutral-500 mb-2">
|
||||
Restrict access based on the location of the Client.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div
|
||||
id="policy_conditions_remote_ip_location_region_condition"
|
||||
class={[
|
||||
"grid gap-2 sm:grid-cols-5 sm:gap-4",
|
||||
condition_values_empty?(condition_form) && "hidden"
|
||||
]}
|
||||
>
|
||||
<.input
|
||||
type="select"
|
||||
name="policy[conditions][remote_ip_location_region][operator]"
|
||||
id="policy_conditions_remote_ip_location_region_operator"
|
||||
field={condition_form[:operator]}
|
||||
disabled={@disabled}
|
||||
options={condition_operator_options(:remote_ip_location_region)}
|
||||
value={get_in(condition_form, [:operator, Access.key!(:value)])}
|
||||
/>
|
||||
|
||||
<%= for {value, index} <- Enum.with_index((condition_form[:values] && condition_form[:values].value || []) ++ [nil]) do %>
|
||||
<div :if={index > 0} class="text-right mt-3 text-sm text-neutral-900">
|
||||
or
|
||||
</div>
|
||||
|
||||
<div class="col-span-4">
|
||||
<.input
|
||||
type="select"
|
||||
field={condition_form[:values]}
|
||||
name="policy[conditions][remote_ip_location_region][values][]"
|
||||
id={"policy_conditions_remote_ip_location_region_values_#{index}"}
|
||||
options={[{"Select Country", nil}] ++ Domain.Geo.all_country_options!()}
|
||||
disabled={@disabled}
|
||||
value_index={index}
|
||||
value={value}
|
||||
/>
|
||||
</div>
|
||||
<% end %>
|
||||
</div>
|
||||
</fieldset>
|
||||
"""
|
||||
end
|
||||
|
||||
defp remote_ip_condition_form(assigns) do
|
||||
~H"""
|
||||
<fieldset class="mb-2">
|
||||
<% condition_form = find_condition_form(@form[:conditions], :remote_ip) %>
|
||||
|
||||
<.input
|
||||
type="hidden"
|
||||
field={condition_form[:property]}
|
||||
name="policy[conditions][remote_ip][property]"
|
||||
id="policy_conditions_remote_ip_property"
|
||||
value="remote_ip"
|
||||
/>
|
||||
|
||||
<div
|
||||
class="cursor-pointer"
|
||||
phx-click={
|
||||
JS.toggle_class("hidden",
|
||||
to: "#policy_conditions_remote_ip_condition"
|
||||
)
|
||||
|> JS.toggle_class("hero-chevron-down",
|
||||
to: "#policy_conditions_remote_ip_chevron"
|
||||
)
|
||||
|> JS.toggle_class("hero-chevron-up",
|
||||
to: "#policy_conditions_remote_ip_chevron"
|
||||
)
|
||||
}
|
||||
>
|
||||
<legend>
|
||||
<.icon id="policy_conditions_remote_ip_chevron" name="hero-chevron-down" class="w-5 h-5" />
|
||||
IP address
|
||||
</legend>
|
||||
|
||||
<p class="text-sm text-neutral-500 mb-2">
|
||||
Restrict access based on the Client's IP address or CIDR range.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div
|
||||
id="policy_conditions_remote_ip_condition"
|
||||
class={[
|
||||
"grid gap-2 sm:grid-cols-5 sm:gap-4",
|
||||
condition_values_empty?(condition_form) && "hidden"
|
||||
]}
|
||||
>
|
||||
<.input
|
||||
type="select"
|
||||
name="policy[conditions][remote_ip][operator]"
|
||||
id="policy_conditions_remote_ip_operator"
|
||||
field={condition_form[:operator]}
|
||||
options={condition_operator_options(:remote_ip)}
|
||||
disabled={@disabled}
|
||||
value={get_in(condition_form, [:operator, Access.key!(:value)])}
|
||||
/>
|
||||
|
||||
<%= for {value, index} <- Enum.with_index((condition_form[:values] && condition_form[:values].value || []) ++ [nil]) do %>
|
||||
<div :if={index > 0} class="text-right mt-3 text-sm text-neutral-900">
|
||||
or
|
||||
</div>
|
||||
|
||||
<div class="col-span-4">
|
||||
<.input
|
||||
type="text"
|
||||
field={condition_form[:values]}
|
||||
name="policy[conditions][remote_ip][values][]"
|
||||
id={"policy_conditions_remote_ip_values_#{index}"}
|
||||
placeholder="E.g. 189.172.0.0/24 or 10.10.10.1"
|
||||
disabled={@disabled}
|
||||
value_index={index}
|
||||
value={value}
|
||||
/>
|
||||
</div>
|
||||
<% end %>
|
||||
</div>
|
||||
</fieldset>
|
||||
"""
|
||||
end
|
||||
|
||||
defp provider_id_condition_form(assigns) do
|
||||
~H"""
|
||||
<fieldset class="mb-2">
|
||||
<% condition_form = find_condition_form(@form[:conditions], :provider_id) %>
|
||||
|
||||
<.input
|
||||
type="hidden"
|
||||
field={condition_form[:property]}
|
||||
name="policy[conditions][provider_id][property]"
|
||||
id="policy_conditions_provider_id_property"
|
||||
value="provider_id"
|
||||
/>
|
||||
|
||||
<div
|
||||
class="cursor-pointer"
|
||||
phx-click={
|
||||
JS.toggle_class("hidden",
|
||||
to: "#policy_conditions_provider_id_condition"
|
||||
)
|
||||
|> JS.toggle_class("hero-chevron-down",
|
||||
to: "#policy_conditions_provider_id_chevron"
|
||||
)
|
||||
|> JS.toggle_class("hero-chevron-up",
|
||||
to: "#policy_conditions_provider_id_chevron"
|
||||
)
|
||||
}
|
||||
>
|
||||
<legend>
|
||||
<.icon id="policy_conditions_provider_id_chevron" name="hero-chevron-down" class="w-5 h-5" />
|
||||
Authentication Provider
|
||||
</legend>
|
||||
|
||||
<p class="text-sm text-neutral-500 mb-2">
|
||||
Restrict access based on the identity provider that was used to sign in.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div
|
||||
id="policy_conditions_provider_id_condition"
|
||||
class={[
|
||||
"grid gap-2 sm:grid-cols-5 sm:gap-4",
|
||||
condition_values_empty?(condition_form) && "hidden"
|
||||
]}
|
||||
>
|
||||
<.input
|
||||
type="select"
|
||||
name="policy[conditions][provider_id][operator]"
|
||||
id="policy_conditions_provider_id_operator"
|
||||
field={condition_form[:operator]}
|
||||
options={condition_operator_options(:provider_id)}
|
||||
disabled={@disabled}
|
||||
value={get_in(condition_form, [:operator, Access.key!(:value)])}
|
||||
/>
|
||||
|
||||
<%= for {value, index} <- Enum.with_index((condition_form[:values] && condition_form[:values].value || []) ++ [nil]) do %>
|
||||
<div :if={index > 0} class="text-right mt-3 text-sm text-neutral-900">
|
||||
or
|
||||
</div>
|
||||
|
||||
<div class="col-span-4">
|
||||
<.input
|
||||
type="select"
|
||||
field={condition_form[:values]}
|
||||
name="policy[conditions][provider_id][values][]"
|
||||
id={"policy_conditions_provider_id_values_#{index}"}
|
||||
options={[{"Select Provider", nil}] ++ Enum.map(@providers, &{&1.name, &1.id})}
|
||||
disabled={@disabled}
|
||||
value_index={index}
|
||||
value={value}
|
||||
/>
|
||||
</div>
|
||||
<% end %>
|
||||
</div>
|
||||
</fieldset>
|
||||
"""
|
||||
end
|
||||
|
||||
defp current_utc_datetime_condition_form(assigns) do
|
||||
assigns = assign_new(assigns, :days_of_week, fn -> @days_of_week end)
|
||||
|
||||
~H"""
|
||||
<fieldset>
|
||||
<% condition_form = find_condition_form(@form[:conditions], :current_utc_datetime) %>
|
||||
|
||||
<.input
|
||||
type="hidden"
|
||||
field={condition_form[:property]}
|
||||
name="policy[conditions][current_utc_datetime][property]"
|
||||
id="policy_conditions_current_utc_datetime_property"
|
||||
value="current_utc_datetime"
|
||||
/>
|
||||
|
||||
<.input
|
||||
type="hidden"
|
||||
name="policy[conditions][current_utc_datetime][operator]"
|
||||
id="policy_conditions_current_utc_datetime_operator"
|
||||
field={condition_form[:operator]}
|
||||
value={:is_in_day_of_week_time_ranges}
|
||||
/>
|
||||
|
||||
<div
|
||||
class="cursor-pointer"
|
||||
phx-click={
|
||||
JS.toggle_class("hidden",
|
||||
to: "#policy_conditions_current_utc_datetime_condition"
|
||||
)
|
||||
|> JS.toggle_class("hero-chevron-down",
|
||||
to: "#policy_conditions_current_utc_datetime_chevron"
|
||||
)
|
||||
|> JS.toggle_class("hero-chevron-up",
|
||||
to: "#policy_conditions_current_utc_datetime_chevron"
|
||||
)
|
||||
}
|
||||
>
|
||||
<legend>
|
||||
<.icon
|
||||
id="policy_conditions_current_utc_datetime_chevron"
|
||||
name="hero-chevron-down"
|
||||
class="w-5 h-5"
|
||||
/> Current time
|
||||
</legend>
|
||||
|
||||
<p class="text-sm text-neutral-500 mb-2">
|
||||
Restrict access based on the current time of the day in 24hr format. Multiple time ranges per day are supported.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div
|
||||
id="policy_conditions_current_utc_datetime_condition"
|
||||
class={[
|
||||
"space-y-2",
|
||||
condition_values_empty?(condition_form) && "hidden"
|
||||
]}
|
||||
>
|
||||
<.input
|
||||
type="select"
|
||||
label="Timezone"
|
||||
name="policy[conditions][current_utc_datetime][timezone]"
|
||||
id="policy_conditions_current_utc_datetime_timezone"
|
||||
field={condition_form[:timezone]}
|
||||
options={Tzdata.zone_list()}
|
||||
disabled={@disabled}
|
||||
value={condition_form[:timezone].value || @timezone}
|
||||
/>
|
||||
|
||||
<div class="space-y-2">
|
||||
<.current_utc_datetime_condition_day_input
|
||||
:for={{code, _name} <- @days_of_week}
|
||||
disabled={@disabled}
|
||||
condition_form={condition_form}
|
||||
day={code}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</fieldset>
|
||||
"""
|
||||
end
|
||||
|
||||
defp find_condition_form(form_field, property) do
|
||||
condition_form =
|
||||
form_field.value
|
||||
|> Enum.find_value(fn condition ->
|
||||
if Ecto.Changeset.get_field(condition, :property) == property do
|
||||
to_form(condition)
|
||||
end
|
||||
end)
|
||||
|
||||
condition_form || to_form(%{})
|
||||
end
|
||||
|
||||
defp current_utc_datetime_condition_day_input(assigns) do
|
||||
~H"""
|
||||
<.input
|
||||
type="text"
|
||||
label={day_of_week_name(@day)}
|
||||
field={@condition_form[:values]}
|
||||
name={"policy[conditions][current_utc_datetime][values][#{@day}]"}
|
||||
id={"policy_conditions_current_utc_datetime_values_#{@day}"}
|
||||
placeholder="E.g. 9:00-12:00, 13:00-17:00"
|
||||
value={get_datetime_range_for_day_of_week(@day, @condition_form[:values])}
|
||||
disabled={@disabled}
|
||||
value_index={day_of_week_index(@day)}
|
||||
/>
|
||||
"""
|
||||
end
|
||||
|
||||
defp get_datetime_range_for_day_of_week(day, form_field) do
|
||||
Enum.find_value(form_field.value || [], fn dow_time_ranges ->
|
||||
case String.split(dow_time_ranges, "/", parts: 3) do
|
||||
[^day, ranges, _timezone] -> ranges
|
||||
_other -> false
|
||||
end
|
||||
end)
|
||||
end
|
||||
|
||||
defp condition_operator_options(property) do
|
||||
Domain.Policies.Condition.Changeset.valid_operators_for_property(property)
|
||||
|> Enum.map(&{condition_operator_option_name(&1), &1})
|
||||
end
|
||||
end
|
||||
|
||||
@@ -1,22 +1,26 @@
|
||||
defmodule Web.Policies.New do
|
||||
use Web, :live_view
|
||||
alias Domain.{Resources, Actors, Policies}
|
||||
import Web.Policies.Components
|
||||
alias Domain.{Resources, Actors, Policies, Auth}
|
||||
|
||||
def mount(params, _session, socket) do
|
||||
# TODO: unify this dropdown and the one we use for live table filters
|
||||
resources = Resources.all_resources!(socket.assigns.subject, preload: [:gateway_groups])
|
||||
# TODO: unify this dropdown and the one we use for live table filters
|
||||
actor_groups = Actors.all_groups!(socket.assigns.subject, preload: :provider)
|
||||
providers = Auth.all_active_providers_for_account!(socket.assigns.account)
|
||||
form = to_form(Policies.new_policy(%{}, socket.assigns.subject))
|
||||
|
||||
socket =
|
||||
assign(socket,
|
||||
resources: resources,
|
||||
actor_groups: actor_groups,
|
||||
providers: providers,
|
||||
params: Map.take(params, ["site_id"]),
|
||||
resource_id: params["resource_id"],
|
||||
actor_group_id: params["actor_group_id"],
|
||||
page_title: "New Policy",
|
||||
timezone: Map.get(socket.private.connect_params, "timezone", "UTC"),
|
||||
form: form
|
||||
)
|
||||
|
||||
@@ -35,7 +39,7 @@ defmodule Web.Policies.New do
|
||||
</:title>
|
||||
<:content>
|
||||
<div class="max-w-2xl px-4 py-8 mx-auto lg:py-16">
|
||||
<h2 class="mb-4 text-xl text-neutral-900">Policy details</h2>
|
||||
<h2 class="mb-4 text-xl text-neutral-900">Define a Policy</h2>
|
||||
<div
|
||||
:if={@actor_groups == []}
|
||||
class={[
|
||||
@@ -56,40 +60,50 @@ defmodule Web.Policies.New do
|
||||
<.form :if={@actor_groups != []} for={@form} phx-submit="submit" phx-change="validate">
|
||||
<.base_error form={@form} field={:base} />
|
||||
<div class="grid gap-4 mb-4 sm:grid-cols-1 sm:gap-6 sm:mb-6">
|
||||
<.input
|
||||
field={@form[:actor_group_id]}
|
||||
label="Group"
|
||||
type="group_select"
|
||||
options={Web.Groups.Components.select_options(@actor_groups)}
|
||||
value={@actor_group_id || @form[:actor_group_id].value}
|
||||
disabled={not is_nil(@actor_group_id)}
|
||||
required
|
||||
/>
|
||||
<fieldset class="flex flex-col gap-2">
|
||||
<.input
|
||||
field={@form[:actor_group_id]}
|
||||
label="Group"
|
||||
type="group_select"
|
||||
options={Web.Groups.Components.select_options(@actor_groups)}
|
||||
value={@actor_group_id || @form[:actor_group_id].value}
|
||||
disabled={not is_nil(@actor_group_id)}
|
||||
required
|
||||
/>
|
||||
|
||||
<.input
|
||||
field={@form[:resource_id]}
|
||||
label="Resource"
|
||||
type="select"
|
||||
options={
|
||||
Enum.map(@resources, fn resource ->
|
||||
group_names = resource.gateway_groups |> Enum.map(& &1.name)
|
||||
<.input
|
||||
field={@form[:resource_id]}
|
||||
label="Resource"
|
||||
type="select"
|
||||
options={
|
||||
Enum.map(@resources, fn resource ->
|
||||
group_names = resource.gateway_groups |> Enum.map(& &1.name)
|
||||
|
||||
[
|
||||
key: "#{resource.name} - #{Enum.join(group_names, ",")}",
|
||||
value: resource.id
|
||||
]
|
||||
end)
|
||||
}
|
||||
value={@resource_id || @form[:resource_id].value}
|
||||
disabled={not is_nil(@resource_id)}
|
||||
required
|
||||
/>
|
||||
<.input
|
||||
field={@form[:description]}
|
||||
type="textarea"
|
||||
label="Description"
|
||||
placeholder="Enter a reason for creating a policy here"
|
||||
phx-debounce="300"
|
||||
[
|
||||
key: "#{resource.name} - #{Enum.join(group_names, ",")}",
|
||||
value: resource.id
|
||||
]
|
||||
end)
|
||||
}
|
||||
value={@resource_id || @form[:resource_id].value}
|
||||
disabled={not is_nil(@resource_id)}
|
||||
required
|
||||
/>
|
||||
|
||||
<.input
|
||||
field={@form[:description]}
|
||||
label="Description"
|
||||
type="textarea"
|
||||
placeholder="Optionally, enter a reason for creating a policy here."
|
||||
phx-debounce="300"
|
||||
/>
|
||||
</fieldset>
|
||||
|
||||
<.condition_form
|
||||
form={@form}
|
||||
account={@account}
|
||||
timezone={@timezone}
|
||||
providers={@providers}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -103,21 +117,24 @@ defmodule Web.Policies.New do
|
||||
"""
|
||||
end
|
||||
|
||||
def handle_event("validate", %{"policy" => policy_params}, socket) do
|
||||
def handle_event("validate", %{"policy" => params}, socket) do
|
||||
form =
|
||||
policy_params
|
||||
|> put_default_policy_params(socket)
|
||||
params
|
||||
|> put_default_params(socket)
|
||||
|> map_condition_params(empty_values: :keep)
|
||||
|> Policies.new_policy(socket.assigns.subject)
|
||||
|> Map.put(:action, :validate)
|
||||
|> to_form()
|
||||
|> to_form(action: :validate)
|
||||
|
||||
{:noreply, assign(socket, form: form)}
|
||||
end
|
||||
|
||||
def handle_event("submit", %{"policy" => policy_params}, socket) do
|
||||
policy_params = put_default_policy_params(policy_params, socket)
|
||||
def handle_event("submit", %{"policy" => params}, socket) do
|
||||
params =
|
||||
params
|
||||
|> put_default_params(socket)
|
||||
|> map_condition_params(empty_values: :drop)
|
||||
|
||||
with {:ok, policy} <- Policies.create_policy(policy_params, socket.assigns.subject) do
|
||||
with {:ok, policy} <- Policies.create_policy(params, socket.assigns.subject) do
|
||||
if site_id = socket.assigns.params["site_id"] do
|
||||
{:noreply,
|
||||
push_navigate(socket, to: ~p"/#{socket.assigns.account}/sites/#{site_id}?#resources")}
|
||||
@@ -126,12 +143,11 @@ defmodule Web.Policies.New do
|
||||
end
|
||||
else
|
||||
{:error, %Ecto.Changeset{} = changeset} ->
|
||||
form = to_form(changeset)
|
||||
{:noreply, assign(socket, form: form)}
|
||||
{:noreply, assign(socket, form: to_form(changeset, action: :insert))}
|
||||
end
|
||||
end
|
||||
|
||||
defp put_default_policy_params(attrs, socket) do
|
||||
defp put_default_params(attrs, socket) do
|
||||
if resource_id = socket.assigns.resource_id do
|
||||
Map.put(attrs, "resource_id", resource_id)
|
||||
else
|
||||
|
||||
@@ -1,13 +1,15 @@
|
||||
defmodule Web.Policies.Show do
|
||||
use Web, :live_view
|
||||
import Web.Policies.Components
|
||||
alias Domain.{Accounts, Policies, Flows}
|
||||
alias Domain.{Accounts, Policies, Flows, Auth}
|
||||
|
||||
def mount(%{"id" => id}, _session, socket) do
|
||||
with {:ok, policy} <-
|
||||
Policies.fetch_policy_by_id(id, socket.assigns.subject,
|
||||
preload: [actor_group: [:provider], resource: [], created_by_identity: :actor]
|
||||
) do
|
||||
providers = Auth.all_active_providers_for_account!(socket.assigns.account)
|
||||
|
||||
if connected?(socket) do
|
||||
:ok = Policies.subscribe_to_events_for_policy(policy)
|
||||
end
|
||||
@@ -16,6 +18,7 @@ defmodule Web.Policies.Show do
|
||||
assign(socket,
|
||||
page_title: "Policy #{policy.id}",
|
||||
policy: policy,
|
||||
providers: providers,
|
||||
flow_activities_enabled?: Accounts.flow_activities_enabled?(socket.assigns.account)
|
||||
)
|
||||
|> assign_live_table("flows",
|
||||
@@ -126,7 +129,15 @@ defmodule Web.Policies.Show do
|
||||
</span>
|
||||
</:value>
|
||||
</.vertical_table_row>
|
||||
<.vertical_table_row>
|
||||
<.vertical_table_row :if={@policy.conditions != []}>
|
||||
<:label>
|
||||
Conditions
|
||||
</:label>
|
||||
<:value>
|
||||
<.conditions providers={@providers} conditions={@policy.conditions} />
|
||||
</:value>
|
||||
</.vertical_table_row>
|
||||
<.vertical_table_row :if={@policy.description}>
|
||||
<:label>
|
||||
Description
|
||||
</:label>
|
||||
@@ -153,11 +164,9 @@ defmodule Web.Policies.Show do
|
||||
</.section>
|
||||
|
||||
<.section>
|
||||
<:title>
|
||||
Activity
|
||||
</:title>
|
||||
<:title>Authorized Activity</:title>
|
||||
<:help>
|
||||
Attempts by actors to access the resource governed by this policy.
|
||||
Authorized attempts by actors to access the resource governed by this policy.
|
||||
</:help>
|
||||
<:content>
|
||||
<.live_table
|
||||
|
||||
@@ -251,11 +251,9 @@ defmodule Web.Resources.Show do
|
||||
</.section>
|
||||
|
||||
<.section>
|
||||
<:title>
|
||||
Activity
|
||||
</:title>
|
||||
<:title>Authorized Activity</:title>
|
||||
<:help>
|
||||
Attempts by actors to access this resource.
|
||||
Authorized attempts by actors to access the resource governed by this policy.
|
||||
</:help>
|
||||
<:content>
|
||||
<.live_table
|
||||
|
||||
@@ -49,6 +49,8 @@ defmodule Web.MixProject do
|
||||
# CLDR and unit conversions
|
||||
{:ex_cldr_dates_times, "~> 2.13"},
|
||||
{:ex_cldr_numbers, "~> 2.31"},
|
||||
{:ex_cldr, "~> 2.38"},
|
||||
{:tzdata, "~> 1.1"},
|
||||
{:sizeable, "~> 1.0"},
|
||||
|
||||
# Asset pipeline deps
|
||||
|
||||
@@ -60,6 +60,25 @@ defmodule Web.Live.Policies.NewTest do
|
||||
|
||||
assert find_inputs(form) == [
|
||||
"policy[actor_group_id]",
|
||||
"policy[conditions][current_utc_datetime][operator]",
|
||||
"policy[conditions][current_utc_datetime][property]",
|
||||
"policy[conditions][current_utc_datetime][timezone]",
|
||||
"policy[conditions][current_utc_datetime][values][F]",
|
||||
"policy[conditions][current_utc_datetime][values][M]",
|
||||
"policy[conditions][current_utc_datetime][values][R]",
|
||||
"policy[conditions][current_utc_datetime][values][S]",
|
||||
"policy[conditions][current_utc_datetime][values][T]",
|
||||
"policy[conditions][current_utc_datetime][values][U]",
|
||||
"policy[conditions][current_utc_datetime][values][W]",
|
||||
"policy[conditions][provider_id][operator]",
|
||||
"policy[conditions][provider_id][property]",
|
||||
"policy[conditions][provider_id][values][]",
|
||||
"policy[conditions][remote_ip][operator]",
|
||||
"policy[conditions][remote_ip][property]",
|
||||
"policy[conditions][remote_ip][values][]",
|
||||
"policy[conditions][remote_ip_location_region][operator]",
|
||||
"policy[conditions][remote_ip_location_region][property]",
|
||||
"policy[conditions][remote_ip_location_region][values][]",
|
||||
"policy[description]",
|
||||
"policy[resource_id]"
|
||||
]
|
||||
@@ -80,6 +99,25 @@ defmodule Web.Live.Policies.NewTest do
|
||||
|
||||
assert find_inputs(form) == [
|
||||
"policy[actor_group_id]",
|
||||
"policy[conditions][current_utc_datetime][operator]",
|
||||
"policy[conditions][current_utc_datetime][property]",
|
||||
"policy[conditions][current_utc_datetime][timezone]",
|
||||
"policy[conditions][current_utc_datetime][values][F]",
|
||||
"policy[conditions][current_utc_datetime][values][M]",
|
||||
"policy[conditions][current_utc_datetime][values][R]",
|
||||
"policy[conditions][current_utc_datetime][values][S]",
|
||||
"policy[conditions][current_utc_datetime][values][T]",
|
||||
"policy[conditions][current_utc_datetime][values][U]",
|
||||
"policy[conditions][current_utc_datetime][values][W]",
|
||||
"policy[conditions][provider_id][operator]",
|
||||
"policy[conditions][provider_id][property]",
|
||||
"policy[conditions][provider_id][values][]",
|
||||
"policy[conditions][remote_ip][operator]",
|
||||
"policy[conditions][remote_ip][property]",
|
||||
"policy[conditions][remote_ip][values][]",
|
||||
"policy[conditions][remote_ip_location_region][operator]",
|
||||
"policy[conditions][remote_ip_location_region][property]",
|
||||
"policy[conditions][remote_ip_location_region][values][]",
|
||||
"policy[description]",
|
||||
"policy[resource_id]"
|
||||
]
|
||||
@@ -111,6 +149,25 @@ defmodule Web.Live.Policies.NewTest do
|
||||
|
||||
assert find_inputs(form) == [
|
||||
"policy[actor_group_id]",
|
||||
"policy[conditions][current_utc_datetime][operator]",
|
||||
"policy[conditions][current_utc_datetime][property]",
|
||||
"policy[conditions][current_utc_datetime][timezone]",
|
||||
"policy[conditions][current_utc_datetime][values][F]",
|
||||
"policy[conditions][current_utc_datetime][values][M]",
|
||||
"policy[conditions][current_utc_datetime][values][R]",
|
||||
"policy[conditions][current_utc_datetime][values][S]",
|
||||
"policy[conditions][current_utc_datetime][values][T]",
|
||||
"policy[conditions][current_utc_datetime][values][U]",
|
||||
"policy[conditions][current_utc_datetime][values][W]",
|
||||
"policy[conditions][provider_id][operator]",
|
||||
"policy[conditions][provider_id][property]",
|
||||
"policy[conditions][provider_id][values][]",
|
||||
"policy[conditions][remote_ip][operator]",
|
||||
"policy[conditions][remote_ip][property]",
|
||||
"policy[conditions][remote_ip][values][]",
|
||||
"policy[conditions][remote_ip_location_region][operator]",
|
||||
"policy[conditions][remote_ip_location_region][property]",
|
||||
"policy[conditions][remote_ip_location_region][values][]",
|
||||
"policy[description]",
|
||||
"policy[resource_id]"
|
||||
]
|
||||
@@ -135,8 +192,7 @@ defmodule Web.Live.Policies.NewTest do
|
||||
resource = Fixtures.Resources.create_resource(account: account)
|
||||
|
||||
attrs =
|
||||
Fixtures.Policies.policy_attrs()
|
||||
|> Map.take([:name])
|
||||
%{}
|
||||
|> Map.put(:actor_group_id, group.id)
|
||||
|> Map.put(:resource_id, resource.id)
|
||||
|
||||
@@ -196,11 +252,10 @@ defmodule Web.Live.Policies.NewTest do
|
||||
group = Fixtures.Actors.create_group(account: account)
|
||||
resource = Fixtures.Resources.create_resource(account: account)
|
||||
|
||||
attrs =
|
||||
Fixtures.Policies.policy_attrs()
|
||||
|> Map.take([:name])
|
||||
|> Map.put(:actor_group_id, group.id)
|
||||
|> Map.put(:resource_id, resource.id)
|
||||
attrs = %{
|
||||
actor_group_id: group.id,
|
||||
resource_id: resource.id
|
||||
}
|
||||
|
||||
{:ok, lv, _html} =
|
||||
conn
|
||||
@@ -211,7 +266,97 @@ defmodule Web.Live.Policies.NewTest do
|
||||
|> form("form", policy: attrs)
|
||||
|> render_submit()
|
||||
|
||||
policy = Repo.get_by(Domain.Policies.Policy, attrs)
|
||||
assert policy = Repo.get_by(Domain.Policies.Policy, attrs)
|
||||
|
||||
assert assert_redirect(lv, ~p"/#{account}/policies/#{policy}")
|
||||
end
|
||||
|
||||
test "creates a new policy with conditions", %{
|
||||
account: account,
|
||||
identity: identity,
|
||||
conn: conn
|
||||
} do
|
||||
group = Fixtures.Actors.create_group(account: account)
|
||||
resource = Fixtures.Resources.create_resource(account: account)
|
||||
|
||||
attrs = %{
|
||||
actor_group_id: group.id,
|
||||
resource_id: resource.id,
|
||||
conditions: %{
|
||||
current_utc_datetime: %{
|
||||
property: "current_utc_datetime",
|
||||
operator: "is_in_day_of_week_time_ranges",
|
||||
timezone: "US/Pacific",
|
||||
values: %{
|
||||
M: "true",
|
||||
T: "",
|
||||
W: "true",
|
||||
R: "",
|
||||
F: "",
|
||||
S: "10:00:00-15:00:00",
|
||||
U: "23:00:00-23:59:59"
|
||||
}
|
||||
},
|
||||
provider_id: %{
|
||||
property: "provider_id",
|
||||
operator: "is_in",
|
||||
values: [identity.provider_id]
|
||||
},
|
||||
remote_ip: %{
|
||||
property: "remote_ip",
|
||||
operator: "is_not_in_cidr",
|
||||
values: ["0.0.0.0/0"]
|
||||
},
|
||||
remote_ip_location_region: %{
|
||||
property: "remote_ip_location_region",
|
||||
operator: "is_in",
|
||||
values: ["US"]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
{:ok, lv, _html} =
|
||||
conn
|
||||
|> authorize_conn(identity)
|
||||
|> live(~p"/#{account}/policies/new?resource_id=#{resource}")
|
||||
|
||||
assert lv
|
||||
|> form("form", policy: attrs)
|
||||
|> render_submit()
|
||||
|
||||
policy = Repo.get_by(Domain.Policies.Policy, actor_group_id: group.id)
|
||||
assert policy.resource_id == resource.id
|
||||
|
||||
assert policy.conditions == [
|
||||
%Domain.Policies.Condition{
|
||||
property: :current_utc_datetime,
|
||||
operator: :is_in_day_of_week_time_ranges,
|
||||
values: [
|
||||
"M/true/US/Pacific",
|
||||
"T//US/Pacific",
|
||||
"W/true/US/Pacific",
|
||||
"R//US/Pacific",
|
||||
"F//US/Pacific",
|
||||
"S/10:00:00-15:00:00/US/Pacific",
|
||||
"U/23:00:00-23:59:59/US/Pacific"
|
||||
]
|
||||
},
|
||||
%Domain.Policies.Condition{
|
||||
property: :provider_id,
|
||||
operator: :is_in,
|
||||
values: [identity.provider_id]
|
||||
},
|
||||
%Domain.Policies.Condition{
|
||||
property: :remote_ip,
|
||||
operator: :is_not_in_cidr,
|
||||
values: ["0.0.0.0/0"]
|
||||
},
|
||||
%Domain.Policies.Condition{
|
||||
property: :remote_ip_location_region,
|
||||
operator: :is_in,
|
||||
values: ["US"]
|
||||
}
|
||||
]
|
||||
|
||||
assert assert_redirect(lv, ~p"/#{account}/policies/#{policy}")
|
||||
end
|
||||
@@ -225,9 +370,7 @@ defmodule Web.Live.Policies.NewTest do
|
||||
resource = Fixtures.Resources.create_resource(account: account)
|
||||
|
||||
attrs =
|
||||
Fixtures.Policies.policy_attrs()
|
||||
|> Map.take([:name])
|
||||
|> Map.put(:actor_group_id, group.id)
|
||||
%{actor_group_id: group.id}
|
||||
|
||||
{:ok, lv, _html} =
|
||||
conn
|
||||
@@ -254,10 +397,7 @@ defmodule Web.Live.Policies.NewTest do
|
||||
|
||||
gateway_group = Fixtures.Gateways.create_group(account: account)
|
||||
|
||||
attrs =
|
||||
Fixtures.Policies.policy_attrs()
|
||||
|> Map.take([:name])
|
||||
|> Map.put(:actor_group_id, group.id)
|
||||
attrs = %{actor_group_id: group.id}
|
||||
|
||||
{:ok, lv, _html} =
|
||||
conn
|
||||
|
||||
@@ -14,6 +14,36 @@ defmodule Web.Live.Policies.ShowTest do
|
||||
account: account,
|
||||
subject: subject,
|
||||
resource: resource,
|
||||
conditions: [
|
||||
%{
|
||||
property: :current_utc_datetime,
|
||||
operator: :is_in_day_of_week_time_ranges,
|
||||
values: [
|
||||
"M/true/UTC",
|
||||
"T//UTC",
|
||||
"W/true/UTC",
|
||||
"R//UTC",
|
||||
"F//UTC",
|
||||
"S/10:00:00-15:00:00/UTC",
|
||||
"U/23:00:00-23:59:59/UTC"
|
||||
]
|
||||
},
|
||||
%{
|
||||
property: :provider_id,
|
||||
operator: :is_in,
|
||||
values: [identity.provider_id]
|
||||
},
|
||||
%{
|
||||
property: :remote_ip,
|
||||
operator: :is_not_in_cidr,
|
||||
values: ["0.0.0.0/0"]
|
||||
},
|
||||
%{
|
||||
property: :remote_ip_location_region,
|
||||
operator: :is_in,
|
||||
values: ["US"]
|
||||
}
|
||||
],
|
||||
description: "Test Policy"
|
||||
)
|
||||
|
||||
@@ -125,6 +155,19 @@ defmodule Web.Live.Policies.ShowTest do
|
||||
assert table["resource"] =~ policy.resource.name
|
||||
assert table["description"] =~ policy.description
|
||||
assert table["created"] =~ actor.name
|
||||
|
||||
assert table["conditions"] =~ "This policy can be used on"
|
||||
assert table["conditions"] =~ "Mondays"
|
||||
assert table["conditions"] =~ "Wednesdays"
|
||||
assert table["conditions"] =~ "Saturdays (10:00:00 - 15:00:00 UTC)"
|
||||
assert table["conditions"] =~ "Sundays (23:00:00 - 23:59:59 UTC)"
|
||||
assert table["conditions"] =~ "from United States of America"
|
||||
assert table["conditions"] =~ "from IP addresses that are"
|
||||
assert table["conditions"] =~ "not in"
|
||||
assert table["conditions"] =~ "0.0.0.0"
|
||||
assert table["conditions"] =~ "when signed in"
|
||||
assert table["conditions"] =~ "with"
|
||||
assert table["conditions"] =~ "provider(s)"
|
||||
end
|
||||
|
||||
test "renders logs table", %{
|
||||
|
||||
@@ -88,6 +88,7 @@ config :domain, :enabled_features,
|
||||
sign_up: true,
|
||||
flow_activities: true,
|
||||
self_hosted_relays: true,
|
||||
policy_conditions: true,
|
||||
multi_site_resources: true,
|
||||
rest_api: true
|
||||
|
||||
|
||||
@@ -62,6 +62,7 @@ if config_env() == :prod do
|
||||
sign_up: compile_config!(:feature_sign_up_enabled),
|
||||
flow_activities: compile_config!(:feature_flow_activities_enabled),
|
||||
self_hosted_relays: compile_config!(:feature_self_hosted_relays_enabled),
|
||||
policy_conditions: compile_config!(:feature_policy_conditions_enabled),
|
||||
multi_site_resources: compile_config!(:feature_multi_site_resources_enabled),
|
||||
rest_api: compile_config!(:feature_rest_api_enabled)
|
||||
|
||||
|
||||
@@ -103,6 +103,7 @@
|
||||
"tesla": {:hex, :tesla, "1.9.0", "8c22db6a826e56a087eeb8cdef56889731287f53feeb3f361dec5d4c8efb6f14", [:mix], [{:castore, "~> 0.1 or ~> 1.0", [hex: :castore, repo: "hexpm", optional: true]}, {:exjsx, ">= 3.0.0", [hex: :exjsx, repo: "hexpm", optional: true]}, {:finch, "~> 0.13", [hex: :finch, repo: "hexpm", optional: true]}, {:fuse, "~> 2.4", [hex: :fuse, repo: "hexpm", optional: true]}, {:gun, ">= 1.0.0", [hex: :gun, repo: "hexpm", optional: true]}, {:hackney, "~> 1.6", [hex: :hackney, repo: "hexpm", optional: true]}, {:ibrowse, "4.4.2", [hex: :ibrowse, repo: "hexpm", optional: true]}, {:jason, ">= 1.0.0", [hex: :jason, repo: "hexpm", optional: true]}, {:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:mint, "~> 1.0", [hex: :mint, repo: "hexpm", optional: true]}, {:msgpax, "~> 2.3", [hex: :msgpax, repo: "hexpm", optional: true]}, {:poison, ">= 1.0.0", [hex: :poison, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: true]}], "hexpm", "7c240c67e855f7e63e795bf16d6b3f5115a81d1f44b7fe4eadbf656bae0fef8a"},
|
||||
"thousand_island": {:hex, :thousand_island, "1.3.5", "6022b6338f1635b3d32406ff98d68b843ba73b3aa95cfc27154223244f3a6ca5", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "2be6954916fdfe4756af3239fb6b6d75d0b8063b5df03ba76fd8a4c87849e180"},
|
||||
"tls_certificate_check": {:hex, :tls_certificate_check, "1.22.1", "0f450cc1568a67a65ce5e15df53c53f9a098c3da081c5f126199a72505858dc1", [:rebar3], [{:ssl_verify_fun, "~> 1.1", [hex: :ssl_verify_fun, repo: "hexpm", optional: false]}], "hexpm", "3092be0babdc0e14c2e900542351e066c0fa5a9cf4b3597559ad1e67f07938c0"},
|
||||
"tzdata": {:hex, :tzdata, "1.1.1", "20c8043476dfda8504952d00adac41c6eda23912278add38edc140ae0c5bcc46", [:mix], [{:hackney, "~> 1.17", [hex: :hackney, repo: "hexpm", optional: false]}], "hexpm", "a69cec8352eafcd2e198dea28a34113b60fdc6cb57eb5ad65c10292a6ba89787"},
|
||||
"unicode_util_compat": {:hex, :unicode_util_compat, "0.7.0", "bc84380c9ab48177092f43ac89e4dfa2c6d62b40b8bd132b1059ecc7232f9a78", [:rebar3], [], "hexpm", "25eee6d67df61960cf6a794239566599b09e17e668d3700247bc498638152521"},
|
||||
"wallaby": {:hex, :wallaby, "0.30.6", "7dc4c1213f3b52c4152581d126632bc7e06892336d3a0f582853efeeabd45a71", [:mix], [{:ecto_sql, ">= 3.0.0", [hex: :ecto_sql, repo: "hexpm", optional: true]}, {:httpoison, "~> 0.12 or ~> 1.0 or ~> 2.0", [hex: :httpoison, repo: "hexpm", optional: false]}, {:jason, "~> 1.1", [hex: :jason, repo: "hexpm", optional: false]}, {:phoenix_ecto, ">= 3.0.0", [hex: :phoenix_ecto, repo: "hexpm", optional: true]}, {:web_driver_client, "~> 0.2.0", [hex: :web_driver_client, repo: "hexpm", optional: false]}], "hexpm", "50950c1d968549b54c20e16175c68c7fc0824138e2bb93feb11ef6add8eb23d4"},
|
||||
"web_driver_client": {:hex, :web_driver_client, "0.2.0", "63b76cd9eb3b0716ec5467a0f8bead73d3d9612e63f7560d21357f03ad86e31a", [:mix], [{:hackney, "~> 1.6", [hex: :hackney, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}, {:tesla, "~> 1.3", [hex: :tesla, repo: "hexpm", optional: false]}], "hexpm", "83cc6092bc3e74926d1c8455f0ce927d5d1d36707b74d9a65e38c084aab0350f"},
|
||||
|
||||
@@ -358,6 +358,10 @@ locals {
|
||||
name = "FEATURE_SELF_HOSTED_RELAYS_ENABLED"
|
||||
value = true
|
||||
},
|
||||
{
|
||||
name = "FEATURE_POLICY_CONDITIONS_ENABLED"
|
||||
value = true
|
||||
},
|
||||
{
|
||||
name = "FEATURE_MULTI_SITE_RESOURCES_ENABLED"
|
||||
value = true
|
||||
|
||||
@@ -327,6 +327,10 @@ locals {
|
||||
name = "FEATURE_SELF_HOSTED_RELAYS_ENABLED"
|
||||
value = true
|
||||
},
|
||||
{
|
||||
name = "FEATURE_POLICY_CONDITIONS_ENABLED"
|
||||
value = true
|
||||
},
|
||||
{
|
||||
name = "FEATURE_MULTI_SITE_RESOURCES_ENABLED"
|
||||
value = true
|
||||
|
||||
Reference in New Issue
Block a user