Packages

Cased provides user and employee activity audit trails to companies that need to monitor access to information or demonstrate regulatory compliance.

Current section

Files

Jump to
cased lib cased.ex
Raw

lib/cased.ex

defmodule Cased do
@moduledoc """
Documentation for Cased.
"""
import Norm
defmodule ConfigurationError do
@moduledoc false
defexception message: "invalid configuration", details: nil
@type t :: %__MODULE__{
message: String.t(),
details: nil | any()
}
def message(exc) do
"#{exc.message}\ndetails #{inspect(exc.details)}"
end
end
defmodule RequestError do
@moduledoc false
defexception message: "invalid request configuration", details: nil
@type t :: %__MODULE__{
message: String.t(),
details: nil | any()
}
def message(exc) do
"#{exc.message}\ndetails #{inspect(exc.details)}"
end
end
defmodule ResponseError do
@moduledoc false
defexception message: "invalid response", details: nil, response: nil
@type t :: %__MODULE__{
message: String.t(),
details: nil | any(),
response: nil | Mojito.response()
}
def message(%{response: nil} = exc) do
"#{exc.message}\ndetails #{inspect(exc.details)}\nstatus code: (none)"
end
def message(exc) do
"#{exc.message}\ndetails #{inspect(exc.details)}\nstatus code: #{exc.status_code}"
end
end
@default_publish_opts [
publishers: [Cased.Publisher.HTTP],
handlers: []
]
@typedoc """
Options when publishing.
- `:publishers`, the list of publisher pids (defaults to `#{
inspect(@default_publish_opts[:publishers])
}`).
- `:handlers`, the list of sensitive data handlers (defaults to `#{
inspect(@default_publish_opts[:handlers])
}`);
see `Cased.Sensitive.Handler`.
"""
@type publish_opts :: [publish_opt()]
@type publish_opt ::
{:publishers, [GenServer.server()]}
| {:handlers, [Cased.Sensitive.Handler.t() | Cased.Sensitive.Handler.spec()]}
@doc """
Publish an audit event to Cased.
Note: Uses `GenServer.call/3` to send events to publisher processes.
```
%{
action: "credit_card.charge",
amount: 2000,
currency: "usd",
source: "tok_amex",
description: "My First Test Charge (created for API docs)",
credit_card_id: "card_1dQpXqQwXxsQs9sohN9HrzRAV6y"
}
|> Cased.publish()
```
"""
@spec publish(audit_event :: map(), opts :: publish_opts()) ::
:ok | {:error, Jason.EncodeError.t() | Exception.t()}
def publish(audit_event, opts \\ []) do
opts =
@default_publish_opts
|> Keyword.merge(opts)
audit_event =
audit_event
|> Map.merge(Cased.Context.to_map())
case validate_publish_opts(opts) do
{:ok, %{publishers: publishers, handlers: handlers}} ->
Cased.Sensitive.Processor.process(audit_event, handlers: handlers)
|> do_publish(publishers)
{:error, details} ->
{:error, %ConfigurationError{details: details}}
end
end
@spec do_publish(data :: term(), publishers :: [GenServer.server()]) ::
:ok | {:error, Jason.EncodeError.t() | Exception.t()}
defp do_publish(data, publishers) do
case Jason.encode(data) do
{:ok, json} ->
for publisher <- publishers do
GenServer.call(publisher, {:publish, json})
end
:ok
other ->
other
end
end
@doc """
Publish an audit event to Cased, raising an exception in the event of failure.
"""
@spec publish!(data :: term(), opts :: publish_opts()) :: :ok | no_return()
def publish!(data, opts \\ []) do
case publish(data, opts) do
:ok ->
:ok
{:error, err} ->
raise err
end
end
defp validate_publish_opts(opts) do
opts
|> Map.new()
|> conform(publish_opts_schema())
end
defp publish_opts_schema() do
schema(%{
# Effectively [GenServer.server()]
publishers: coll_of(spec(is_pid() or is_atom() or is_tuple())),
# TODO: Use is_struct() vs is_map(), post-Elixir v1.10
handlers: coll_of(spec(is_tuple() or is_map()))
})
|> selection()
end
end