Skip to content

Commit 68587cc

Browse files
authored
Merge pull request #190 from mattrent/admin_check
Add Admin auth check on `/subjects` endpoints
2 parents c558550 + 5efb815 commit 68587cc

14 files changed

Lines changed: 398 additions & 16 deletions

File tree

Dockerfile.core

Lines changed: 2 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -14,10 +14,10 @@
1414

1515
# The version of Alpine to use for the final image
1616
# This should match the version of Alpine that the `elixir:1.14-alpine` image uses
17-
ARG ALPINE_VERSION=3.16
17+
ARG ALPINE_VERSION=3.17
1818
ARG SECRET_KEY_BASE
1919

20-
FROM elixir:1.13-alpine AS builder
20+
FROM elixir:1.14-alpine AS builder
2121

2222
ARG MIX_ENV=prod
2323

@@ -34,11 +34,6 @@ RUN apk update && \
3434
mix local.rebar --force && \
3535
mix local.hex --force
3636

37-
# Get Rust
38-
RUN curl https://sh.rustup.rs -sSf | bash -s -- -y
39-
ENV PATH="/root/.cargo/bin:${PATH}"
40-
ENV RUSTFLAGS='--codegen target-feature=-crt-static'
41-
4237
# This copies our app source code into the build container
4338
COPY . .
4439

Dockerfile.worker

Lines changed: 2 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -14,9 +14,9 @@
1414

1515
# The version of Alpine to use for the final image
1616
# This should match the version of Alpine that the `elixir:1.13.4-alpine` image uses
17-
ARG ALPINE_VERSION=3.16
17+
ARG ALPINE_VERSION=3.17
1818

19-
FROM elixir:1.13-alpine AS builder
19+
FROM elixir:1.14-alpine AS builder
2020

2121
# The following are build arguments used to change variable parts of the image.
2222
# The name of your application/release (required)
@@ -35,11 +35,6 @@ RUN apk update && \
3535
mix local.rebar --force && \
3636
mix local.hex --force
3737

38-
# Get Rust
39-
RUN curl https://sh.rustup.rs -sSf | bash -s -- -y
40-
ENV PATH="/root/.cargo/bin:${PATH}"
41-
ENV RUSTFLAGS='--codegen target-feature=-crt-static'
42-
4338
# This copies our app source code into the build container
4439
COPY . .
4540

Lines changed: 135 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,135 @@
1+
# Copyright 2023 Giuseppe De Palma, Matteo Trentin
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
15+
defmodule Core.Domain.Admins do
16+
@moduledoc """
17+
The Admins context.
18+
"""
19+
20+
import Ecto.Query, warn: false
21+
alias Core.SubjectsRepo, as: Repo
22+
23+
alias Core.Schemas.Admin
24+
25+
@doc """
26+
Returns the list of admins.
27+
28+
## Examples
29+
30+
iex> list_admins()
31+
[%Admin{}, ...]
32+
33+
"""
34+
def list_admins do
35+
Repo.all(Admin)
36+
end
37+
38+
@doc """
39+
Gets a single admin.
40+
41+
Raises `Ecto.NoResultsError` if the Admin does not exist.
42+
43+
## Examples
44+
45+
iex> get_admin!(123)
46+
%Admin{}
47+
48+
iex> get_admin!(456)
49+
** (Ecto.NoResultsError)
50+
51+
"""
52+
def get_admin!(id), do: Repo.get!(Admin, id)
53+
54+
@doc """
55+
Gets a single admin by name.
56+
57+
Returns `nil` if the Admin does not exist.
58+
59+
## Examples
60+
61+
iex> get_admin_by_name("name")
62+
%Admin{}
63+
64+
iex> get_admin_by_name("not_found")
65+
nil
66+
"""
67+
def get_admin_by_name(name) do
68+
Repo.get_by(Admin, name: name)
69+
end
70+
71+
@doc """
72+
Creates a admin.
73+
74+
## Examples
75+
76+
iex> create_admin(%{field: value})
77+
{:ok, %Admin{}}
78+
79+
iex> create_admin(%{field: bad_value})
80+
{:error, %Ecto.Changeset{}}
81+
82+
"""
83+
def create_admin(attrs \\ %{}) do
84+
%Admin{}
85+
|> Admin.changeset(attrs)
86+
|> Repo.insert()
87+
end
88+
89+
@doc """
90+
Updates a admin.
91+
92+
## Examples
93+
94+
iex> update_admin(admin, %{field: new_value})
95+
{:ok, %Admin{}}
96+
97+
iex> update_admin(admin, %{field: bad_value})
98+
{:error, %Ecto.Changeset{}}
99+
100+
"""
101+
def update_admin(%Admin{} = admin, attrs) do
102+
admin
103+
|> Admin.changeset(attrs)
104+
|> Repo.update()
105+
end
106+
107+
@doc """
108+
Deletes a admin.
109+
110+
## Examples
111+
112+
iex> delete_admin(admin)
113+
{:ok, %Admin{}}
114+
115+
iex> delete_admin(admin)
116+
{:error, %Ecto.Changeset{}}
117+
118+
"""
119+
def delete_admin(%Admin{} = admin) do
120+
Repo.delete(admin)
121+
end
122+
123+
@doc """
124+
Returns an `%Ecto.Changeset{}` for tracking admin changes.
125+
126+
## Examples
127+
128+
iex> change_admin(admin)
129+
%Ecto.Changeset{data: %Admin{}}
130+
131+
"""
132+
def change_admin(%Admin{} = admin, attrs \\ %{}) do
133+
Admin.changeset(admin, attrs)
134+
end
135+
end
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
# Copyright 2023 Giuseppe De Palma, Matteo Trentin
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
15+
defmodule Core.Schemas.Admin do
16+
@moduledoc """
17+
The Admin schema
18+
"""
19+
use Ecto.Schema
20+
import Ecto.Changeset
21+
22+
schema "admins" do
23+
field(:name, :string)
24+
field(:token, :string, redact: true)
25+
26+
timestamps()
27+
end
28+
29+
@doc false
30+
def changeset(admin, attrs) do
31+
# only allow valid letters, numbers and underscores in the middle
32+
regex = ~r/^[_a-zA-Z0-9]+$/
33+
msg = "must contain only alphanumeric characters and underscores"
34+
35+
admin
36+
|> cast(attrs, [:name, :token])
37+
|> validate_required([:name, :token])
38+
|> validate_format(:name, regex, message: msg)
39+
|> validate_length(:name, min: 1, max: 160)
40+
|> unique_constraint(:name)
41+
end
42+
end
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
# Copyright 2022 Giuseppe De Palma, Matteo Trentin
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
15+
defmodule CoreWeb.Plug.AuthenticateAdmin do
16+
@moduledoc """
17+
Plug to authenticate an admin user by token.
18+
"""
19+
import Plug.Conn
20+
require Logger
21+
22+
alias Core.Domain.Admins
23+
alias CoreWeb.Token
24+
25+
def init(opts) do
26+
opts
27+
end
28+
29+
@spec call(Plug.Conn.t(), any) :: Plug.Conn.t()
30+
def call(conn, _opts) do
31+
with ["Bearer " <> token] <- get_req_header(conn, "authorization"),
32+
{:ok, %{user: name}} <- Token.verify(token),
33+
{:ok, stored_token} <- retrieve_admin(name),
34+
{:ok, %{user: stored_name}} <- Token.verify(stored_token),
35+
true <- stored_token == token && name == stored_name do
36+
assign(conn, :current_user, name)
37+
else
38+
_error ->
39+
conn
40+
|> put_status(:unauthorized)
41+
|> Phoenix.Controller.put_view(CoreWeb.ErrorView)
42+
|> Phoenix.Controller.render(:"401")
43+
|> halt()
44+
end
45+
end
46+
47+
@spec retrieve_admin(String.t()) :: {:ok, any()} | {:error, any()}
48+
defp retrieve_admin(name) do
49+
case Admins.get_admin_by_name(name) do
50+
nil ->
51+
{:error, :admin_not_found}
52+
53+
user ->
54+
{:ok, user.token}
55+
end
56+
end
57+
end

