Skip to content

Commit

Permalink
REST API: Token migration (#1149)
Browse files Browse the repository at this point in the history
* Add RevokeToken

* Checkpoint UUID ApiToken

* Passing tests

* github restore cache

* Fix on_delete behavior
  • Loading branch information
jamilbk committed Nov 29, 2022
1 parent 7a1ffd8 commit 6bed6c9
Show file tree
Hide file tree
Showing 15 changed files with 295 additions and 9 deletions.
2 changes: 1 addition & 1 deletion .codespellrc
@@ -1,3 +1,3 @@
[codespell]
skip = ./vendor,./omnibus,*.json,yarn.lock,seeds.exs,./docs/node_modules,./deps,./priv/static,./priv/plts,./**/priv/static,./.git,./docs/build,./_build
skip = ./cover,./vendor,./omnibus,*.json,yarn.lock,seeds.exs,./docs/node_modules,./deps,./priv/static,./priv/plts,./**/priv/static,./.git,./docs/build,./_build
ignore-words-list = keypair,keypairs,iif,statics,wee
2 changes: 1 addition & 1 deletion .pre-commit-config.yaml
Expand Up @@ -61,4 +61,4 @@ repos:
- -b
- master
- --pattern
- '^(?!((chore|feat|feature|bug|fix|build|ci|docs|style|refactor|perf|test|revert)\/[a-zA-Z0-9\-\.\/]+)$).*'
- '^(?!((chore|feat|feature|bug|fix|build|ci|docs|style|refactor|perf|test|revert)\/[@a-zA-Z0-9\-\.\/]+)$).*'
104 changes: 104 additions & 0 deletions apps/fz_http/lib/fz_http/api_tokens.ex
@@ -0,0 +1,104 @@
defmodule FzHttp.ApiTokens do
@moduledoc """
The ApiTokens context.
"""

import Ecto.Query, warn: false
alias FzHttp.Repo

alias FzHttp.ApiTokens.ApiToken

@doc """
Returns the list of api_tokens.
## Examples
iex> list_api_tokens()
[%ApiToken{}, ...]
"""
def list_api_tokens do
Repo.all(ApiToken)
end

@doc """
Gets a single api_token.
Raises `Ecto.NoResultsError` if the Api token does not exist.
## Examples
iex> get_api_token!(123)
%ApiToken{}
iex> get_api_token!(456)
** (Ecto.NoResultsError)
"""
def get_api_token!(id), do: Repo.get!(ApiToken, id)

@doc """
Creates a api_token.
## Examples
iex> create_api_token(%{field: value})
{:ok, %ApiToken{}}
iex> create_api_token(%{field: bad_value})
{:error, %Ecto.Changeset{}}
"""
def create_api_token(attrs \\ %{}) do
%ApiToken{}
|> ApiToken.changeset(attrs)
|> Repo.insert()
end

@doc """
Updates a api_token.
## Examples
iex> update_api_token(api_token, %{field: new_value})
{:ok, %ApiToken{}}
iex> update_api_token(api_token, %{field: bad_value})
{:error, %Ecto.Changeset{}}
"""
def update_api_token(%ApiToken{} = api_token, attrs) do
api_token
|> ApiToken.changeset(attrs)
|> Repo.update()
end

@doc """
Deletes a api_token.
## Examples
iex> delete_api_token(api_token)
{:ok, %ApiToken{}}
iex> delete_api_token(api_token)
{:error, %Ecto.Changeset{}}
"""
def delete_api_token(%ApiToken{} = api_token) do
Repo.delete(api_token)
end

@doc """
Returns an `%Ecto.Changeset{}` for tracking api_token changes.
## Examples
iex> change_api_token(api_token)
%Ecto.Changeset{data: %ApiToken{}}
"""
def change_api_token(%ApiToken{} = api_token, attrs \\ %{}) do
ApiToken.changeset(api_token, attrs)
end
end
31 changes: 31 additions & 0 deletions apps/fz_http/lib/fz_http/api_tokens/api_token.ex
@@ -0,0 +1,31 @@
defmodule FzHttp.ApiTokens.ApiToken do
@moduledoc """
Stores API Token metadata to check for revocation.
"""

use Ecto.Schema
import Ecto.Changeset

alias FzHttp.Users.User

@primary_key {:id, :binary_id, autogenerate: true}

schema "api_tokens" do
field :revoked_at, :utc_datetime_usec

belongs_to :user, User

timestamps(type: :utc_datetime_usec)
end

@doc false
def changeset(api_token, attrs) do
api_token
|> cast(attrs, [
:user_id,
:revoked_at
])
|> validate_required(:user_id)
|> assoc_constraint(:user)
end
end
1 change: 0 additions & 1 deletion apps/fz_http/lib/fz_http/conf/configuration.ex
Expand Up @@ -6,7 +6,6 @@ defmodule FzHttp.Configurations.Configuration do
use Ecto.Schema
import Ecto.Changeset
alias FzHttp.Configurations.Logo

@primary_key {:id, :binary_id, autogenerate: true}

schema "configurations" do
Expand Down
2 changes: 2 additions & 0 deletions apps/fz_http/lib/fz_http/configurations.ex
Expand Up @@ -28,6 +28,8 @@ defmodule FzHttp.Configurations do
config
|> Configuration.changeset(attrs)
|> prepare_changes(fn changeset ->
# XXX: Move OIDC and SAML restart logic in here after SAML PR is merged

for {k, v} <- changeset.changes do
case v do
%Ecto.Changeset{} ->
Expand Down
1 change: 1 addition & 0 deletions apps/fz_http/lib/fz_http/devices/device.ex
Expand Up @@ -123,6 +123,7 @@ defmodule FzHttp.Devices.Device do

defp shared_changeset(changeset) do
changeset
|> assoc_constraint(:user)
|> validate_required([
:user_id,
:name,
Expand Down
1 change: 0 additions & 1 deletion apps/fz_http/lib/fz_http/sites/site.ex
Expand Up @@ -15,7 +15,6 @@ defmodule FzHttp.Sites.Site do
]

@primary_key {:id, :binary_id, autogenerate: true}
@foreign_key_type :binary_id

# Postgres max int size is 4 bytes
@max_pg_integer 2_147_483_647
Expand Down
11 changes: 8 additions & 3 deletions apps/fz_http/lib/fz_http/users/user.ex
Expand Up @@ -11,7 +11,11 @@ defmodule FzHttp.Users.User do
import FzHttp.Users.PasswordHelpers
import FzHttp.Validators.Common, only: [trim: 2]

alias FzHttp.{Devices.Device, OIDC.Connection}
alias FzHttp.{
ApiTokens.ApiToken,
Devices.Device,
OIDC.Connection
}

# Fields for which to trim whitespace after cast, before validation
@whitespace_trimmed_fields :email
Expand All @@ -33,8 +37,9 @@ defmodule FzHttp.Users.User do
field :password_confirmation, :string, virtual: true
field :current_password, :string, virtual: true

has_many :devices, Device, on_delete: :delete_all
has_many :oidc_connections, Connection, on_delete: :delete_all
has_many :devices, Device
has_many :oidc_connections, Connection
has_many :api_tokens, ApiToken

timestamps(type: :utc_datetime_usec)
end
Expand Down
@@ -0,0 +1,16 @@
defmodule FzHttp.Repo.Migrations.CreateApiTokens do
use Ecto.Migration

def change do
create table(:api_tokens, primary_key: false) do
add(:id, :uuid, primary_key: true)
add(:revoked_at, :utc_datetime_usec)
add(:user_id, references(:users, on_delete: :delete_all), null: false)

timestamps(type: :utc_datetime_usec)
end

create(index(:api_tokens, [:revoked_at]))
create(index(:api_tokens, [:user_id]))
end
end
@@ -0,0 +1,33 @@
defmodule FzHttp.Repo.Migrations.UpdateOnDeleteBehavior do
use Ecto.Migration

def change do
drop(constraint(:oidc_connections, "oidc_connections_user_id_fkey"))

alter table(:oidc_connections) do
modify(
:user_id,
references(:users, on_delete: :delete_all),
null: false,
from: {
references(:users, on_delete: :nothing),
null: false
}
)
end

drop(constraint(:mfa_methods, "mfa_methods_user_id_fkey"))

alter table(:mfa_methods) do
modify(
:user_id,
references(:users, on_delete: :delete_all),
null: false,
from: {
references(:users, on_delete: :nothing),
null: false
}
)
end
end
end
63 changes: 63 additions & 0 deletions apps/fz_http/test/fz_http/api_tokens_test.exs
@@ -0,0 +1,63 @@
defmodule FzHttp.ApiTokensTest do
use FzHttp.DataCase

alias FzHttp.ApiTokens

describe "api_tokens" do
alias FzHttp.ApiTokens.ApiToken

import FzHttp.ApiTokensFixtures
import FzHttp.UsersFixtures

@invalid_attrs %{user_id: nil}

test "list_api_tokens/0 returns all api_tokens" do
api_token = api_token_fixture()
assert ApiTokens.list_api_tokens() == [api_token]
end

test "get_api_token!/1 returns the api_token with given id" do
api_token = api_token_fixture()
assert ApiTokens.get_api_token!(api_token.id) == api_token
end

test "create_api_token/1 with valid data creates a api_token" do
valid_attrs = %{
user_id: user().id,
revoked_at: ~U[2022-11-25 04:48:00.000000Z]
}

assert {:ok, %ApiToken{} = api_token} = ApiTokens.create_api_token(valid_attrs)
assert api_token.revoked_at == ~U[2022-11-25 04:48:00.000000Z]
end

test "create_api_token/1 with invalid data returns error changeset" do
assert {:error, %Ecto.Changeset{}} = ApiTokens.create_api_token(@invalid_attrs)
end

test "update_api_token/2 with valid data updates the api_token" do
api_token = api_token_fixture()
update_attrs = %{revoked_at: ~U[2022-11-26 04:48:00.000000Z]}

assert {:ok, %ApiToken{} = api_token} = ApiTokens.update_api_token(api_token, update_attrs)
assert api_token.revoked_at == ~U[2022-11-26 04:48:00.000000Z]
end

test "update_api_token/2 with invalid data returns error changeset" do
api_token = api_token_fixture()
assert {:error, %Ecto.Changeset{}} = ApiTokens.update_api_token(api_token, @invalid_attrs)
assert api_token == ApiTokens.get_api_token!(api_token.id)
end

test "delete_api_token/1 deletes the api_token" do
api_token = api_token_fixture()
assert {:ok, %ApiToken{}} = ApiTokens.delete_api_token(api_token)
assert_raise Ecto.NoResultsError, fn -> ApiTokens.get_api_token!(api_token.id) end
end

test "change_api_token/1 returns a api_token changeset" do
api_token = api_token_fixture()
assert %Ecto.Changeset{} = ApiTokens.change_api_token(api_token)
end
end
end
30 changes: 30 additions & 0 deletions apps/fz_http/test/support/fixtures/api_tokens_fixtures.ex
@@ -0,0 +1,30 @@
defmodule FzHttp.ApiTokensFixtures do
@moduledoc """
This module defines test helpers for creating
entities via the `FzHttp.ApiTokens` context.
"""

@doc """
Generate a api_token.
"""
def api_token_fixture(attrs \\ %{}) do
user_id =
Map.get_lazy(
attrs,
:user_id,
fn ->
FzHttp.UsersFixtures.user().id
end
)

{:ok, api_token} =
attrs
|> Enum.into(%{
user_id: user_id,
revoked_at: ~U[2022-11-25 04:48:00.000000Z]
})
|> FzHttp.ApiTokens.create_api_token()

api_token
end
end
5 changes: 4 additions & 1 deletion apps/fz_http/test/support/fixtures/devices_fixtures.ex
Expand Up @@ -4,7 +4,10 @@ defmodule FzHttp.DevicesFixtures do
entities via the `FzHttp.Devices` context.
"""

alias FzHttp.{Devices, UsersFixtures}
alias FzHttp.{
Devices,
UsersFixtures
}

@doc """
Generate a device.
Expand Down
2 changes: 1 addition & 1 deletion mix.lock
Expand Up @@ -4,7 +4,7 @@
"cachex": {:hex, :cachex, "3.4.0", "868b2959ea4aeb328c6b60ff66c8d5123c083466ad3c33d3d8b5f142e13101fb", [:mix], [{:eternal, "~> 1.2", [hex: :eternal, repo: "hexpm", optional: false]}, {:jumper, "~> 1.0", [hex: :jumper, repo: "hexpm", optional: false]}, {:sleeplocks, "~> 1.1", [hex: :sleeplocks, repo: "hexpm", optional: false]}, {:unsafe, "~> 1.0", [hex: :unsafe, repo: "hexpm", optional: false]}], "hexpm", "370123b1ab4fba4d2965fb18f87fd758325709787c8c5fce35b3fe80645ccbe5"},
"castore": {:hex, :castore, "0.1.19", "a2c3e46d62b7f3aa2e6f88541c21d7400381e53704394462b9fd4f06f6d42bb6", [:mix], [], "hexpm", "e96e0161a5dc82ef441da24d5fa74aefc40d920f3a6645d15e1f9f3e66bb2109"},
"certifi": {:hex, :certifi, "2.9.0", "6f2a475689dd47f19fb74334859d460a2dc4e3252a3324bd2111b8f0429e7e21", [:rebar3], [], "hexpm", "266da46bdb06d6c6d35fde799bcb28d36d985d424ad7c08b5bb48f5b5cdd4641"},
"cidr": {:git, "https://github.com/firezone/cidr-elixir.git", "9072aaab069bca38ef55fd901a37448861596532", []},
"cidr": {:git, "https://github.com/firezone/cidr-elixir.git", "a32125127a7910f476734f45391ba6d37036ee11", []},
"cloak": {:hex, :cloak, "1.1.2", "7e0006c2b0b98d976d4f559080fabefd81f0e0a50a3c4b621f85ceeb563e80bb", [:mix], [{:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}], "hexpm", "940d5ac4fcd51b252930fd112e319ea5ae6ab540b722f3ca60a85666759b9585"},
"cloak_ecto": {:hex, :cloak_ecto, "1.2.0", "e86a3df3bf0dc8980f70406bcb0af2858bac247d55494d40bc58a152590bd402", [:mix], [{:cloak, "~> 1.1.1", [hex: :cloak, repo: "hexpm", optional: false]}, {:ecto, "~> 3.0", [hex: :ecto, repo: "hexpm", optional: false]}], "hexpm", "8bcc677185c813fe64b786618bd6689b1707b35cd95acaae0834557b15a0c62f"},
"combine": {:hex, :combine, "0.10.0", "eff8224eeb56498a2af13011d142c5e7997a80c8f5b97c499f84c841032e429f", [:mix], [], "hexpm", "1b1dbc1790073076580d0d1d64e42eae2366583e7aecd455d1215b0d16f2451b"},
Expand Down

0 comments on commit 6bed6c9

Please sign in to comment.