Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions .env.sample
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,14 @@ MAIL_FROM_NAME=Claper
# GS_JPG_RESOLUTION=300x300
# LANGUAGES=en,fr,es,it,nl,de

# == Reverse proxy / IP forwarding (set these if Claper runs behind a load balancer or reverse proxy)

# Comma-separated list of trusted proxy IPs or CIDR ranges whose forwarding headers will be trusted
# REMOTE_IP_PROXIES=10.0.0.0/8,172.16.0.0/12,192.168.0.0/16

# Comma-separated list of headers to inspect for the real client IP (replaces the defaults: forwarded, x-forwarded-for, x-client-ip, x-real-ip)
# REMOTE_IP_HEADERS=forwarded,x-forwarded-for,x-client-ip,x-real-ip


# === OIDC configuration ===

Expand Down
2 changes: 2 additions & 0 deletions config/config.exs
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,8 @@ config :porcelain, driver: Porcelain.Driver.Basic

config :claper, :storage_dir, System.get_env("PRESENTATION_STORAGE_DIR", "priv/static")

config :flop, repo: Claper.Repo

# Import environment specific config. This must remain at the bottom
# of this file so it overrides the configuration defined above.
import_config "#{config_env()}.exs"
20 changes: 19 additions & 1 deletion config/runtime.exs
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,22 @@ s3_public_url =
)
)

remote_ip_proxies =
get_var_from_path_or_env(config_dir, "REMOTE_IP_PROXIES", "")
|> String.split(",")
|> Enum.map(&String.trim/1)
|> Enum.reject(&(&1 == ""))

remote_ip_headers =
get_var_from_path_or_env(
config_dir,
"REMOTE_IP_HEADERS",
"forwarded,x-forwarded-for,x-client-ip,x-real-ip"
)
|> String.split(",")
|> Enum.map(&String.trim/1)
|> Enum.reject(&(&1 == ""))

same_site_cookie = get_var_from_path_or_env(config_dir, "SAME_SITE_COOKIE", "Lax")

secure_cookie =
Expand Down Expand Up @@ -192,7 +208,9 @@ config :claper,
email_confirmation: email_confirmation,
allow_unlink_external_provider: allow_unlink_external_provider,
logout_redirect_url: logout_redirect_url,
languages: languages
languages: languages,
remote_ip_proxies: remote_ip_proxies,
remote_ip_headers: remote_ip_headers

config :claper, :presentations,
max_file_size: max_file_size,
Expand Down
125 changes: 125 additions & 0 deletions lib/claper/audit.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
defmodule Claper.Audit do
@moduledoc """
The Audit context for tracking actions in the system.
"""

import Ecto.Query, only: [from: 2]

alias Claper.{Accounts, Repo}
alias Claper.Audit.Log

@doc """
Logs an action.

## Examples

iex> log_action(user, "user.login", %{ip_address: "127.0.0.1"})
{:ok, %Log{}}

iex> log_action(nil, "system.startup", %{})
{:ok, %Log{}}

"""
def log_action(user, action, metadata \\ %{})

def log_action(%Accounts.User{} = user, action, metadata) do
create_log(%{
user_id: user.id,
action: action,
metadata: metadata
})
end

def log_action(nil, action, metadata) do
create_log(%{
action: action,
metadata: metadata
})
end

@doc """
Logs an action related to a specific resource.

## Examples

iex> log_resource_action(user, "event.create", "event", 123, %{})
{:ok, %Log{}}

"""
def log_resource_action(
%Accounts.User{} = user,
action,
resource_type,
resource_id,
metadata \\ %{}
) do
create_log(%{
user_id: user.id,
action: action,
resource_type: resource_type,
resource_id: resource_id,
metadata: metadata
})
end

@doc """
Returns a paginated, optionally filtered and sorted, list of audit logs.
"""
def list_logs(params \\ %{}) do
query = from l in Log, left_join: u in assoc(l, :user), as: :user, preload: [user: u]
Flop.validate_and_run!(query, params, for: Log, replace_invalid_params: true)
end

@doc """
Returns a list of distinct action types for filtering.
"""
def list_action_types do
from(l in Log,
distinct: true,
select: l.action,
order_by: l.action
)
|> Repo.all()
end

@doc """
Gets a single log.

Raises `Ecto.NoResultsError` if the Log does not exist.

## Examples

iex> get_log!(123)
%Log{}

iex> get_log!(456)
** (Ecto.NoResultsError)

"""
def get_log!(id) do
Repo.one!(
from l in Log, left_join: u in assoc(l, :user), where: l.id == ^id, preload: [user: u]
)
end

@doc """
Creates a log entry.

This is a simple wrapper over a low-level insert. You likely want to use
`log_action/3` and `log_resource_action/5` instead.

## Examples

iex> create_log(%{action: "user.login"})
{:ok, %Log{}}

iex> create_log(%{action: nil})
{:error, %Ecto.Changeset{}}

"""
def create_log(attrs \\ %{}) do
%Log{}
|> Log.changeset(attrs)
|> Repo.insert()
end
end
45 changes: 45 additions & 0 deletions lib/claper/audit/log.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
defmodule Claper.Audit.Log do
use Ecto.Schema
import Ecto.Changeset

@derive {
Flop.Schema,
max_limit: 100,
filterable: [:action, :user_email],
sortable: [:inserted_at, :action],
default_order: %{
order_by: [:inserted_at],
order_directions: [:desc]
},
adapter_opts: [
join_fields: [
user_email: [
binding: :user,
field: :email,
path: [:user, :email]
]
]
]
}

schema "audit_logs" do
field :action, :string
field :resource_type, :string
field :resource_id, :integer
field :metadata, :map, default: %{}

belongs_to :user, Claper.Accounts.User

timestamps(updated_at: false)
end

@doc false
def changeset(log, attrs) do
log
|> cast(attrs, [:action, :resource_type, :resource_id, :metadata, :user_id])
|> validate_required([:action])
|> validate_length(:action, max: 255)
|> validate_length(:resource_type, max: 255)
|> assoc_constraint(:user)
end
end
Loading
Loading