apps/core/lib/core_web/router.ex

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,12 +23,20 @@ defmodule CoreWeb.Router do
2323
plug(CoreWeb.Plug.Authenticate)
2424
end
2525

26+
pipeline :admin do
27+
plug(CoreWeb.Plug.AuthenticateAdmin)
28+
end
29+
2630
# Endpoints without authentication
2731
scope "/v1", CoreWeb do
2832
pipe_through(:api)
2933

3034
# A simple get "/" to health check
3135
get("/", DefaultController, :index)
36+
end
37+
38+
scope "/v1", CoreWeb do
39+
pipe_through([:api, :admin])
3240

3341
get("/admin/subjects", SubjectController, :index)
3442
post("/admin/subjects", SubjectController, :create)

apps/core/lib/core_web/token.ex

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,12 @@ defmodule CoreWeb.Token do
2626
@spec sign(map()) :: binary()
2727
def sign(data) do
2828
Phoenix.Token.sign(CoreWeb.Endpoint, @signing_salt, data)
29+
rescue
30+
# if the ETS table does not exist (i.e. the application is not running),
31+
# we pass the secret key itself as an argument
32+
_ in ArgumentError ->
33+
key_base = System.fetch_env!("SECRET_KEY_BASE")
34+
Phoenix.Token.sign(key_base, @signing_salt, data)
2935
end
3036

3137
@doc """
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
# Copyright 2023 Giuseppe De Palma, Matteo Trentin
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
15+
defmodule Core.SubjectsRepo.Migrations.CreateAdmins do
16+
use Ecto.Migration
17+
18+
def change do
19+
create table(:admins) do
20+
add :name, :string
21+
add :token, :string
22+
23+
timestamps()
24+
end
25+
26+
create unique_index(:admins, [:name])
27+
end
28+
end

apps/core/priv/subjects_repo/seeds/seeds.exs

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,17 @@
2626

2727
alias Core.SubjectsRepo
2828
alias Core.Schemas.Subject
29+
alias Core.Schemas.Admin
2930

3031
signed_token = CoreWeb.Token.sign(%{user: "guest"})
3132
SubjectsRepo.insert!(%Subject{name: "guest", token: signed_token})
33+
34+
admin_token = CoreWeb.Token.sign(%{user: "admin"})
35+
SubjectsRepo.insert!(%Admin{name: "admin", token: admin_token})
36+
37+
38+
file_path = :core |> Application.compile_env!(Core.Seeds) |> Keyword.fetch!(:path)
39+
40+
with :ok <- File.mkdir_p(Path.dirname(file_path)) do
41+
File.write(file_path, "Admin=#{admin_token}\nGuest=#{signed_token}")
42+
end

0 commit comments

Comments
 (0)