mirror of
https://github.com/outbackdingo/firezone.git
synced 2026-01-27 18:18:55 +00:00
Fix user-reported errors (#2954)
This commit is contained in:
@@ -142,11 +142,9 @@ defmodule Domain.Auth do
|
||||
end
|
||||
|
||||
def list_providers_pending_sync_by_adapter(adapter) do
|
||||
datetime_filter = DateTime.utc_now() |> DateTime.add(-10, :minute)
|
||||
|
||||
Provider.Query.by_adapter(adapter)
|
||||
|> Provider.Query.by_provisioner(:custom)
|
||||
|> Provider.Query.last_synced_at({:lt, datetime_filter})
|
||||
|> Provider.Query.only_ready_to_be_synced()
|
||||
|> Provider.Query.not_disabled()
|
||||
|> Repo.list()
|
||||
end
|
||||
|
||||
@@ -126,10 +126,23 @@ defmodule Domain.Auth.Adapters.GoogleWorkspace.APIClient do
|
||||
{:ok, list} <- Map.fetch(json_response, key) do
|
||||
{:ok, list, json_response["nextPageToken"]}
|
||||
else
|
||||
{:ok, %Finch.Response{status: status}} when status in 500..599 -> {:error, :retry_later}
|
||||
{:ok, %Finch.Response{body: response, status: status}} -> {:error, {status, response}}
|
||||
:error -> {:ok, [], nil}
|
||||
other -> other
|
||||
{:ok, %Finch.Response{status: status}} when status in 500..599 ->
|
||||
{:error, :retry_later}
|
||||
|
||||
{:ok, %Finch.Response{body: response, status: status}} ->
|
||||
case Jason.decode(response) do
|
||||
{:ok, json_response} ->
|
||||
{:error, {status, json_response}}
|
||||
|
||||
_error ->
|
||||
{:error, {status, response}}
|
||||
end
|
||||
|
||||
:error ->
|
||||
{:ok, [], nil}
|
||||
|
||||
other ->
|
||||
other
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -100,7 +100,7 @@ defmodule Domain.Auth.Adapters.GoogleWorkspace.Jobs do
|
||||
|> Ecto.Multi.append(Actors.sync_provider_groups_multi(provider, actor_groups_attrs))
|
||||
|> Actors.sync_provider_memberships_multi(provider, tuples)
|
||||
|> Ecto.Multi.update(:save_last_updated_at, fn _effects_so_far ->
|
||||
Ecto.Changeset.change(provider, last_synced_at: DateTime.utc_now())
|
||||
Auth.Provider.Changeset.sync_finished(provider)
|
||||
end)
|
||||
|> Domain.Repo.transaction()
|
||||
|> case do
|
||||
@@ -152,8 +152,25 @@ defmodule Domain.Auth.Adapters.GoogleWorkspace.Jobs do
|
||||
)
|
||||
end
|
||||
else
|
||||
{:error, {status, %{"error" => %{"message" => message}}}} ->
|
||||
provider =
|
||||
Auth.Provider.Changeset.sync_failed(provider, message)
|
||||
|> Domain.Repo.update!()
|
||||
|
||||
log_sync_error(provider, "Google API returned #{status}: #{message}")
|
||||
|
||||
{:error, :retry_later} ->
|
||||
message = "Google API is temporarily unavailable"
|
||||
|
||||
provider =
|
||||
Auth.Provider.Changeset.sync_failed(provider, message)
|
||||
|> Domain.Repo.update!()
|
||||
|
||||
log_sync_error(provider, message)
|
||||
|
||||
{:error, reason} ->
|
||||
Logger.error("Failed syncing provider",
|
||||
account_id: provider.account_id,
|
||||
provider_id: provider.id,
|
||||
reason: inspect(reason)
|
||||
)
|
||||
@@ -163,6 +180,20 @@ defmodule Domain.Auth.Adapters.GoogleWorkspace.Jobs do
|
||||
end
|
||||
end
|
||||
|
||||
defp log_sync_error(provider, message) do
|
||||
metadata = [
|
||||
account_id: provider.account_id,
|
||||
provider_id: provider.id,
|
||||
reason: message
|
||||
]
|
||||
|
||||
if provider.last_syncs_failed >= 3 do
|
||||
Logger.warning("Failed syncing provider", metadata)
|
||||
else
|
||||
Logger.info("Failed syncing provider", metadata)
|
||||
end
|
||||
end
|
||||
|
||||
defp list_membership_tuples(access_token, groups) do
|
||||
Enum.reduce_while(groups, {:ok, []}, fn group, {:ok, tuples} ->
|
||||
case GoogleWorkspace.APIClient.list_group_members(access_token, group["id"]) do
|
||||
|
||||
@@ -17,7 +17,10 @@ defmodule Domain.Auth.Provider do
|
||||
field :created_by, Ecto.Enum, values: ~w[system identity]a
|
||||
belongs_to :created_by_identity, Domain.Auth.Identity
|
||||
|
||||
field :last_syncs_failed, :integer
|
||||
field :last_sync_error, :string
|
||||
field :last_synced_at, :utc_datetime_usec
|
||||
|
||||
field :disabled_at, :utc_datetime_usec
|
||||
field :deleted_at, :utc_datetime_usec
|
||||
timestamps()
|
||||
|
||||
@@ -4,7 +4,7 @@ defmodule Domain.Auth.Provider.Changeset do
|
||||
alias Domain.Auth.{Subject, Provider}
|
||||
|
||||
@create_fields ~w[id name adapter provisioner adapter_config adapter_state disabled_at]a
|
||||
@update_fields ~w[name adapter_config adapter_state provisioner disabled_at deleted_at]a
|
||||
@update_fields ~w[name adapter_config last_syncs_failed last_sync_error adapter_state provisioner disabled_at deleted_at]a
|
||||
@required_fields ~w[name adapter adapter_config provisioner]a
|
||||
|
||||
def create(account, attrs, %Subject{} = subject) do
|
||||
@@ -28,6 +28,24 @@ defmodule Domain.Auth.Provider.Changeset do
|
||||
|> changeset()
|
||||
end
|
||||
|
||||
def sync_finished(%Provider{} = provider) do
|
||||
provider
|
||||
|> change()
|
||||
|> put_change(:last_synced_at, DateTime.utc_now())
|
||||
|> put_change(:last_sync_error, nil)
|
||||
|> put_change(:last_syncs_failed, 0)
|
||||
end
|
||||
|
||||
def sync_failed(%Provider{} = provider, error) do
|
||||
last_syncs_failed = provider.last_syncs_failed || 0
|
||||
|
||||
provider
|
||||
|> change()
|
||||
|> put_change(:last_synced_at, nil)
|
||||
|> put_change(:last_sync_error, error)
|
||||
|> put_change(:last_syncs_failed, last_syncs_failed + 1)
|
||||
end
|
||||
|
||||
defp changeset(changeset) do
|
||||
changeset
|
||||
|> validate_length(:name, min: 1, max: 255)
|
||||
|
||||
@@ -38,6 +38,19 @@ defmodule Domain.Auth.Provider.Query do
|
||||
)
|
||||
end
|
||||
|
||||
def only_ready_to_be_synced(queryable \\ not_deleted()) do
|
||||
where(
|
||||
queryable,
|
||||
[provider: provider],
|
||||
is_nil(provider.last_synced_at) or
|
||||
fragment(
|
||||
"? + LEAST((interval '10 minute' * (COALESCE(?, 0) ^ 2 + 1)), interval '4 hours') < NOW()",
|
||||
provider.last_synced_at,
|
||||
provider.last_syncs_failed
|
||||
)
|
||||
)
|
||||
end
|
||||
|
||||
def by_non_empty_refresh_token(queryable \\ not_deleted()) do
|
||||
where(
|
||||
queryable,
|
||||
@@ -66,6 +79,10 @@ defmodule Domain.Auth.Provider.Query do
|
||||
where(queryable, [provider: provider], is_nil(provider.disabled_at))
|
||||
end
|
||||
|
||||
def not_exceeded_attempts(queryable \\ not_deleted()) do
|
||||
where(queryable, [provider: provider], provider.last_syncs_failed <= 10)
|
||||
end
|
||||
|
||||
def lock(queryable \\ not_deleted()) do
|
||||
lock(queryable, "FOR UPDATE")
|
||||
end
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
defmodule Domain.Clients do
|
||||
use Supervisor
|
||||
alias Domain.{Repo, Auth, Validator}
|
||||
alias Domain.Actors
|
||||
alias Domain.{Accounts, Actors}
|
||||
alias Domain.Clients.{Client, Authorizer, Presence}
|
||||
require Ecto.Query
|
||||
|
||||
@@ -127,13 +127,15 @@ defmodule Domain.Clients do
|
||||
|
||||
# TODO: this is ugly!
|
||||
defp preload_online_status(client) do
|
||||
connected_clients = Presence.list("clients:#{client.id}")
|
||||
%{client | online?: Map.has_key?(connected_clients, client.id)}
|
||||
case Presence.get_by_key("clients:#{client.account_id}", client.id) do
|
||||
[] -> %{client | online?: false}
|
||||
%{metas: [_ | _]} -> %{client | online?: true}
|
||||
end
|
||||
end
|
||||
|
||||
defp preload_online_statuses([]), do: []
|
||||
def preload_online_statuses([]), do: []
|
||||
|
||||
defp preload_online_statuses([client | _] = clients) do
|
||||
def preload_online_statuses([client | _] = clients) do
|
||||
connected_clients = Presence.list("clients:#{client.account_id}")
|
||||
|
||||
Enum.map(clients, fn client ->
|
||||
@@ -233,6 +235,18 @@ defmodule Domain.Clients do
|
||||
:ok
|
||||
end
|
||||
|
||||
def subscribe_for_clients_presence_in_account(%Accounts.Account{} = account) do
|
||||
subscribe_for_clients_presence_in_account(account.id)
|
||||
end
|
||||
|
||||
def subscribe_for_clients_presence_in_account(account_id) do
|
||||
Phoenix.PubSub.subscribe(Domain.PubSub, "clients:#{account_id}")
|
||||
end
|
||||
|
||||
def subscribe_for_clients_presence_for_actor(%Actors.Actor{} = actor) do
|
||||
Phoenix.PubSub.subscribe(Domain.PubSub, "actor_clients:#{actor.id}")
|
||||
end
|
||||
|
||||
def fetch_client_config!(%Client{} = client) do
|
||||
%{
|
||||
clients_upstream_dns: upstream_dns
|
||||
|
||||
@@ -123,13 +123,17 @@ defmodule Domain.Cluster.GoogleComputeLabelsStrategy do
|
||||
end
|
||||
|
||||
{:error, reason} ->
|
||||
Logger.error("Can not fetch list of nodes or access token: #{inspect(reason)}",
|
||||
module: __MODULE__
|
||||
)
|
||||
|
||||
if remaining_retry_count == 0 do
|
||||
Logger.error("Can not fetch list of nodes or access token: #{inspect(reason)}",
|
||||
module: __MODULE__
|
||||
)
|
||||
|
||||
{:error, reason}
|
||||
else
|
||||
Logger.warning("Can not fetch list of nodes or access token: #{inspect(reason)}",
|
||||
module: __MODULE__
|
||||
)
|
||||
|
||||
backoff_interval = Keyword.get(state.config, :backoff_interval, 1_000)
|
||||
:timer.sleep(backoff_interval)
|
||||
fetch_nodes(state, remaining_retry_count - 1)
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
defmodule Domain.Repo.Migrations.AddAuthProvidersLastSyncError do
|
||||
use Ecto.Migration
|
||||
|
||||
def change do
|
||||
alter table(:auth_providers) do
|
||||
add(:last_syncs_failed, :integer, default: 0)
|
||||
add(:last_sync_error, :text)
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -466,5 +466,75 @@ defmodule Domain.Auth.Adapters.GoogleWorkspace.JobsTest do
|
||||
assert org_unit.id in membership_group_ids
|
||||
assert deleted_group.id not in membership_group_ids
|
||||
end
|
||||
|
||||
test "persists the sync error on the provider", %{provider: provider} do
|
||||
bypass = Bypass.open()
|
||||
GoogleWorkspaceDirectory.override_endpoint_url("http://localhost:#{bypass.port}/")
|
||||
|
||||
error_message =
|
||||
"Admin SDK API has not been used in project XXXX before or it is disabled. " <>
|
||||
"Enable it by visiting https://console.developers.google.com/apis/api/admin.googleapis.com/overview?project=XXXX " <>
|
||||
"then retry. If you enabled this API recently, wait a few minutes for the action to propagate to our systems and retry."
|
||||
|
||||
response =
|
||||
%{
|
||||
"error" => %{
|
||||
"code" => 403,
|
||||
"message" => error_message,
|
||||
"errors" => [
|
||||
%{
|
||||
"message" => error_message,
|
||||
"domain" => "usageLimits",
|
||||
"reason" => "accessNotConfigured",
|
||||
"extendedHelp" => "https://console.developers.google.com"
|
||||
}
|
||||
],
|
||||
"status" => "PERMISSION_DENIED",
|
||||
"details" => [
|
||||
%{
|
||||
"@type" => "type.googleapis.com/google.rpc.Help",
|
||||
"links" => [
|
||||
%{
|
||||
"description" => "Google developers console API activation",
|
||||
"url" =>
|
||||
"https://console.developers.google.com/apis/api/admin.googleapis.com/overview?project=100421656358"
|
||||
}
|
||||
]
|
||||
},
|
||||
%{
|
||||
"@type" => "type.googleapis.com/google.rpc.ErrorInfo",
|
||||
"reason" => "SERVICE_DISABLED",
|
||||
"domain" => "googleapis.com",
|
||||
"metadata" => %{
|
||||
"service" => "admin.googleapis.com",
|
||||
"consumer" => "projects/100421656358"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
Bypass.expect_once(bypass, "GET", "/admin/directory/v1/users", fn conn ->
|
||||
Plug.Conn.send_resp(conn, 403, Jason.encode!(response))
|
||||
end)
|
||||
|
||||
assert sync_directory(%{}) == :ok
|
||||
|
||||
assert updated_provider = Repo.get(Domain.Auth.Provider, provider.id)
|
||||
refute updated_provider.last_synced_at
|
||||
assert updated_provider.last_syncs_failed == 1
|
||||
assert updated_provider.last_sync_error == error_message
|
||||
|
||||
Bypass.expect_once(bypass, "GET", "/admin/directory/v1/users", fn conn ->
|
||||
Plug.Conn.send_resp(conn, 500, "")
|
||||
end)
|
||||
|
||||
assert sync_directory(%{}) == :ok
|
||||
|
||||
assert updated_provider = Repo.get(Domain.Auth.Provider, provider.id)
|
||||
refute updated_provider.last_synced_at
|
||||
assert updated_provider.last_syncs_failed == 2
|
||||
assert updated_provider.last_sync_error == "Google API is temporarily unavailable"
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -362,6 +362,31 @@ defmodule Domain.AuthTest do
|
||||
assert Enum.map(providers, & &1.id) |> Enum.sort() ==
|
||||
Enum.sort([provider1.id, provider2.id])
|
||||
end
|
||||
|
||||
test "uses 1/2 regular timeout backoff for failed attempts" do
|
||||
{provider, _bypass} = Fixtures.Auth.start_and_create_google_workspace_provider()
|
||||
# backoff: 10 minutes * (1 + 3 ^ 2) = 100 minutes
|
||||
provider = Domain.Fixture.update!(provider, %{last_sync_error: "foo", last_syncs_failed: 3})
|
||||
|
||||
ninety_nine_minute_ago = DateTime.utc_now() |> DateTime.add(-99, :minute)
|
||||
Domain.Fixture.update!(provider, %{last_synced_at: ninety_nine_minute_ago})
|
||||
assert list_providers_pending_sync_by_adapter(:google_workspace) == {:ok, []}
|
||||
|
||||
one_hundred_one_minute_ago = DateTime.utc_now() |> DateTime.add(-101, :minute)
|
||||
Domain.Fixture.update!(provider, %{last_synced_at: one_hundred_one_minute_ago})
|
||||
assert {:ok, [_provider]} = list_providers_pending_sync_by_adapter(:google_workspace)
|
||||
|
||||
# max backoff: 4 hours
|
||||
provider = Domain.Fixture.update!(provider, %{last_syncs_failed: 300})
|
||||
|
||||
three_hours_fifty_nine_minutes_ago = DateTime.utc_now() |> DateTime.add(-239, :minute)
|
||||
Domain.Fixture.update!(provider, %{last_synced_at: three_hours_fifty_nine_minutes_ago})
|
||||
assert list_providers_pending_sync_by_adapter(:google_workspace) == {:ok, []}
|
||||
|
||||
four_hours_one_minute_ago = DateTime.utc_now() |> DateTime.add(-241, :minute)
|
||||
Domain.Fixture.update!(provider, %{last_synced_at: four_hours_one_minute_ago})
|
||||
assert {:ok, [_provider]} = list_providers_pending_sync_by_adapter(:google_workspace)
|
||||
end
|
||||
end
|
||||
|
||||
describe "new_provider/2" do
|
||||
|
||||
@@ -71,6 +71,17 @@ defmodule Domain.ClientsTest do
|
||||
assert fetch_client_by_id(client.id, subject) == {:ok, client}
|
||||
end
|
||||
|
||||
test "preloads online status", %{unprivileged_actor: actor, unprivileged_subject: subject} do
|
||||
client = Fixtures.Clients.create_client(actor: actor)
|
||||
|
||||
assert {:ok, client} = fetch_client_by_id(client.id, subject)
|
||||
assert client.online? == false
|
||||
|
||||
assert connect_client(client) == :ok
|
||||
assert {:ok, client} = fetch_client_by_id(client.id, subject)
|
||||
assert client.online? == true
|
||||
end
|
||||
|
||||
test "returns client that belongs to another actor with manage permission", %{
|
||||
account: account,
|
||||
unprivileged_subject: subject
|
||||
@@ -182,6 +193,17 @@ defmodule Domain.ClientsTest do
|
||||
assert length(clients) == 2
|
||||
end
|
||||
|
||||
test "preloads online status", %{unprivileged_actor: actor, unprivileged_subject: subject} do
|
||||
Fixtures.Clients.create_client(actor: actor)
|
||||
|
||||
assert {:ok, [client]} = list_clients(subject)
|
||||
assert client.online? == false
|
||||
|
||||
assert connect_client(client) == :ok
|
||||
assert {:ok, [client]} = list_clients(subject)
|
||||
assert client.online? == true
|
||||
end
|
||||
|
||||
test "returns error when subject has no permission to manage clients", %{
|
||||
unprivileged_subject: subject
|
||||
} do
|
||||
|
||||
@@ -198,6 +198,18 @@ defmodule Domain.Fixtures.Auth do
|
||||
update!(provider, deleted_at: DateTime.utc_now())
|
||||
end
|
||||
|
||||
def fail_provider_sync(provider) do
|
||||
update!(provider, last_sync_error: "Message from fixture", last_syncs_failed: 3)
|
||||
end
|
||||
|
||||
def finish_provider_sync(provider) do
|
||||
update!(provider,
|
||||
last_synced_at: DateTime.utc_now(),
|
||||
last_sync_error: nil,
|
||||
last_syncs_failed: 0
|
||||
)
|
||||
end
|
||||
|
||||
def identity_attrs(attrs \\ %{}) do
|
||||
Enum.into(attrs, %{
|
||||
provider_virtual_state: %{}
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import StatusPage from "../vendor/status_page";
|
||||
import { initTabs } from "flowbite";
|
||||
|
||||
let Hooks = {};
|
||||
@@ -62,38 +61,4 @@ Hooks.Copy = {
|
||||
},
|
||||
};
|
||||
|
||||
// Update status indicator when sidebar is mounted or updated
|
||||
let statusIndicatorClassNames = {
|
||||
none: "bg-green-100 text-green-800",
|
||||
minor: "bg-yellow-100 text-yellow-800",
|
||||
major: "bg-orange-100 text-orange-800",
|
||||
critical: "bg-red-100 text-red-800",
|
||||
};
|
||||
|
||||
const statusUpdater = function () {
|
||||
const self = this;
|
||||
const sp = new StatusPage.page({ page: "firezone" });
|
||||
|
||||
sp.summary({
|
||||
success: function (data) {
|
||||
self.el.innerHTML = `
|
||||
<span class="text-xs font-medium mr-2 px-2.5 py-0.5 rounded ${
|
||||
statusIndicatorClassNames[data.status.indicator]
|
||||
}">
|
||||
${data.status.description}
|
||||
</span>
|
||||
`;
|
||||
},
|
||||
error: function (data) {
|
||||
console.error("An error occurred while fetching status page data");
|
||||
self.el.innerHTML = `<span class="${statusIndicatorClassNames.minor}">Unable to fetch status</span>`;
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
Hooks.StatusPage = {
|
||||
mounted: statusUpdater,
|
||||
updated: statusUpdater,
|
||||
};
|
||||
|
||||
export default Hooks;
|
||||
|
||||
184
elixir/apps/web/assets/vendor/status_page.js
vendored
184
elixir/apps/web/assets/vendor/status_page.js
vendored
@@ -1,184 +0,0 @@
|
||||
// StatusPage API Wrapper. Vendored and reviewed by @jamilbk from https://cdn.statuspage.io/se-v2.js
|
||||
;(StatusPage = "undefined" == typeof StatusPage ? {} : StatusPage),
|
||||
(StatusPage.page = function (e) {
|
||||
if (!(e = e || {}).page)
|
||||
throw new Error("A pageId is required to initialize.")
|
||||
;(this.apiKey = e.apiKey || null),
|
||||
(this.error = e.error || this.error),
|
||||
(this.format = e.format || "json"),
|
||||
(this.pageId = e.page),
|
||||
(this.version = e.version || "v2"),
|
||||
(this.secure = !("secure" in e) || e.secure),
|
||||
(this.protocol = this.secure ? "https" : "http"),
|
||||
(this.host = e.host || "statuspage.io"),
|
||||
(this.host_with_port_and_protocol = e.test
|
||||
? ""
|
||||
: this.protocol + "://" + this.pageId + "." + this.host)
|
||||
}),
|
||||
(StatusPage.page.prototype.serialize = function (e, t) {
|
||||
var s = [],
|
||||
r = { sms: "email_sms", webhook: "endpoint" }
|
||||
for (var o in e)
|
||||
if ("to_sentence" !== o) {
|
||||
var i = o
|
||||
o = o in r ? r[o] : o
|
||||
var a = t ? t + "[" + o + "]" : o,
|
||||
n = e[i]
|
||||
s.push(
|
||||
"object" == typeof n
|
||||
? this.serialize(n, a)
|
||||
: encodeURIComponent(a) + "=" + encodeURIComponent(n)
|
||||
)
|
||||
}
|
||||
return s.join("&")
|
||||
}),
|
||||
(StatusPage.page.prototype.createStatusPageCORSRequest = function (e, t) {
|
||||
var s = new XMLHttpRequest()
|
||||
return (
|
||||
"withCredentials" in s
|
||||
? s.open(e, t, !0)
|
||||
: "undefined" != typeof XDomainRequest
|
||||
? (s = new XDomainRequest()).open(e, t)
|
||||
: (s = null),
|
||||
s
|
||||
)
|
||||
}),
|
||||
(StatusPage.page.prototype.executeRequestAndCallbackWithResponse = function (
|
||||
e
|
||||
) {
|
||||
if (!e.path) throw new Error("A path is required to make a request")
|
||||
var t = e.path,
|
||||
s = e.method || "GET",
|
||||
r = e.success || null,
|
||||
o = e.error || this.error,
|
||||
i =
|
||||
this.host_with_port_and_protocol +
|
||||
"/api/" +
|
||||
this.version +
|
||||
"/" +
|
||||
t +
|
||||
"." +
|
||||
this.format,
|
||||
a = this.createStatusPageCORSRequest(s, i)
|
||||
if (a)
|
||||
if (
|
||||
(this.apiKey &&
|
||||
(console.log(
|
||||
"!!! API KEY IN USE - REMOVE BEFORE DEPLOYING TO PRODUCTION !!!"
|
||||
),
|
||||
console.log(
|
||||
"!!! API KEY IN USE - REMOVE BEFORE DEPLOYING TO PRODUCTION !!!"
|
||||
),
|
||||
console.log(
|
||||
"!!! API KEY IN USE - REMOVE BEFORE DEPLOYING TO PRODUCTION !!!"
|
||||
),
|
||||
a.setRequestHeader("Authorization", "OAuth " + this.apiKey)),
|
||||
(a.onload = function () {
|
||||
var e = JSON.parse(a.responseText)
|
||||
r && r(e)
|
||||
}),
|
||||
(a.onerror = o),
|
||||
"POST" === s || "DELETE" === s)
|
||||
) {
|
||||
var n = e.data || {}
|
||||
a.setRequestHeader("Content-type", "application/x-www-form-urlencoded"),
|
||||
a.send(this.serialize(n))
|
||||
} else a.send()
|
||||
}),
|
||||
(StatusPage.page.prototype.get = function (e, t) {
|
||||
if (((t = t || {}), !e)) throw new Error("Path is required.")
|
||||
if (!t.success) throw new Error("Success Callback is required.")
|
||||
var s = t.success || {},
|
||||
r = t.error || {}
|
||||
this.executeRequestAndCallbackWithResponse({
|
||||
path: e,
|
||||
success: s,
|
||||
error: r,
|
||||
method: "GET",
|
||||
})
|
||||
}),
|
||||
(StatusPage.page.prototype.post = function (e, t) {
|
||||
if (((t = t || {}), !e)) throw new Error("Path is required.")
|
||||
var s = {}
|
||||
if ("subscribers" === e) {
|
||||
if (!t.subscriber) throw new Error("Subscriber is required to post.")
|
||||
s.subscriber = t.subscriber
|
||||
} else {
|
||||
if (!t.data) throw new Error("Data is required to post.")
|
||||
s = t.data
|
||||
}
|
||||
var r = t.success || {},
|
||||
o = t.error || {}
|
||||
this.executeRequestAndCallbackWithResponse({
|
||||
data: s,
|
||||
path: e,
|
||||
success: r,
|
||||
error: o,
|
||||
method: "POST",
|
||||
})
|
||||
}),
|
||||
(StatusPage.page.prototype["delete"] = function (e, t) {
|
||||
if (((t = t || {}), !e)) throw new Error("Path is required.")
|
||||
if (!t.subscriber) throw new Error("Data is required to delete.")
|
||||
var s = {}
|
||||
"subscribers" === e ? (s.subscriber = t.subscriber) : (s = t.data)
|
||||
var r = t.success || {},
|
||||
o = t.error || {}
|
||||
this.executeRequestAndCallbackWithResponse({
|
||||
data: s,
|
||||
path: e,
|
||||
success: r,
|
||||
error: o,
|
||||
method: "DELETE",
|
||||
})
|
||||
}),
|
||||
(StatusPage.page.prototype.error = function () {
|
||||
console.log("There was an error with your request")
|
||||
}),
|
||||
(StatusPage.page.prototype.summary = function (e) {
|
||||
this.get("summary", e)
|
||||
}),
|
||||
(StatusPage.page.prototype.status = function (e) {
|
||||
this.get("status", e)
|
||||
}),
|
||||
(StatusPage.page.prototype.components = function (e) {
|
||||
this.get("components", e)
|
||||
}),
|
||||
(StatusPage.page.prototype.incidents = function (e) {
|
||||
switch (e.filter) {
|
||||
case "unresolved":
|
||||
this.get("incidents/unresolved", e)
|
||||
break
|
||||
case "resolved":
|
||||
this.get("incidents/resolved", e)
|
||||
break
|
||||
default:
|
||||
this.get("incidents", e)
|
||||
}
|
||||
}),
|
||||
(StatusPage.page.prototype.scheduled_maintenances = function (e) {
|
||||
switch (e.filter) {
|
||||
case "active":
|
||||
this.get("scheduled-maintenances/active", e)
|
||||
break
|
||||
case "upcoming":
|
||||
this.get("scheduled-maintenances/upcoming", e)
|
||||
break
|
||||
default:
|
||||
this.get("scheduled-maintenances", e)
|
||||
}
|
||||
}),
|
||||
(StatusPage.page.prototype.subscribe = function (e) {
|
||||
if (!e || !e.subscriber) throw new Error("A subscriber object is required.")
|
||||
this.post("subscribers", e)
|
||||
}),
|
||||
(StatusPage.page.prototype.unsubscribe = function (e) {
|
||||
if (!e || !e.subscriber) throw new Error("A subscriber object is required.")
|
||||
if (!e.subscriber.id)
|
||||
throw new Error(
|
||||
"You must supply a subscriber.id in order to cancel a subscription."
|
||||
)
|
||||
this["delete"]("subscribers", e)
|
||||
})
|
||||
|
||||
export default StatusPage
|
||||
@@ -673,16 +673,6 @@ defmodule Web.CoreComponents do
|
||||
"""
|
||||
end
|
||||
|
||||
def status_page_widget(assigns) do
|
||||
~H"""
|
||||
<div class="absolute bottom-0 left-0 justify-left p-4 space-x-4 w-full lg:flex bg-white z-20">
|
||||
<.link href="https://firezone.statuspage.io" class="text-xs hover:underline">
|
||||
<span id="status-page-widget" phx-update="ignore" phx-hook="StatusPage" />
|
||||
</.link>
|
||||
</div>
|
||||
"""
|
||||
end
|
||||
|
||||
attr :type, :string, default: "neutral"
|
||||
attr :class, :string, default: nil
|
||||
attr :rest, :global
|
||||
|
||||
@@ -61,7 +61,13 @@
|
||||
</.sidebar_item_group>
|
||||
|
||||
<:bottom>
|
||||
<.status_page_widget />
|
||||
<div class="absolute bottom-0 left-0 justify-left p-4 space-x-4 w-full lg:flex bg-white z-20">
|
||||
<.link href="https://firezone.statuspage.io" class={["text-xs", link_style()]}>
|
||||
<span class="mr-2 px-2.5 py-0.5 rounded bg-neutral-100 text-neutral-800">
|
||||
Platform status
|
||||
</span>
|
||||
</.link>
|
||||
</div>
|
||||
</:bottom>
|
||||
</.sidebar>
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
defmodule Web.Actors.Show do
|
||||
use Web, :live_view
|
||||
import Web.Actors.Components
|
||||
alias Domain.{Auth, Flows}
|
||||
alias Domain.{Auth, Flows, Clients}
|
||||
alias Domain.Actors
|
||||
|
||||
def mount(%{"id" => id}, _session, socket) do
|
||||
@@ -17,6 +17,9 @@ defmodule Web.Actors.Show do
|
||||
Flows.list_flows_for(actor, socket.assigns.subject,
|
||||
preload: [gateway: [:group], client: [], policy: [:resource, :actor_group]]
|
||||
) do
|
||||
actor = %{actor | clients: Clients.preload_online_statuses(actor.clients)}
|
||||
:ok = Clients.subscribe_for_clients_presence_for_actor(actor)
|
||||
|
||||
{:ok,
|
||||
assign(socket,
|
||||
actor: actor,
|
||||
@@ -263,6 +266,17 @@ defmodule Web.Actors.Show do
|
||||
"""
|
||||
end
|
||||
|
||||
def handle_info(%Phoenix.Socket.Broadcast{topic: "actor_clients:" <> _account_id}, socket) do
|
||||
{:ok, actor} =
|
||||
Actors.fetch_actor_by_id(socket.assigns.actor.id, socket.assigns.subject,
|
||||
preload: [clients: []]
|
||||
)
|
||||
|
||||
actor = %{socket.assigns.actor | clients: Clients.preload_online_statuses(actor.clients)}
|
||||
|
||||
{:noreply, assign(socket, actor: actor)}
|
||||
end
|
||||
|
||||
def handle_event("delete", _params, socket) do
|
||||
with {:ok, _actor} <- Actors.delete_actor(socket.assigns.actor, socket.assigns.subject) do
|
||||
{:noreply, push_navigate(socket, to: ~p"/#{socket.assigns.account}/actors")}
|
||||
|
||||
@@ -4,14 +4,13 @@ defmodule Web.Clients.Index do
|
||||
|
||||
def mount(_params, _session, socket) do
|
||||
with {:ok, clients} <- Clients.list_clients(socket.assigns.subject, preload: :actor) do
|
||||
:ok = Clients.subscribe_for_clients_presence_in_account(socket.assigns.subject.account)
|
||||
{:ok, assign(socket, clients: clients)}
|
||||
else
|
||||
{:error, _reason} -> raise Web.LiveErrors.NotFoundError
|
||||
end
|
||||
end
|
||||
|
||||
# subscribe for presence
|
||||
|
||||
def render(assigns) do
|
||||
~H"""
|
||||
<.breadcrumbs account={@account}>
|
||||
@@ -58,43 +57,8 @@ defmodule Web.Clients.Index do
|
||||
"""
|
||||
end
|
||||
|
||||
# defp resource_filter(assigns) do
|
||||
# ~H"""
|
||||
# <div class="flex flex-col md:flex-row items-center justify-between space-y-3 md:space-y-0 md:space-x-4 p-4">
|
||||
# <div class="w-full md:w-1/2">
|
||||
# <form class="flex items-center">
|
||||
# <label for="simple-search" class="sr-only">Search</label>
|
||||
# <div class="relative w-full">
|
||||
# <div class="absolute inset-y-0 left-0 flex items-center pl-3 pointer-events-none">
|
||||
# <.icon name="hero-magnifying-glass" class="w-5 h-5 text-neutral-500" />
|
||||
# </div>
|
||||
# <input
|
||||
# type="text"
|
||||
# id="simple-search"
|
||||
# class =
|
||||
# {[
|
||||
# "bg-neutral-50 border border-neutral-300 text-neutral-900",
|
||||
# "text-sm rounded-lg",
|
||||
# "block w-full pl-10 p-2"
|
||||
# ]}
|
||||
# placeholder="Search"
|
||||
# required=""
|
||||
# />
|
||||
# </div>
|
||||
# </form>
|
||||
# </div>
|
||||
# <.button_group>
|
||||
# <:first>
|
||||
# All
|
||||
# </:first>
|
||||
# <:middle>
|
||||
# Online
|
||||
# </:middle>
|
||||
# <:last>
|
||||
# Archived
|
||||
# </:last>
|
||||
# </.button_group>
|
||||
# </div>
|
||||
# """
|
||||
# end
|
||||
def handle_info(%Phoenix.Socket.Broadcast{topic: "clients:" <> _account_id}, socket) do
|
||||
{:ok, clients} = Clients.list_clients(socket.assigns.subject, preload: :actor)
|
||||
{:noreply, assign(socket, clients: clients)}
|
||||
end
|
||||
end
|
||||
|
||||
@@ -9,6 +9,8 @@ defmodule Web.Clients.Show do
|
||||
Flows.list_flows_for(client, socket.assigns.subject,
|
||||
preload: [gateway: [:group], policy: [:resource, :actor_group]]
|
||||
) do
|
||||
:ok = Clients.subscribe_for_clients_presence_in_account(client.account_id)
|
||||
|
||||
socket =
|
||||
assign(
|
||||
socket,
|
||||
@@ -52,6 +54,10 @@ defmodule Web.Clients.Show do
|
||||
<:label>Name</:label>
|
||||
<:value><%= @client.name %></:value>
|
||||
</.vertical_table_row>
|
||||
<.vertical_table_row>
|
||||
<:label>Status</:label>
|
||||
<:value><.connection_status schema={@client} /></:value>
|
||||
</.vertical_table_row>
|
||||
<.vertical_table_row>
|
||||
<:label>Owner</:label>
|
||||
<:value>
|
||||
@@ -152,6 +158,27 @@ defmodule Web.Clients.Show do
|
||||
"""
|
||||
end
|
||||
|
||||
def handle_info(
|
||||
%Phoenix.Socket.Broadcast{topic: "clients:" <> _account_id, payload: payload},
|
||||
socket
|
||||
) do
|
||||
client = socket.assigns.client
|
||||
|
||||
socket =
|
||||
cond do
|
||||
Map.has_key?(payload.joins, client.id) ->
|
||||
assign(socket, client: %{client | online?: true})
|
||||
|
||||
Map.has_key?(payload.leaves, client.id) ->
|
||||
assign(socket, client: %{client | online?: false})
|
||||
|
||||
true ->
|
||||
socket
|
||||
end
|
||||
|
||||
{:noreply, socket}
|
||||
end
|
||||
|
||||
def handle_event("delete", _params, socket) do
|
||||
{:ok, _client} = Clients.delete_client(socket.assigns.client, socket.assigns.subject)
|
||||
{:noreply, push_navigate(socket, to: ~p"/#{socket.assigns.account}/clients")}
|
||||
|
||||
@@ -121,17 +121,21 @@ defmodule Web.Gateways.Show do
|
||||
%Phoenix.Socket.Broadcast{topic: "gateway_groups:" <> _account_id, payload: payload},
|
||||
socket
|
||||
) do
|
||||
if Map.has_key?(payload.joins, socket.assigns.gateway.id) or
|
||||
Map.has_key?(payload.leaves, socket.assigns.gateway.id) do
|
||||
{:ok, gateway} =
|
||||
Gateways.fetch_gateway_by_id(socket.assigns.gateway.id, socket.assigns.subject,
|
||||
preload: :group
|
||||
)
|
||||
gateway = socket.assigns.gateway
|
||||
|
||||
{:noreply, assign(socket, gateway: gateway)}
|
||||
else
|
||||
{:noreply, socket}
|
||||
end
|
||||
socket =
|
||||
cond do
|
||||
Map.has_key?(payload.joins, gateway.id) ->
|
||||
assign(socket, gateway: %{gateway | online?: true})
|
||||
|
||||
Map.has_key?(payload.leaves, gateway.id) ->
|
||||
assign(socket, gateway: %{gateway | online?: false})
|
||||
|
||||
true ->
|
||||
socket
|
||||
end
|
||||
|
||||
{:noreply, socket}
|
||||
end
|
||||
|
||||
def handle_event("delete", _params, socket) do
|
||||
|
||||
@@ -110,12 +110,15 @@ defmodule Web.RelayGroups.Show do
|
||||
end
|
||||
|
||||
def handle_info(%Phoenix.Socket.Broadcast{topic: "relay_groups:" <> _account_id}, socket) do
|
||||
socket =
|
||||
push_navigate(socket,
|
||||
to: ~p"/#{socket.assigns.account}/relay_groups/#{socket.assigns.group}"
|
||||
{:ok, group} =
|
||||
Relays.fetch_group_by_id(socket.assigns.group.id, socket.assigns.subject,
|
||||
preload: [
|
||||
relays: [token: [created_by_identity: [:actor]]],
|
||||
created_by_identity: [:actor]
|
||||
]
|
||||
)
|
||||
|
||||
{:noreply, socket}
|
||||
{:noreply, assign(socket, group: group)}
|
||||
end
|
||||
|
||||
def handle_event("delete", _params, socket) do
|
||||
|
||||
@@ -118,15 +118,21 @@ defmodule Web.Relays.Show do
|
||||
%Phoenix.Socket.Broadcast{topic: "relay_groups:" <> _account_id, payload: payload},
|
||||
socket
|
||||
) do
|
||||
if Map.has_key?(payload.joins, socket.assigns.relay.id) or
|
||||
Map.has_key?(payload.leaves, socket.assigns.relay.id) do
|
||||
{:ok, relay} =
|
||||
Relays.fetch_relay_by_id(socket.assigns.relay.id, socket.assigns.subject, preload: :group)
|
||||
relay = socket.assigns.relay
|
||||
|
||||
{:noreply, assign(socket, relay: relay)}
|
||||
else
|
||||
{:noreply, socket}
|
||||
end
|
||||
socket =
|
||||
cond do
|
||||
Map.has_key?(payload.joins, relay.id) ->
|
||||
assign(socket, relay: %{relay | online?: true})
|
||||
|
||||
Map.has_key?(payload.leaves, relay.id) ->
|
||||
assign(socket, relay: %{relay | online?: false})
|
||||
|
||||
true ->
|
||||
socket
|
||||
end
|
||||
|
||||
{:noreply, socket}
|
||||
end
|
||||
|
||||
def handle_event("delete", _params, socket) do
|
||||
|
||||
@@ -148,7 +148,11 @@ defmodule Web.Settings.IdentityProviders.Components do
|
||||
def sync_status(%{provider: %{provisioner: :custom}} = assigns) do
|
||||
~H"""
|
||||
<div :if={not is_nil(@provider.last_synced_at)} class="flex items-center">
|
||||
<span class="w-3 h-3 bg-green-500 rounded-full"></span>
|
||||
<span class={[
|
||||
"w-3 h-3 rounded-full",
|
||||
(@provider.last_syncs_failed > 3 && "bg-red-500") || "bg-green-500"
|
||||
]}>
|
||||
</span>
|
||||
<span class="ml-3">
|
||||
Synced
|
||||
<.link navigate={~p"/#{@account}/actors?provider_id=#{@provider.id}"} class={link_style()}>
|
||||
|
||||
@@ -47,7 +47,9 @@ defmodule Web.Settings.IdentityProviders.GoogleWorkspace.Connect do
|
||||
GoogleWorkspace.verify_and_upsert_identity(subject.actor, provider, payload),
|
||||
attrs = %{
|
||||
adapter_state: identity.provider_state,
|
||||
disabled_at: nil
|
||||
disabled_at: nil,
|
||||
last_syncs_failed: 0,
|
||||
last_sync_error: nil
|
||||
},
|
||||
{:ok, _provider} <- Domain.Auth.update_provider(provider, attrs, subject) do
|
||||
redirect(conn,
|
||||
|
||||
@@ -1,14 +1,23 @@
|
||||
defmodule Web.Settings.IdentityProviders.GoogleWorkspace.Show do
|
||||
use Web, :live_view
|
||||
import Web.Settings.IdentityProviders.Components
|
||||
alias Domain.Auth
|
||||
alias Domain.{Auth, Actors}
|
||||
|
||||
def mount(%{"provider_id" => provider_id}, _session, socket) do
|
||||
with {:ok, provider} <-
|
||||
Auth.fetch_provider_by_id(provider_id, socket.assigns.subject,
|
||||
preload: [created_by_identity: [:actor]]
|
||||
) do
|
||||
{:ok, assign(socket, provider: provider)}
|
||||
),
|
||||
{:ok, identities_count_by_provider_id} <-
|
||||
Auth.fetch_identities_count_grouped_by_provider_id(socket.assigns.subject),
|
||||
{:ok, groups_count_by_provider_id} <-
|
||||
Actors.fetch_groups_count_grouped_by_provider_id(socket.assigns.subject) do
|
||||
{:ok,
|
||||
assign(socket,
|
||||
provider: provider,
|
||||
identities_count_by_provider_id: identities_count_by_provider_id,
|
||||
groups_count_by_provider_id: groups_count_by_provider_id
|
||||
)}
|
||||
else
|
||||
_ -> raise Web.LiveErrors.NotFoundError
|
||||
end
|
||||
@@ -80,6 +89,33 @@ defmodule Web.Settings.IdentityProviders.GoogleWorkspace.Show do
|
||||
<.status provider={@provider} />
|
||||
</:value>
|
||||
</.vertical_table_row>
|
||||
|
||||
<.vertical_table_row>
|
||||
<:label>Sync Status</:label>
|
||||
<:value>
|
||||
<.sync_status
|
||||
account={@account}
|
||||
provider={@provider}
|
||||
identities_count_by_provider_id={@identities_count_by_provider_id}
|
||||
groups_count_by_provider_id={@groups_count_by_provider_id}
|
||||
/>
|
||||
<div
|
||||
:if={
|
||||
(is_nil(@provider.last_synced_at) and not is_nil(@provider.last_sync_error)) or
|
||||
(@provider.last_syncs_failed > 3 and not is_nil(@provider.last_sync_error))
|
||||
}
|
||||
class="p-3 mt-2 border-l-4 border-red-500 bg-red-100 rounded-md"
|
||||
>
|
||||
<p class="font-bold text-red-700">
|
||||
IdP provider reported an error during the last sync:
|
||||
</p>
|
||||
<div class="flex items-center mt-1">
|
||||
<span class="text-red-500 font-mono"><%= @provider.last_sync_error %></span>
|
||||
</div>
|
||||
</div>
|
||||
</:value>
|
||||
</.vertical_table_row>
|
||||
|
||||
<.vertical_table_row>
|
||||
<:label>Client ID</:label>
|
||||
<:value><%= @provider.adapter_config["client_id"] %></:value>
|
||||
|
||||
@@ -52,6 +52,44 @@ defmodule Web.Live.Actors.ShowTest do
|
||||
assert breadcrumbs =~ actor.name
|
||||
end
|
||||
|
||||
test "renders clients table", %{
|
||||
conn: conn
|
||||
} do
|
||||
account = Fixtures.Accounts.create_account()
|
||||
actor = Fixtures.Actors.create_actor(type: :account_admin_user, account: account)
|
||||
identity = Fixtures.Auth.create_identity(account: account, actor: actor)
|
||||
client = Fixtures.Clients.create_client(account: account, actor: actor)
|
||||
|
||||
{:ok, lv, _html} =
|
||||
conn
|
||||
|> authorize_conn(identity)
|
||||
|> live(~p"/#{account}/actors/#{actor}")
|
||||
|
||||
[row] =
|
||||
lv
|
||||
|> element("#clients")
|
||||
|> render()
|
||||
|> table_to_map()
|
||||
|
||||
assert row["name"] == client.name
|
||||
assert row["status"] == "Offline"
|
||||
|
||||
assert Domain.Clients.connect_client(client) == :ok
|
||||
|
||||
{:ok, lv, _html} =
|
||||
conn
|
||||
|> authorize_conn(identity)
|
||||
|> live(~p"/#{account}/actors/#{actor}")
|
||||
|
||||
[row] =
|
||||
lv
|
||||
|> element("#clients")
|
||||
|> render()
|
||||
|> table_to_map()
|
||||
|
||||
assert row["status"] == "Online"
|
||||
end
|
||||
|
||||
test "renders flows table", %{
|
||||
conn: conn
|
||||
} do
|
||||
|
||||
@@ -91,6 +91,7 @@ defmodule Web.Live.Clients.ShowTest do
|
||||
assert table["identifier"] == client.id
|
||||
assert table["name"] == client.name
|
||||
assert table["owner"] =~ actor.name
|
||||
assert table["status"] =~ "Offline"
|
||||
assert table["created"]
|
||||
assert table["last seen"]
|
||||
assert table["last seen remote ip"] =~ to_string(client.last_seen_remote_ip)
|
||||
|
||||
@@ -149,6 +149,59 @@ defmodule Web.Live.Settings.IdentityProviders.GoogleWorkspace.Connect do
|
||||
assert flash(conn, :error) == "Your session has expired, please try again."
|
||||
end
|
||||
|
||||
test "resets the sync error when IdP is reconnected", %{
|
||||
account: account,
|
||||
conn: conn
|
||||
} do
|
||||
{provider, bypass} =
|
||||
Fixtures.Auth.start_and_create_google_workspace_provider(account: account)
|
||||
|
||||
provider = Fixtures.Auth.fail_provider_sync(provider)
|
||||
|
||||
actor = Fixtures.Actors.create_actor(type: :account_admin_user, account: account)
|
||||
identity = Fixtures.Auth.create_identity(account: account, actor: actor, provider: provider)
|
||||
|
||||
redirected_conn =
|
||||
conn
|
||||
|> authorize_conn(identity)
|
||||
|> assign(:account, account)
|
||||
|> get(
|
||||
~p"/#{account}/settings/identity_providers/google_workspace/#{provider}/redirect",
|
||||
%{}
|
||||
)
|
||||
|
||||
{token, _claims} = Mocks.OpenIDConnect.generate_openid_connect_token(provider, identity)
|
||||
Mocks.OpenIDConnect.expect_refresh_token(bypass, %{"id_token" => token})
|
||||
Mocks.OpenIDConnect.expect_userinfo(bypass)
|
||||
|
||||
cookie_key = "fz_auth_state_#{provider.id}"
|
||||
redirected_conn = fetch_cookies(redirected_conn)
|
||||
{state, _verifier} = redirected_conn.cookies[cookie_key] |> :erlang.binary_to_term([:safe])
|
||||
%{value: signed_state} = redirected_conn.resp_cookies[cookie_key]
|
||||
|
||||
conn =
|
||||
conn
|
||||
|> authorize_conn(identity)
|
||||
|> assign(:account, account)
|
||||
|> put_req_cookie(cookie_key, signed_state)
|
||||
|> put_session(:foo, "bar")
|
||||
|> put_session(:preferred_locale, "en_US")
|
||||
|> get(
|
||||
~p"/#{account.id}/settings/identity_providers/google_workspace/#{provider.id}/handle_callback",
|
||||
%{
|
||||
"state" => state,
|
||||
"code" => "MyFakeCode"
|
||||
}
|
||||
)
|
||||
|
||||
assert redirected_to(conn) ==
|
||||
~p"/#{account}/settings/identity_providers/google_workspace/#{provider}"
|
||||
|
||||
assert provider = Repo.get(Domain.Auth.Provider, provider.id)
|
||||
assert provider.last_sync_error == nil
|
||||
assert provider.last_syncs_failed == 0
|
||||
end
|
||||
|
||||
test "redirects to the actors index when credentials are valid and return path is empty", %{
|
||||
account: account,
|
||||
conn: conn
|
||||
|
||||
@@ -91,10 +91,48 @@ defmodule Web.Live.Settings.IdentityProviders.GoogleWorkspace.ShowTest do
|
||||
|
||||
assert table["name"] == provider.name
|
||||
assert table["status"] == "Active"
|
||||
assert table["sync status"] == "Never synced"
|
||||
assert table["client id"] == provider.adapter_config["client_id"]
|
||||
assert around_now?(table["created"])
|
||||
end
|
||||
|
||||
test "renders sync status", %{
|
||||
account: account,
|
||||
provider: provider,
|
||||
identity: identity,
|
||||
conn: conn
|
||||
} do
|
||||
provider = Fixtures.Auth.fail_provider_sync(provider)
|
||||
|
||||
{:ok, lv, _html} =
|
||||
conn
|
||||
|> authorize_conn(identity)
|
||||
|> live(~p"/#{account}/settings/identity_providers/google_workspace/#{provider}")
|
||||
|
||||
table =
|
||||
lv
|
||||
|> element("#provider")
|
||||
|> render()
|
||||
|> vertical_table_to_map()
|
||||
|
||||
assert table["sync status"] =~ provider.last_sync_error
|
||||
|
||||
provider = Fixtures.Auth.finish_provider_sync(provider)
|
||||
|
||||
{:ok, lv, _html} =
|
||||
conn
|
||||
|> authorize_conn(identity)
|
||||
|> live(~p"/#{account}/settings/identity_providers/google_workspace/#{provider}")
|
||||
|
||||
table =
|
||||
lv
|
||||
|> element("#provider")
|
||||
|> render()
|
||||
|> vertical_table_to_map()
|
||||
|
||||
assert table["sync status"] =~ "Synced 1 identity and 0 groups"
|
||||
end
|
||||
|
||||
test "renders name of actor that created provider", %{
|
||||
account: account,
|
||||
actor: actor,
|
||||
|
||||
Reference in New Issue
Block a user