refactor(portal): Add email as separate column on auth_identities table (#7472)

Why:

* Currently, when using the API, a user has no way of easily identifying
what identities they are pulling back as the response only includes the
`provider_identifier` which for most of our AuthProviders is an ID for
the IdP and not an email address. Along with that, when adding users to
an OIDC provider within Firezone, there is no check for whether or not
an identity has already been added with a given email address. By
creating a separate email column on the `auth_identities` table, it will
be very straight forward to know whether an email address exists for a
given identity, return it in an API response and allow the admin of a
Firezone account to track users (Identities) by email rather than IdP
identifier.

Fixes #7392
This commit is contained in:
Brian Manifold
2024-12-13 09:26:47 -08:00
committed by GitHub
parent b63061994d
commit f114bc95cd
21 changed files with 415 additions and 45 deletions

View File

@@ -56,6 +56,8 @@ defmodule API.IdentityController do
"provider_identifier_confirmation",
Map.get(params, "provider_identifier")
)
|> maybe_put_email()
|> maybe_put_identifier()
with {:ok, actor} <- Domain.Actors.fetch_actor_by_id(actor_id, subject),
{:ok, provider} <- Auth.fetch_provider_by_id(provider_id, subject),
@@ -140,4 +142,51 @@ defmodule API.IdentityController do
{:provider_check, valid?}
end
defp maybe_put_email(params) do
email =
params["email"]
|> to_string
|> String.trim()
identifier =
params["provider_identifier"]
|> to_string()
|> String.trim()
cond do
Domain.Auth.valid_email?(email) ->
params
Domain.Auth.valid_email?(identifier) ->
Map.put(params, "email", identifier)
true ->
params
end
end
defp maybe_put_identifier(params) do
email =
params["email"]
|> to_string()
|> String.trim()
identifier =
params["provider_identifier"]
|> to_string()
|> String.trim()
cond do
identifier != "" ->
params
Domain.Auth.valid_email?(email) ->
Map.put(params, "provider_identifier", email)
|> Map.put("provider_identifier_confirmation", email)
true ->
params
end
end
end

View File

@@ -24,7 +24,8 @@ defmodule API.IdentityJSON do
id: identity.id,
actor_id: identity.actor_id,
provider_id: identity.provider_id,
provider_identifier: identity.provider_identifier
provider_identifier: identity.provider_identifier,
email: identity.email
}
end
end

View File

