mirror of
https://github.com/outbackdingo/firezone.git
synced 2026-01-27 18:18:55 +00:00
feat(portal): Use Service Accounts to sync Google Workspace directory (#6390)
We will need to update the docs for the website, some screens to show where the settings are: <img width="1728" alt="Screenshot 2024-08-19 at 1 04 23 PM" src="https://github.com/user-attachments/assets/88ebb06f-241d-44c8-90fa-258d0b78905e"> <img width="1436" alt="Screenshot 2024-08-19 at 1 04 02 PM" src="https://github.com/user-attachments/assets/5f7a1011-5a53-4348-81cb-da804ee18bed"> Related [#5959](https://github.com/firezone/firezone/issues/5959) --------- Signed-off-by: Andrew Dryga <andrew@dryga.com> Co-authored-by: Jamil <jamilbk@users.noreply.github.com>
This commit is contained in:
@@ -4,6 +4,7 @@ defmodule Domain.Auth.Adapters.GoogleWorkspace do
|
||||
alias Domain.Auth.{Provider, Adapter}
|
||||
alias Domain.Auth.Adapters.OpenIDConnect
|
||||
alias Domain.Auth.Adapters.GoogleWorkspace
|
||||
alias Domain.Auth.Adapters.GoogleWorkspace.APIClient
|
||||
require Logger
|
||||
|
||||
@behaviour Adapter
|
||||
@@ -69,6 +70,30 @@ defmodule Domain.Auth.Adapters.GoogleWorkspace do
|
||||
OpenIDConnect.sign_out(provider, identity, redirect_url)
|
||||
end
|
||||
|
||||
def fetch_service_account_token(%Provider{} = provider) do
|
||||
unix_timestamp = :os.system_time(:seconds)
|
||||
key = provider.adapter_config["service_account_json_key"]
|
||||
jws = %{"alg" => "RS256", "typ" => "JWT"}
|
||||
jwk = JOSE.JWK.from_pem(key["private_key"])
|
||||
|
||||
claim_set =
|
||||
%{
|
||||
"iss" => key["client_email"],
|
||||
"scope" => Enum.join(GoogleWorkspace.Settings.scope(), " "),
|
||||
"aud" => "https://oauth2.googleapis.com/token",
|
||||
"exp" => unix_timestamp + 3600,
|
||||
"iat" => unix_timestamp
|
||||
}
|
||||
|> Jason.encode!()
|
||||
|
||||
jwt =
|
||||
JOSE.JWS.sign(jwk, claim_set, jws)
|
||||
|> JOSE.JWS.compact()
|
||||
|> elem(1)
|
||||
|
||||
APIClient.fetch_service_account_token(jwt)
|
||||
end
|
||||
|
||||
@impl true
|
||||
def verify_and_update_identity(%Provider{} = provider, payload) do
|
||||
OpenIDConnect.verify_and_update_identity(provider, payload)
|
||||
|
||||
@@ -34,6 +34,49 @@ defmodule Domain.Auth.Adapters.GoogleWorkspace.APIClient do
|
||||
[conn_opts: [transport_opts: transport_opts]]
|
||||
end
|
||||
|
||||
def fetch_service_account_token(jwt) do
|
||||
endpoint =
|
||||
Domain.Config.fetch_env!(:domain, __MODULE__)
|
||||
|> Keyword.fetch!(:token_endpoint)
|
||||
|
||||
token_endpoint = Path.join(endpoint, "token")
|
||||
|
||||
payload =
|
||||
URI.encode_query(%{
|
||||
"grant_type" => "urn:ietf:params:oauth:grant-type:jwt-bearer",
|
||||
"assertion" => jwt
|
||||
})
|
||||
|
||||
request =
|
||||
Finch.build(
|
||||
:post,
|
||||
token_endpoint,
|
||||
[{"Content-Type", "application/x-www-form-urlencoded"}],
|
||||
payload
|
||||
)
|
||||
|
||||
with {:ok, %Finch.Response{body: response, status: status}} when status in 200..299 <-
|
||||
Finch.request(request, @pool_name),
|
||||
{:ok, %{"access_token" => access_token}} <- Jason.decode(response) do
|
||||
{:ok, access_token}
|
||||
else
|
||||
{: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
|
||||
|
||||
other ->
|
||||
other
|
||||
end
|
||||
end
|
||||
|
||||
def list_users(api_token) do
|
||||
endpoint =
|
||||
Domain.Config.fetch_env!(:domain, __MODULE__)
|
||||
|
||||
@@ -23,7 +23,26 @@ defmodule Domain.Auth.Adapters.GoogleWorkspace.Jobs.SyncDirectory do
|
||||
end
|
||||
|
||||
def gather_provider_data(provider, task_supervisor_pid) do
|
||||
access_token = provider.adapter_state["access_token"]
|
||||
access_token =
|
||||
with json_key when not is_nil(json_key) <-
|
||||
provider.adapter_config["service_account_json_key"],
|
||||
{:ok, access_token} <- GoogleWorkspace.fetch_service_account_token(provider) do
|
||||
access_token
|
||||
else
|
||||
{:error, reason} ->
|
||||
Logger.error("Failed to fetch service account token",
|
||||
reason: inspect(reason),
|
||||
account_id: provider.account_id,
|
||||
account_slug: provider.account.slug,
|
||||
provider_id: provider.id,
|
||||
provider_adapter: provider.adapter
|
||||
)
|
||||
|
||||
{:error, reason}
|
||||
|
||||
_other ->
|
||||
provider.adapter_state["access_token"]
|
||||
end
|
||||
|
||||
async_results =
|
||||
DirectorySync.run_async_requests(task_supervisor_pid,
|
||||
|
||||
@@ -3,6 +3,7 @@ defmodule Domain.Auth.Adapters.GoogleWorkspace.Settings do
|
||||
|
||||
@scope ~w[
|
||||
openid email profile
|
||||
https://www.googleapis.com/auth/admin.directory.customer.readonly
|
||||
https://www.googleapis.com/auth/admin.directory.orgunit.readonly
|
||||
https://www.googleapis.com/auth/admin.directory.group.readonly
|
||||
https://www.googleapis.com/auth/admin.directory.user.readonly
|
||||
@@ -17,6 +18,26 @@ defmodule Domain.Auth.Adapters.GoogleWorkspace.Settings do
|
||||
field :client_id, :string
|
||||
field :client_secret, :string
|
||||
field :discovery_document_uri, :string, default: @discovery_document_uri
|
||||
|
||||
embeds_one :service_account_json_key, GoogleServiceAccountKey,
|
||||
primary_key: false,
|
||||
on_replace: :update do
|
||||
field :type, :string
|
||||
field :project_id, :string
|
||||
|
||||
field :private_key_id, :string
|
||||
field :private_key, :string
|
||||
|
||||
field :client_email, :string
|
||||
field :client_id, :string
|
||||
|
||||
field :auth_uri, :string
|
||||
field :token_uri, :string
|
||||
field :auth_provider_x509_cert_url, :string
|
||||
field :client_x509_cert_url, :string
|
||||
|
||||
field :universe_domain, :string
|
||||
end
|
||||
end
|
||||
|
||||
def scope, do: @scope
|
||||
|
||||
@@ -8,6 +8,12 @@ defmodule Domain.Auth.Adapters.GoogleWorkspace.Settings.Changeset do
|
||||
client_id client_secret
|
||||
discovery_document_uri]a
|
||||
|
||||
@key_fields ~w[type project_id
|
||||
private_key_id private_key
|
||||
client_email client_id
|
||||
auth_uri token_uri auth_provider_x509_cert_url client_x509_cert_url
|
||||
universe_domain]a
|
||||
|
||||
def changeset(%Settings{} = settings, attrs) do
|
||||
changeset =
|
||||
settings
|
||||
@@ -15,9 +21,19 @@ defmodule Domain.Auth.Adapters.GoogleWorkspace.Settings.Changeset do
|
||||
|> validate_required(@fields)
|
||||
|> OpenIDConnect.Settings.Changeset.validate_discovery_document_uri()
|
||||
|> validate_inclusion(:response_type, ~w[code])
|
||||
|> cast_embed(:service_account_json_key,
|
||||
with: &service_account_key_changeset/2,
|
||||
required: true
|
||||
)
|
||||
|
||||
Enum.reduce(Settings.scope(), changeset, fn scope, changeset ->
|
||||
validate_format(changeset, :scope, ~r/#{scope}/, message: "must include #{scope} scope")
|
||||
end)
|
||||
end
|
||||
|
||||
def service_account_key_changeset(%Settings.GoogleServiceAccountKey{} = key, attrs) do
|
||||
key
|
||||
|> cast(attrs, @key_fields)
|
||||
|> validate_required(@key_fields)
|
||||
end
|
||||
end
|
||||
|
||||
@@ -55,6 +55,7 @@ defmodule Domain.MixProject do
|
||||
|
||||
# Auth-related deps
|
||||
{:plug_crypto, "~> 2.0"},
|
||||
{:jose, "~> 1.11"},
|
||||
{:openid_connect,
|
||||
github: "firezone/openid_connect", ref: "e4d9dca8ae43c765c00a7d3dfa12d6f24f5b3418"},
|
||||
{:argon2_elixir, "~> 4.0"},
|
||||
|
||||
@@ -32,6 +32,43 @@ defmodule Domain.Auth.Adapters.GoogleWorkspace.Jobs.SyncDirectoryTest do
|
||||
"IdP sync is not enabled in your subscription plan"
|
||||
end
|
||||
|
||||
test "uses service account token when it's available" do
|
||||
bypass = Bypass.open()
|
||||
|
||||
GoogleWorkspaceDirectory.override_endpoint_url("http://localhost:#{bypass.port}/")
|
||||
GoogleWorkspaceDirectory.mock_groups_list_endpoint(bypass, [])
|
||||
GoogleWorkspaceDirectory.mock_organization_units_list_endpoint(bypass, [])
|
||||
GoogleWorkspaceDirectory.mock_users_list_endpoint(bypass, [])
|
||||
GoogleWorkspaceDirectory.mock_token_endpoint(bypass)
|
||||
|
||||
{:ok, pid} = Task.Supervisor.start_link()
|
||||
assert execute(%{task_supervisor: pid}) == :ok
|
||||
|
||||
assert_receive {:bypass_request,
|
||||
%{req_headers: [{"authorization", "Bearer GOOGLE_0AUTH_ACCESS_TOKEN"} | _]}}
|
||||
end
|
||||
|
||||
test "uses admin user token as a fallback", %{provider: provider} do
|
||||
bypass = Bypass.open()
|
||||
|
||||
GoogleWorkspaceDirectory.override_endpoint_url("http://localhost:#{bypass.port}/")
|
||||
GoogleWorkspaceDirectory.mock_groups_list_endpoint(bypass, [])
|
||||
GoogleWorkspaceDirectory.mock_organization_units_list_endpoint(bypass, [])
|
||||
GoogleWorkspaceDirectory.mock_users_list_endpoint(bypass, [])
|
||||
|
||||
provider
|
||||
|> Ecto.Changeset.change(
|
||||
adapter_config: Map.put(provider.adapter_config, "service_account_json_key", nil)
|
||||
)
|
||||
|> Repo.update!()
|
||||
|
||||
{:ok, pid} = Task.Supervisor.start_link()
|
||||
assert execute(%{task_supervisor: pid}) == :ok
|
||||
|
||||
assert_receive {:bypass_request,
|
||||
%{req_headers: [{"authorization", "Bearer OIDC_ACCESS_TOKEN"} | _]}}
|
||||
end
|
||||
|
||||
test "syncs IdP data", %{provider: provider} do
|
||||
bypass = Bypass.open()
|
||||
|
||||
@@ -173,6 +210,7 @@ defmodule Domain.Auth.Adapters.GoogleWorkspace.Jobs.SyncDirectoryTest do
|
||||
GoogleWorkspaceDirectory.mock_groups_list_endpoint(bypass, groups)
|
||||
GoogleWorkspaceDirectory.mock_organization_units_list_endpoint(bypass, organization_units)
|
||||
GoogleWorkspaceDirectory.mock_users_list_endpoint(bypass, users)
|
||||
GoogleWorkspaceDirectory.mock_token_endpoint(bypass)
|
||||
|
||||
Enum.each(groups, fn group ->
|
||||
GoogleWorkspaceDirectory.mock_group_members_list_endpoint(bypass, group["id"], members)
|
||||
@@ -268,6 +306,7 @@ defmodule Domain.Auth.Adapters.GoogleWorkspace.Jobs.SyncDirectoryTest do
|
||||
GoogleWorkspaceDirectory.mock_groups_list_endpoint(bypass, [])
|
||||
GoogleWorkspaceDirectory.mock_organization_units_list_endpoint(bypass, [])
|
||||
GoogleWorkspaceDirectory.mock_users_list_endpoint(bypass, users)
|
||||
GoogleWorkspaceDirectory.mock_token_endpoint(bypass)
|
||||
|
||||
{:ok, pid} = Task.Supervisor.start_link()
|
||||
assert execute(%{task_supervisor: pid}) == :ok
|
||||
@@ -497,6 +536,7 @@ defmodule Domain.Auth.Adapters.GoogleWorkspace.Jobs.SyncDirectoryTest do
|
||||
GoogleWorkspaceDirectory.mock_groups_list_endpoint(bypass, groups)
|
||||
GoogleWorkspaceDirectory.mock_organization_units_list_endpoint(bypass, organization_units)
|
||||
GoogleWorkspaceDirectory.mock_users_list_endpoint(bypass, users)
|
||||
GoogleWorkspaceDirectory.mock_token_endpoint(bypass)
|
||||
|
||||
GoogleWorkspaceDirectory.mock_group_members_list_endpoint(bypass, "GROUP_ID1", two_members)
|
||||
GoogleWorkspaceDirectory.mock_group_members_list_endpoint(bypass, "GROUP_ID2", one_member)
|
||||
@@ -621,6 +661,7 @@ defmodule Domain.Auth.Adapters.GoogleWorkspace.Jobs.SyncDirectoryTest do
|
||||
|
||||
bypass = Bypass.open()
|
||||
GoogleWorkspaceDirectory.override_endpoint_url("http://localhost:#{bypass.port}/")
|
||||
GoogleWorkspaceDirectory.mock_token_endpoint(bypass)
|
||||
|
||||
for path <- [
|
||||
"/admin/directory/v1/users",
|
||||
@@ -668,6 +709,7 @@ defmodule Domain.Auth.Adapters.GoogleWorkspace.Jobs.SyncDirectoryTest do
|
||||
test "disables the sync on 401 response code", %{provider: provider} do
|
||||
bypass = Bypass.open()
|
||||
GoogleWorkspaceDirectory.override_endpoint_url("http://localhost:#{bypass.port}/")
|
||||
GoogleWorkspaceDirectory.mock_token_endpoint(bypass)
|
||||
|
||||
error_message =
|
||||
"Admin SDK API has not been used in project XXXX before or it is disabled. " <>
|
||||
|
||||
@@ -55,7 +55,8 @@ defmodule Domain.Auth.Adapters.GoogleWorkspaceTest do
|
||||
assert errors_on(changeset) == %{
|
||||
adapter_config: %{
|
||||
client_id: ["can't be blank"],
|
||||
client_secret: ["can't be blank"]
|
||||
client_secret: ["can't be blank"],
|
||||
service_account_json_key: ["can't be blank"]
|
||||
}
|
||||
}
|
||||
end
|
||||
@@ -71,7 +72,20 @@ defmodule Domain.Auth.Adapters.GoogleWorkspaceTest do
|
||||
adapter_config: %{
|
||||
client_id: "client_id",
|
||||
client_secret: "client_secret",
|
||||
discovery_document_uri: discovery_document_url
|
||||
discovery_document_uri: discovery_document_url,
|
||||
service_account_json_key: %{
|
||||
type: "service_account",
|
||||
project_id: "project_id",
|
||||
private_key_id: "private_key_id",
|
||||
private_key: "private_key",
|
||||
client_email: "client_email",
|
||||
client_id: "client_id",
|
||||
auth_uri: "auth_uri",
|
||||
token_uri: "token_uri",
|
||||
auth_provider_x509_cert_url: "auth_provider_x509_cert_url",
|
||||
client_x509_cert_url: "client_x509_cert_url",
|
||||
universe_domain: "universe_domain"
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
@@ -90,6 +104,7 @@ defmodule Domain.Auth.Adapters.GoogleWorkspaceTest do
|
||||
"openid",
|
||||
"email",
|
||||
"profile",
|
||||
"https://www.googleapis.com/auth/admin.directory.customer.readonly",
|
||||
"https://www.googleapis.com/auth/admin.directory.orgunit.readonly",
|
||||
"https://www.googleapis.com/auth/admin.directory.group.readonly",
|
||||
"https://www.googleapis.com/auth/admin.directory.user.readonly"
|
||||
@@ -99,7 +114,20 @@ defmodule Domain.Auth.Adapters.GoogleWorkspaceTest do
|
||||
"response_type" => "code",
|
||||
"client_id" => "client_id",
|
||||
"client_secret" => "client_secret",
|
||||
"discovery_document_uri" => discovery_document_url
|
||||
"discovery_document_uri" => discovery_document_url,
|
||||
"service_account_json_key" => %{
|
||||
type: "service_account",
|
||||
client_id: "client_id",
|
||||
project_id: "project_id",
|
||||
private_key: "private_key",
|
||||
private_key_id: "private_key_id",
|
||||
client_email: "client_email",
|
||||
auth_uri: "auth_uri",
|
||||
token_uri: "token_uri",
|
||||
auth_provider_x509_cert_url: "auth_provider_x509_cert_url",
|
||||
client_x509_cert_url: "client_x509_cert_url",
|
||||
universe_domain: "universe_domain"
|
||||
}
|
||||
}
|
||||
end
|
||||
end
|
||||
@@ -111,6 +139,30 @@ defmodule Domain.Auth.Adapters.GoogleWorkspaceTest do
|
||||
end
|
||||
end
|
||||
|
||||
describe "fetch_service_account_token/1" do
|
||||
test "generates a valid JWT" do
|
||||
Bypass.open()
|
||||
|> Mocks.GoogleWorkspaceDirectory.mock_token_endpoint()
|
||||
|
||||
{provider, _bypass} = Fixtures.Auth.start_and_create_google_workspace_provider()
|
||||
provider = Repo.get!(Auth.Provider, provider.id)
|
||||
|
||||
assert fetch_service_account_token(provider) == {:ok, "GOOGLE_0AUTH_ACCESS_TOKEN"}
|
||||
end
|
||||
|
||||
test "returns error when API is not available" do
|
||||
Bypass.open()
|
||||
|> Mocks.GoogleWorkspaceDirectory.mock_token_endpoint()
|
||||
|> Bypass.down()
|
||||
|
||||
{provider, _bypass} = Fixtures.Auth.start_and_create_google_workspace_provider()
|
||||
provider = Repo.get!(Auth.Provider, provider.id)
|
||||
|
||||
assert fetch_service_account_token(provider) ==
|
||||
{:error, %Mint.TransportError{reason: :econnrefused}}
|
||||
end
|
||||
end
|
||||
|
||||
describe "verify_and_update_identity/2" do
|
||||
setup do
|
||||
account = Fixtures.Accounts.create_account()
|
||||
|
||||
@@ -639,7 +639,11 @@ defmodule Domain.AuthTest do
|
||||
|
||||
assert {:error, changeset} = create_provider(account, attrs)
|
||||
refute changeset.valid?
|
||||
assert errors_on(changeset) == %{adapter: ["is invalid"]}
|
||||
|
||||
assert errors_on(changeset) == %{
|
||||
adapter: ["is invalid"],
|
||||
adapter_config: %{service_account_json_key: ["can't be blank"]}
|
||||
}
|
||||
end
|
||||
|
||||
test "creates a provider", %{
|
||||
|
||||
@@ -2,6 +2,38 @@ defmodule Domain.Fixtures.Auth do
|
||||
use Domain.Fixture
|
||||
alias Domain.Auth
|
||||
|
||||
# this key is revoked so don't bother trying to use it
|
||||
@google_workspace_private_key """
|
||||
-----BEGIN PRIVATE KEY-----
|
||||
MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQCZU+IlZMT1ExqS
|
||||
LAi7Fa2bGYiGSFIvbVoOVvu8VZyOAR3Bjfe2TiLzVTc35+D5fYzQftx7sC0ZF6Ub
|
||||
ZSK5mgBi0LcVw8xsMcDhroD0MZdE5E1Lg/tvCdYJCkWFsvCHk8yN40hPgw2lB9Bu
|
||||
xJ4uV5agl8zAkEr+Y8ck0BFY3aK5uyA5McdmakkEUCYRfUaoRCP4y+kR22PJJnYN
|
||||
yYma4nLk6b3OwMs58z5U0N2tmDj8o8zWPSlh4HJgMmOnwtl1EjZ9ZlwjENhzooL2
|
||||
E00gFglm8Lgj34HZp6zhF3bhiCQz0j06puLScXAsLDa5AMf4mBVNsefG59lGZLd4
|
||||
HEaRoxrjAgMBAAECggEAHdiDO84qvJ3UXUGvDWPB4GAPADyRquO5VPM/m0B68fVr
|
||||
qmKNJnJ9QSqETiCX3VjAEVGwb28yyCCfJf8AzGoayyFfkiAD6cehiQyj02TX0jQy
|
||||
i5GMXufmPuo98DGNuoZdmfz09W1IOaiUvQsO02x/SJFj7NPplS0s9ZB+3/J8m3Rx
|
||||
OmYzWg27zV5yITSE4N5FVfK7zfOHzFSdo+yXULRS8ZfzdQeQBFqlnWYSMe9P3QlG
|
||||
kJDyB0JULGcUfpcKQfcI//AMSFjhNn5CngYCU4Qedsm04PmbQr73qMZdbmzLw1Nq
|
||||
NToSwc9SsH2rUBjwffdUK8JNE2wY8JVF96pqX3C8QQKBgQDNQ2o5HZmI2vGqVG0G
|
||||
8/cDVDoJuEjgVPuYAeCHjfjXKR/AKanUTu0Pv/Q45K419T4IdMbOcqr4TvkDHsgZ
|
||||
qQ7Uus+soDz6kY5oyYL43NBS1XAeTmjBkyKT4+k3goUg1+rPyKEATT3dXwT377CS
|
||||
CC3HQE4mZ4RFhEocxku0l/M1oQKBgQC/Ohm3f2/tod5xeJSXfdC0mHKcxcTjQiax
|
||||
pYWHbr+YH4GRBTZUNpCMIoYpSjLCoCXcQ5yhxK3K2BEp/5t44OrmfI1o91Xz2XXJ
|
||||
x0A7q27umTRug8J7E3GaoTDutFBUP5C0nJSQgdQaTOAMzZpJqtM27tFJYAHxI2gS
|
||||
0cEeFsM6AwKBgH/r0qhTvRqgMFnRkbzyj++gLyddlPVRoRZjnRV9siYNN/9fN7rb
|
||||
kTvuifpm8fcopodIl5mTtt9XADMknNn5FQgYgFJ57mbODa1aYGhN3Pqyj9QjU3/H
|
||||
/ZWjRPXWPrdwOKNTyprQiIyMqiEGXMk1laoGdm3St4lHX5S9M/MRe33hAoGBAJXi
|
||||
TFXvpSN1RI1cHdu/2d4zv2HyAai/KOUE/+xvee0ahMvOcg7/1byBMvcaGT9Dl2lV
|
||||
9Wc2aaIcSRfKKWpNoNCXv58Ofmhrgk9txYL/lCugGeCllcIyM1EoFtqCqpPeXuWx
|
||||
9SBvInia2OIwJUaohnUAKzp/7gW74s8daWjUHqFRAoGAJ6JJYh749pfDYB4LKwia
|
||||
R9Iyld0qDPR6FXY0ZkOWKczHM2OFjhTT5LglNhoso4zavakyIRmWH8y1tiQnSO/m
|
||||
XI2ckSJQwxpnezLFkP2poJaaM4UqbvRFpXAvUOwvMLpbN57WSngm7Gsm6c9dKvZl
|
||||
7aghWWogzrdN9hMNjXRevao=
|
||||
-----END PRIVATE KEY-----
|
||||
"""
|
||||
|
||||
def user_password, do: "Hello w0rld!"
|
||||
def remote_ip, do: {100, 64, 100, 58}
|
||||
def user_agent, do: "iOS/12.5 (iPhone) connlib/1.3.0"
|
||||
@@ -100,7 +132,21 @@ defmodule Domain.Fixtures.Auth do
|
||||
openid_connect_adapter_config(
|
||||
discovery_document_uri:
|
||||
"http://localhost:#{bypass.port}/.well-known/openid-configuration",
|
||||
scope: Domain.Auth.Adapters.GoogleWorkspace.Settings.scope() |> Enum.join(" ")
|
||||
scope: Domain.Auth.Adapters.GoogleWorkspace.Settings.scope() |> Enum.join(" "),
|
||||
service_account_json_key: %{
|
||||
type: "service_account",
|
||||
project_id: "firezone-test",
|
||||
private_key_id: "e1fc5c12b490aaa1602f3de9133551952b749db3",
|
||||
private_key: @google_workspace_private_key,
|
||||
client_email: "firezone-idp-sync@firezone-test-391719.iam.gserviceaccount.com",
|
||||
client_id: "110986447653011314480",
|
||||
auth_uri: "https://accounts.google.com/o/oauth2/auth",
|
||||
token_uri: "https://oauth2.googleapis.com/token",
|
||||
auth_provider_x509_cert_url: "https://www.googleapis.com/oauth2/v1/certs",
|
||||
client_x509_cert_url:
|
||||
"https://www.googleapis.com/robot/v1/metadata/x509/firezone-idp-sync%40firezone-test-111111.iam.gserviceaccount.com",
|
||||
universe_domain: "googleapis.com"
|
||||
}
|
||||
)
|
||||
|
||||
provider =
|
||||
@@ -225,7 +271,21 @@ defmodule Domain.Fixtures.Auth do
|
||||
"access_token" => "OIDC_ACCESS_TOKEN",
|
||||
"refresh_token" => "OIDC_REFRESH_TOKEN",
|
||||
"expires_at" => DateTime.utc_now() |> DateTime.add(1, :day),
|
||||
"claims" => "openid email profile offline_access"
|
||||
"claims" => "openid email profile offline_access",
|
||||
"service_account_json_key" => %{
|
||||
"type" => "service_account",
|
||||
"project_id" => "firezone-test",
|
||||
"private_key_id" => "e1fc5c12b490aaa1602f3de9133551952b749db3",
|
||||
"private_key" => @google_workspace_private_key,
|
||||
"client_email" => "firezone-idp-sync@firezone-test-391719.iam.gserviceaccount.com",
|
||||
"client_id" => "110986447653011314480",
|
||||
"auth_uri" => "https://accounts.google.com/o/oauth2/auth",
|
||||
"token_uri" => "https://oauth2.googleapis.com/token",
|
||||
"auth_provider_x509_cert_url" => "https://www.googleapis.com/oauth2/v1/certs",
|
||||
"client_x509_cert_url" =>
|
||||
"https://www.googleapis.com/robot/v1/metadata/x509/firezone-idp-sync%40firezone-test-111111.iam.gserviceaccount.com",
|
||||
"universe_domain" => "googleapis.com"
|
||||
}
|
||||
}
|
||||
)
|
||||
end
|
||||
|
||||
@@ -7,6 +7,34 @@ defmodule Domain.Mocks.GoogleWorkspaceDirectory do
|
||||
Domain.Config.put_env_override(:domain, GoogleWorkspace.APIClient, config)
|
||||
end
|
||||
|
||||
def override_token_endpoint(url) do
|
||||
config = Domain.Config.fetch_env!(:domain, GoogleWorkspace.APIClient)
|
||||
config = Keyword.put(config, :token_endpoint, url)
|
||||
Domain.Config.put_env_override(:domain, GoogleWorkspace.APIClient, config)
|
||||
end
|
||||
|
||||
def mock_token_endpoint(bypass) do
|
||||
token_endpoint_path = "/token"
|
||||
|
||||
resp = %{
|
||||
"access_token" => "GOOGLE_0AUTH_ACCESS_TOKEN",
|
||||
"expires_in" => 3599,
|
||||
"token_type" => "Bearer"
|
||||
}
|
||||
|
||||
test_pid = self()
|
||||
|
||||
Bypass.stub(bypass, "POST", token_endpoint_path, fn conn ->
|
||||
conn = Plug.Conn.fetch_query_params(conn)
|
||||
send(test_pid, {:bypass_request, conn})
|
||||
Plug.Conn.send_resp(conn, 200, Jason.encode!(resp))
|
||||
end)
|
||||
|
||||
override_token_endpoint("http://localhost:#{bypass.port}/")
|
||||
|
||||
bypass
|
||||
end
|
||||
|
||||
def mock_users_list_endpoint(bypass, users \\ nil) do
|
||||
users_list_endpoint_path = "/admin/directory/v1/users"
|
||||
|
||||
|
||||
@@ -1,5 +1,19 @@
|
||||
defmodule Web.Settings.IdentityProviders.GoogleWorkspace.Components do
|
||||
use Web, :component_library
|
||||
alias Domain.Auth.Adapters.GoogleWorkspace
|
||||
|
||||
def map_provider_form_attrs(attrs) do
|
||||
attrs
|
||||
|> Map.put("adapter", :google_workspace)
|
||||
|> Map.update("adapter_config", %{}, fn adapter_config ->
|
||||
Map.update(adapter_config, "service_account_json_key", nil, fn service_account_json_key ->
|
||||
case Jason.decode(service_account_json_key) do
|
||||
{:ok, map} -> map
|
||||
{:error, _} -> service_account_json_key
|
||||
end
|
||||
end)
|
||||
end)
|
||||
end
|
||||
|
||||
def provider_form(assigns) do
|
||||
~H"""
|
||||
@@ -104,7 +118,7 @@ defmodule Web.Settings.IdentityProviders.GoogleWorkspace.Components do
|
||||
id="oauth-scopes"
|
||||
class="w-full text-xs mb-4 whitespace-pre-line rounded"
|
||||
phx-no-format
|
||||
><%= scopes() %></.code_block>
|
||||
><%= Enum.join(GoogleWorkspace.Settings.scope(), "\n") %></.code_block>
|
||||
<p class="mb-4">
|
||||
Then click <strong>UPDATE</strong>.
|
||||
</p>
|
||||
@@ -152,13 +166,61 @@ defmodule Web.Settings.IdentityProviders.GoogleWorkspace.Components do
|
||||
<p class="mb-4">
|
||||
Click <strong>CREATE</strong>. Copy the <strong>Client ID</strong>
|
||||
and <strong>Client secret</strong>
|
||||
values from the next screen.
|
||||
values from the next screen to the form below.
|
||||
</p>
|
||||
</:content>
|
||||
</.step>
|
||||
|
||||
<.step>
|
||||
<:title>Step 6. Configure Firezone</:title>
|
||||
<:title>Step 6: Create service account with domain-wide delegation</:title>
|
||||
<:content>
|
||||
<p class="mb-4">
|
||||
Go to the
|
||||
<a
|
||||
href="https://console.cloud.google.com/iam-admin/serviceaccounts"
|
||||
target="_blank"
|
||||
class={link_style()}
|
||||
>
|
||||
<strong>Service Accounts</strong>
|
||||
</a>
|
||||
page of the Google Cloud Console and click <strong>Create Service Account</strong>.
|
||||
</p>
|
||||
<p class="mb-4">
|
||||
Click on the created service account, then click the <strong>Keys</strong>
|
||||
tab, <strong>Add Key</strong>
|
||||
and select <strong>Create new key</strong>. Select <strong>JSON</strong>
|
||||
and click <strong>Create</strong>. The contents of the downloaded JSON will be used for the
|
||||
<strong>Service Account JSON Key</strong>
|
||||
field of the form below.
|
||||
</p>
|
||||
<p class="mb-4">
|
||||
Go back to the <strong>Details</strong>
|
||||
tab and copy the <strong>Unique ID</strong>
|
||||
(OAuth 2 Client ID). You will need it for the next step.
|
||||
</p>
|
||||
<p class="mb-4">
|
||||
Sign in to the Google Workspace Admin Console and go to <strong>Security</strong>, then <strong>Access and Data Control</strong>, and then <a
|
||||
href="https://admin.google.com/ac/owl"
|
||||
target="_blank"
|
||||
class={link_style()}
|
||||
><strong>API Controls</strong></a>. Click <strong>Manage Domain Wide Delegation</strong>,
|
||||
<strong>Add new</strong>
|
||||
and paste the <strong>Unique ID</strong>
|
||||
from the previous step to the <strong>Client ID</strong>
|
||||
field and add the following scopes: <.code_block
|
||||
id="oauth-scopes"
|
||||
class="w-full text-xs mb-4 whitespace-pre-line rounded"
|
||||
phx-no-format
|
||||
><%= Enum.join(GoogleWorkspace.Settings.scope(), "\n") %></.code_block>
|
||||
</p>
|
||||
<p class="mb-4">
|
||||
Finally, click <strong>Authorize</strong>.
|
||||
</p>
|
||||
</:content>
|
||||
</.step>
|
||||
|
||||
<.step>
|
||||
<:title>Step 7. Configure Firezone</:title>
|
||||
<:content>
|
||||
<.base_error form={@form} field={:base} />
|
||||
|
||||
@@ -185,7 +247,7 @@ defmodule Web.Settings.IdentityProviders.GoogleWorkspace.Components do
|
||||
required
|
||||
/>
|
||||
<p class="mt-2 text-xs text-neutral-500">
|
||||
The Client ID from the previous step.
|
||||
The Client ID from Step 5.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@@ -197,7 +259,41 @@ defmodule Web.Settings.IdentityProviders.GoogleWorkspace.Components do
|
||||
required
|
||||
/>
|
||||
<p class="mt-2 text-xs text-neutral-500">
|
||||
The Client secret from the previous step.
|
||||
The Client secret from Step 5.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<.input
|
||||
type="textarea"
|
||||
label="Service Account JSON Key"
|
||||
autocomplete="off"
|
||||
field={adapter_config_form[:service_account_json_key]}
|
||||
placeholder='{"type":"service_account","project_id":...}'
|
||||
value={
|
||||
case adapter_config_form[:service_account_json_key].value do
|
||||
nil ->
|
||||
nil
|
||||
|
||||
%Ecto.Changeset{} = changeset ->
|
||||
changeset
|
||||
|> Ecto.Changeset.apply_changes()
|
||||
|> Map.from_struct()
|
||||
|> Jason.encode!()
|
||||
|
||||
%GoogleWorkspace.Settings.GoogleServiceAccountKey{} = struct ->
|
||||
struct
|
||||
|> Map.from_struct()
|
||||
|> Jason.encode!()
|
||||
|
||||
binary when is_binary(binary) ->
|
||||
binary
|
||||
end
|
||||
}
|
||||
required
|
||||
/>
|
||||
<p class="mt-2 text-xs text-neutral-500">
|
||||
The Service Account JSON Key from Step 6.
|
||||
</p>
|
||||
</div>
|
||||
</.inputs_for>
|
||||
@@ -222,15 +318,4 @@ defmodule Web.Settings.IdentityProviders.GoogleWorkspace.Components do
|
||||
</div>
|
||||
"""
|
||||
end
|
||||
|
||||
def scopes do
|
||||
"""
|
||||
openid
|
||||
profile
|
||||
email
|
||||
https://www.googleapis.com/auth/admin.directory.orgunit.readonly
|
||||
https://www.googleapis.com/auth/admin.directory.group.readonly
|
||||
https://www.googleapis.com/auth/admin.directory.user.readonly
|
||||
"""
|
||||
end
|
||||
end
|
||||
|
||||
@@ -44,6 +44,8 @@ defmodule Web.Settings.IdentityProviders.GoogleWorkspace.Edit do
|
||||
end
|
||||
|
||||
def handle_event("change", %{"provider" => attrs}, socket) do
|
||||
attrs = map_provider_form_attrs(attrs)
|
||||
|
||||
changeset =
|
||||
Auth.change_provider(socket.assigns.provider, attrs)
|
||||
|> Map.put(:action, :insert)
|
||||
@@ -52,6 +54,8 @@ defmodule Web.Settings.IdentityProviders.GoogleWorkspace.Edit do
|
||||
end
|
||||
|
||||
def handle_event("submit", %{"provider" => attrs}, socket) do
|
||||
attrs = map_provider_form_attrs(attrs)
|
||||
|
||||
with {:ok, provider} <-
|
||||
Auth.update_provider(socket.assigns.provider, attrs, socket.assigns.subject) do
|
||||
socket =
|
||||
|
||||
@@ -52,7 +52,7 @@ defmodule Web.Settings.IdentityProviders.GoogleWorkspace.New do
|
||||
end
|
||||
|
||||
def handle_event("change", %{"provider" => attrs}, socket) do
|
||||
attrs = Map.put(attrs, "adapter", :google_workspace)
|
||||
attrs = map_provider_form_attrs(attrs)
|
||||
|
||||
changeset =
|
||||
Auth.new_provider(socket.assigns.account, attrs)
|
||||
@@ -65,7 +65,7 @@ defmodule Web.Settings.IdentityProviders.GoogleWorkspace.New do
|
||||
attrs =
|
||||
attrs
|
||||
|> Map.put("id", socket.assigns.id)
|
||||
|> Map.put("adapter", :google_workspace)
|
||||
|> map_provider_form_attrs()
|
||||
# We create provider in a disabled state because we need to write access token for it first
|
||||
|> Map.put("adapter_state", %{status: "pending_access_token"})
|
||||
|> Map.put("disabled_at", DateTime.utc_now())
|
||||
|
||||
@@ -94,6 +94,7 @@ defmodule Web.Live.Settings.IdentityProviders.GoogleWorkspace.Connect do
|
||||
"openid " <>
|
||||
"email " <>
|
||||
"profile " <>
|
||||
"https://www.googleapis.com/auth/admin.directory.customer.readonly " <>
|
||||
"https://www.googleapis.com/auth/admin.directory.orgunit.readonly " <>
|
||||
"https://www.googleapis.com/auth/admin.directory.group.readonly " <>
|
||||
"https://www.googleapis.com/auth/admin.directory.user.readonly",
|
||||
|
||||
@@ -54,6 +54,7 @@ defmodule Web.Live.Settings.IdentityProviders.GoogleWorkspace.EditTest do
|
||||
"provider[adapter_config][_persistent_id]",
|
||||
"provider[adapter_config][client_id]",
|
||||
"provider[adapter_config][client_secret]",
|
||||
"provider[adapter_config][service_account_json_key]",
|
||||
"provider[name]"
|
||||
]
|
||||
end
|
||||
@@ -68,7 +69,23 @@ defmodule Web.Live.Settings.IdentityProviders.GoogleWorkspace.EditTest do
|
||||
|
||||
adapter_config_attrs =
|
||||
Fixtures.Auth.openid_connect_adapter_config(
|
||||
discovery_document_uri: "http://localhost:#{bypass.port}/.well-known/openid-configuration"
|
||||
discovery_document_uri:
|
||||
"http://localhost:#{bypass.port}/.well-known/openid-configuration",
|
||||
service_account_json_key:
|
||||
Jason.encode!(%{
|
||||
"type" => "service_account",
|
||||
"project_id" => "firezone-test",
|
||||
"private_key_id" => "e1fc5c12b490aaa1602f3de9133551952b749db3",
|
||||
"private_key" => "...",
|
||||
"client_email" => "firezone-idp-sync@firezone-test-391719.iam.gserviceaccount.com",
|
||||
"client_id" => "110986447653011314480",
|
||||
"auth_uri" => "https://accounts.google.com/o/oauth2/auth",
|
||||
"token_uri" => "https://oauth2.googleapis.com/token",
|
||||
"auth_provider_x509_cert_url" => "https://www.googleapis.com/oauth2/v1/certs",
|
||||
"client_x509_cert_url" =>
|
||||
"https://www.googleapis.com/robot/v1/metadata/x509/firezone-idp-sync%40firezone-test-111111.iam.gserviceaccount.com",
|
||||
"universe_domain" => "googleapis.com"
|
||||
})
|
||||
)
|
||||
|
||||
adapter_config_attrs =
|
||||
@@ -162,7 +179,8 @@ defmodule Web.Live.Settings.IdentityProviders.GoogleWorkspace.EditTest do
|
||||
validate_change(form, changed_values, fn form, _html ->
|
||||
assert form_validation_errors(form) == %{
|
||||
"provider[name]" => ["should be at most 255 character(s)"],
|
||||
"provider[adapter_config][client_id]" => ["can't be blank"]
|
||||
"provider[adapter_config][client_id]" => ["can't be blank"],
|
||||
"provider[adapter_config][service_account_json_key]" => ["is invalid"]
|
||||
}
|
||||
end)
|
||||
end
|
||||
|
||||
@@ -46,6 +46,7 @@ defmodule Web.Live.Settings.IdentityProviders.GoogleWorkspace.NewTest do
|
||||
"provider[adapter_config][_persistent_id]",
|
||||
"provider[adapter_config][client_id]",
|
||||
"provider[adapter_config][client_secret]",
|
||||
"provider[adapter_config][service_account_json_key]",
|
||||
"provider[name]"
|
||||
]
|
||||
end
|
||||
@@ -59,7 +60,23 @@ defmodule Web.Live.Settings.IdentityProviders.GoogleWorkspace.NewTest do
|
||||
|
||||
adapter_config_attrs =
|
||||
Fixtures.Auth.openid_connect_adapter_config(
|
||||
discovery_document_uri: "http://localhost:#{bypass.port}/.well-known/openid-configuration"
|
||||
discovery_document_uri:
|
||||
"http://localhost:#{bypass.port}/.well-known/openid-configuration",
|
||||
service_account_json_key:
|
||||
Jason.encode!(%{
|
||||
"type" => "service_account",
|
||||
"project_id" => "firezone-test",
|
||||
"private_key_id" => "e1fc5c12b490aaa1602f3de9133551952b749db3",
|
||||
"private_key" => "...",
|
||||
"client_email" => "firezone-idp-sync@firezone-test-391719.iam.gserviceaccount.com",
|
||||
"client_id" => "110986447653011314480",
|
||||
"auth_uri" => "https://accounts.google.com/o/oauth2/auth",
|
||||
"token_uri" => "https://oauth2.googleapis.com/token",
|
||||
"auth_provider_x509_cert_url" => "https://www.googleapis.com/oauth2/v1/certs",
|
||||
"client_x509_cert_url" =>
|
||||
"https://www.googleapis.com/robot/v1/metadata/x509/firezone-idp-sync%40firezone-test-111111.iam.gserviceaccount.com",
|
||||
"universe_domain" => "googleapis.com"
|
||||
})
|
||||
)
|
||||
|
||||
adapter_config_attrs =
|
||||
@@ -154,7 +171,8 @@ defmodule Web.Live.Settings.IdentityProviders.GoogleWorkspace.NewTest do
|
||||
validate_change(form, changed_values, fn form, _html ->
|
||||
assert form_validation_errors(form) == %{
|
||||
"provider[name]" => ["should be at most 255 character(s)"],
|
||||
"provider[adapter_config][client_id]" => ["can't be blank"]
|
||||
"provider[adapter_config][client_id]" => ["can't be blank"],
|
||||
"provider[adapter_config][service_account_json_key]" => ["is invalid"]
|
||||
}
|
||||
end)
|
||||
end
|
||||
|
||||
@@ -45,6 +45,7 @@ config :domain, Domain.Analytics,
|
||||
|
||||
config :domain, Domain.Auth.Adapters.GoogleWorkspace.APIClient,
|
||||
endpoint: "https://admin.googleapis.com",
|
||||
token_endpoint: "https://oauth2.googleapis.com",
|
||||
finch_transport_opts: []
|
||||
|
||||
config :domain, Domain.Auth.Adapters.MicrosoftEntra.APIClient,
|
||||
|
||||
Reference in New Issue
Block a user