@@ -13,14 +13,16 @@ defmodule API.Schemas.Identity do
id: %Schema{type: :string, description: "Identity ID"},
actor_id: %Schema{type: :string, description: "Actor ID"},
provider_id: %Schema{type: :string, description: "Identity Provider ID"},
provider_identifier: %Schema{type: :string, description: "Identifier from Provider"}
provider_identifier: %Schema{type: :string, description: "Identifier from Provider"},
email: %Schema{type: :string, description: "Email"}
},
required: [:id, :actor_id, :provider_id, :provider_identifier],
required: [:id, :actor_id, :provider_id, :provider_identifier, :email],
example: %{
"id" => "42a7f82f-831a-4a9d-8f17-c66c2bb6e205",
"actor_id" => "cdfa97e6-cca1-41db-8fc7-864daedb46df",
"provider_id" => "989f9e96-e348-47ec-ba85-869fcd7adb19",
"provider_identifier" => "foo@bar.com"
"provider_identifier" => "2551705710219359",
"email" => "foo@bar.com"
}
})
end
@@ -40,7 +42,8 @@ defmodule API.Schemas.Identity do
required: [:identity],
example: %{
"identity" => %{
"provider_identifier" => "foo@bar.com"
"provider_identifier" => "2551705710219359",
"email" => "foo@bar.com"
}
}
})
@@ -63,7 +66,8 @@ defmodule API.Schemas.Identity do
"id" => "42a7f82f-831a-4a9d-8f17-c66c2bb6e205",
"actor_id" => "cdfa97e6-cca1-41db-8fc7-864daedb46df",
"provider_id" => "989f9e96-e348-47ec-ba85-869fcd7adb19",
"provider_identifier" => "foo@bar.com"
"provider_identifier" => "2551705710219359",
"email" => "foo@bar.com"
}
}
})
@@ -88,13 +92,15 @@ defmodule API.Schemas.Identity do
"id" => "42a7f82f-831a-4a9d-8f17-c66c2bb6e205",
"actor_id" => "8f44a02b-b8eb-406f-8202-4274bf60ebd0",
"provider_id" => "6472d898-5b98-4c3b-b4b9-d3158c1891be",
"provider_identifier" => "foo@bar.com"
"provider_identifier" => "2551705710219359",
"email" => "foo@bar.com"
},
%{
"id" => "8a70eb96-e74b-4cdc-91b8-48c05ef74d4c",
"actor_id" => "38c92cda-1ddb-45b3-9d1a-7efc375e00c1",
"provider_id" => "04f13eed-4845-47c3-833e-fdd869fab96f",
"provider_identifier" => "baz@bar.com"
"provider_identifier" => "2638957392736483",
"email" => "baz@bar.com"
}
],
"metadata" => %{

View File

@@ -49,7 +49,7 @@ defmodule API.IdentityControllerTest do
assert equal_ids?(data_ids, identity_ids)
end
test "lists resources with limit", %{conn: conn, account: account, actor: actor} do
test "lists identities with limit", %{conn: conn, account: account, actor: actor} do
identities =
for _ <- 1..3, do: Fixtures.Auth.create_identity(%{account: account, actor: actor})
@@ -88,7 +88,70 @@ defmodule API.IdentityControllerTest do
assert json_response(conn, 401) == %{"error" => %{"reason" => "Unauthorized"}}
end
test "returns a single resource", %{conn: conn, account: account, actor: actor} do
test "returns a single identity with populated email field", %{
conn: conn,
account: account,
actor: actor
} do
identity =
Fixtures.Auth.create_identity(%{
account: account,
actor: actor,
provider_identifier: "172836495673",
email: "foo@bar.com"
})
conn =
conn
|> authorize_conn(actor)
|> put_req_header("content-type", "application/json")
|> get("/actors/#{actor.id}/identities/#{identity.id}")
assert json_response(conn, 200) == %{
"data" => %{
"id" => identity.id,
"actor_id" => actor.id,
"provider_id" => identity.provider_id,
"provider_identifier" => identity.provider_identifier,
"email" => identity.email
}
}
end
test "returns a single identity with populated email field from provider_identifier", %{
conn: conn,
account: account,
actor: actor
} do
identity =
Fixtures.Auth.create_identity(%{
account: account,
actor: actor,
provider_identifier: "foo@bar.com"
})
conn =
conn
|> authorize_conn(actor)
|> put_req_header("content-type", "application/json")
|> get("/actors/#{actor.id}/identities/#{identity.id}")
assert json_response(conn, 200) == %{
"data" => %{
"id" => identity.id,
"actor_id" => actor.id,
"provider_id" => identity.provider_id,
"provider_identifier" => identity.provider_identifier,
"email" => identity.provider_identifier
}
}
end
test "returns a single identity with empty email field", %{
conn: conn,
account: account,
actor: actor
} do
identity = Fixtures.Auth.create_identity(%{account: account, actor: actor})
conn =
@@ -102,7 +165,8 @@ defmodule API.IdentityControllerTest do
"id" => identity.id,
"actor_id" => actor.id,
"provider_id" => identity.provider_id,
"provider_identifier" => identity.provider_identifier
"provider_identifier" => identity.provider_identifier,
"email" => nil
}
}
end
@@ -158,7 +222,7 @@ defmodule API.IdentityControllerTest do
assert resp == %{"error" => %{"reason" => "Not Found"}}
end
test "returns error on invalid identity attrs", %{
test "returns error on empty identity attrs", %{
conn: conn,
account: account,
actor: api_actor
@@ -189,7 +253,7 @@ defmodule API.IdentityControllerTest do
}
end
test "creates a resource with valid attrs", %{
test "returns error on invalid identity attrs", %{
conn: conn,
account: account,
actor: api_actor
@@ -199,7 +263,37 @@ defmodule API.IdentityControllerTest do
actor = Fixtures.Actors.create_actor(account: account)
attrs = %{"provider_identifier" => "foo@local"}
attrs = %{email: "foo"}
conn =
conn
|> authorize_conn(api_actor)
|> put_req_header("content-type", "application/json")
|> post("/actors/#{actor.id}/providers/#{oidc_provider.id}/identities",
identity: attrs
)
assert resp = json_response(conn, 422)
assert resp == %{
"error" => %{
"reason" => "Unprocessable Entity",
"validation_errors" => %{"provider_identifier" => ["can't be blank"]}
}
}
end
test "creates an identity with provider_identifier attr only and is not an email address", %{
conn: conn,
account: account,
actor: api_actor
} do
{oidc_provider, _bypass} =
Fixtures.Auth.start_and_create_openid_connect_provider(account: account)
actor = Fixtures.Actors.create_actor(account: account)
attrs = %{"provider_identifier" => "128asdf92qrh9joqwefoiu23"}
conn =
conn
@@ -210,10 +304,116 @@ defmodule API.IdentityControllerTest do
)
assert resp = json_response(conn, 201)
assert resp["data"]["provider_identifier"] == attrs["provider_identifier"]
assert resp["data"]["provider_id"] == oidc_provider.id
assert resp["data"]["actor_id"] == actor.id
assert resp["data"]["email"] == nil
end
test "creates an identity with provider_identifier attr only and is an email address", %{
conn: conn,
account: account,
actor: api_actor
} do
{oidc_provider, _bypass} =
Fixtures.Auth.start_and_create_openid_connect_provider(account: account)
actor = Fixtures.Actors.create_actor(account: account)
attrs = %{"provider_identifier" => "foo@localhost.local"}
conn =
conn
|> authorize_conn(api_actor)
|> put_req_header("content-type", "application/json")
|> post("/actors/#{actor.id}/providers/#{oidc_provider.id}/identities",
identity: attrs
)
assert resp = json_response(conn, 201)
assert resp["data"]["provider_identifier"] == attrs["provider_identifier"]
assert resp["data"]["email"] == attrs["provider_identifier"]
end
test "creates an identity with email attr only and populates provider_identifier", %{
conn: conn,
account: account,
actor: api_actor
} do
{oidc_provider, _bypass} =
Fixtures.Auth.start_and_create_openid_connect_provider(account: account)
actor = Fixtures.Actors.create_actor(account: account)
attrs = %{"email" => "foo@localhost.local"}
conn =
conn
|> authorize_conn(api_actor)
|> put_req_header("content-type", "application/json")
|> post("/actors/#{actor.id}/providers/#{oidc_provider.id}/identities",
identity: attrs
)
assert resp = json_response(conn, 201)
assert resp["data"]["provider_identifier"] == attrs["email"]
assert resp["data"]["email"] == attrs["email"]
end
test "creates an identity with provider_identifier attr and email attr being the same value",
%{
conn: conn,
account: account,
actor: api_actor
} do
{oidc_provider, _bypass} =
Fixtures.Auth.start_and_create_openid_connect_provider(account: account)
actor = Fixtures.Actors.create_actor(account: account)
attrs = %{
"provider_identifier" => "foo@localhost.local",
"email" => "foo@localhost.local"
}
conn =
conn
|> authorize_conn(api_actor)
|> put_req_header("content-type", "application/json")
|> post("/actors/#{actor.id}/providers/#{oidc_provider.id}/identities",
identity: attrs
)
assert resp = json_response(conn, 201)
assert resp["data"]["provider_identifier"] == attrs["provider_identifier"]
assert resp["data"]["email"] == attrs["email"]
end
test "creates an identity with provider_identifier attr and email attr being different values",
%{
conn: conn,
account: account,
actor: api_actor
} do
{oidc_provider, _bypass} =
Fixtures.Auth.start_and_create_openid_connect_provider(account: account)
actor = Fixtures.Actors.create_actor(account: account)
attrs = %{
"provider_identifier" => "foo@localhost.local",
"email" => "bar@localhost.local"
}
conn =
conn
|> authorize_conn(api_actor)
|> put_req_header("content-type", "application/json")
|> post("/actors/#{actor.id}/providers/#{oidc_provider.id}/identities",
identity: attrs
)
assert resp = json_response(conn, 201)
assert resp["data"]["provider_identifier"] == attrs["provider_identifier"]
assert resp["data"]["email"] == attrs["email"]
end
end
@@ -224,7 +424,7 @@ defmodule API.IdentityControllerTest do
assert json_response(conn, 401) == %{"error" => %{"reason" => "Unauthorized"}}
end
test "deletes a resource", %{conn: conn, account: account, actor: actor} do
test "deletes an identity", %{conn: conn, account: account, actor: actor} do
identity = Fixtures.Auth.create_identity(%{account: account, actor: actor})
conn =
@@ -238,7 +438,8 @@ defmodule API.IdentityControllerTest do
"id" => identity.id,
"actor_id" => actor.id,
"provider_id" => identity.provider_id,
"provider_identifier" => identity.provider_identifier
"provider_identifier" => identity.provider_identifier,
"email" => nil
}
}