Skip to content

Commit

Permalink
Merge pull request #1669 from UrbanOS-Public/1136
Browse files Browse the repository at this point in the history
Audit Log endpoint
  • Loading branch information
c-m-duncan authored Apr 27, 2023
2 parents ea17417 + 32f5eee commit de769ec
Show file tree
Hide file tree
Showing 8 changed files with 225 additions and 60 deletions.
60 changes: 60 additions & 0 deletions apps/andi/lib/andi_web/controllers/api/audit_log_controller.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
defmodule AndiWeb.API.AuditLogController do
@moduledoc """
Module returns audit logs from postgres.
"""
use AndiWeb, :controller
alias Andi.Schemas.AuditEvents

access_levels(get: [:private])

@generic_error_text "Unsupported request. Only one filter can be used at a time - 'user_id', 'audit_id', 'type', 'event_id.'" <>
"For time, exactly 'start_date' and 'end_date' must be used and formatted in ISO-8601. ex. /start_date=2020-12-31&end-date=2021-01-01"

@spec get(Plug.Conn.t(), any()) :: Plug.Conn.t()
def get(conn, params) do
with {:ok, audit_events} <- get_audit_events(params) do
text(conn, format_events(audit_events))
else
{:error, error} -> respond_error(conn, error)
end
end

defp get_audit_events(params) do
case Map.keys(params) do
["user_id"] -> {:ok, AuditEvents.get_all_for_user(params["user_id"])}
["audit_id"] -> {:ok, AuditEvents.get(params["audit_id"])}
["type"] -> {:ok, AuditEvents.get_all_of_type(params["type"])}
["event_id"] -> {:ok, AuditEvents.get_all_by_event_id(params["event_id"])}
["start_date", "end_date"] -> get_range(params["start_date"], params["end_date"])
["end_date", "start_date"] -> get_range(params["start_date"], params["end_date"])
[] -> {:ok, AuditEvents.get_all()}
_ -> {:error, @generic_error_text}
end
end

defp get_range(start_date, end_date) do
with {:ok, start_struct} <- Date.from_iso8601(start_date),
{:ok, end_struct} <- Date.from_iso8601(end_date) do
{:ok, AuditEvents.get_all_in_range(start_struct, end_struct)}
else
{:error, _error} -> {:error, "Inproperly formatted dates. Use ISO-8601, ex. 2021-01-01"}
end
end

defp format_events(audit_events) do
for event <- audit_events do
trimmed_event =
Map.from_struct(event)
|> Map.delete(:__meta__)
|> Kernel.inspect()

"#{trimmed_event}\n"
end
end

defp respond_error(conn, error) do
conn
|> put_status(:bad_request)
|> text(error)
end
end
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@ defmodule AndiWeb.API.IngestionController do
defp dataset_exists?(id) do
case DatasetStore.get(id) do
{:ok, nil} -> {:ok, false}
{:ok, dataset} -> {:ok, true}
{:ok, _dataset} -> {:ok, true}
{:error, error} -> {:error, error}
end
end
Expand Down
10 changes: 7 additions & 3 deletions apps/andi/lib/andi_web/router.ex
Original file line number Diff line number Diff line change
Expand Up @@ -83,17 +83,21 @@ defmodule AndiWeb.Router do

get "/v1/datasets", DatasetController, :get_all
get "/v1/dataset/:dataset_id", DatasetController, :get
put "/v1/dataset", DatasetController, :create
post "/v1/dataset/disable", DatasetController, :disable
post "/v1/dataset/delete", DatasetController, :delete

get "/v1/ingestions", IngestionController, :get_all
get "/v1/ingestion/:ingestion_id", IngestionController, :get
put "/v1/dataset", DatasetController, :create
put "/v1/ingestion", IngestionController, :create
post "/v1/ingestion/publish", IngestionController, :publish
post "/v1/dataset/disable", DatasetController, :disable
post "/v1/dataset/delete", DatasetController, :delete
post "/v1/ingestion/delete", IngestionController, :delete

get "/v1/organizations", OrganizationController, :get_all
post "/v1/organization/:org_id/users/add", OrganizationController, :add_users_to_organization
post "/v1/organization", OrganizationController, :create

get "/v1/audit", AuditLogController, :get
end

scope "/auth", AndiWeb do
Expand Down
2 changes: 1 addition & 1 deletion apps/andi/mix.exs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ defmodule Andi.MixProject do
def project do
[
app: :andi,
version: "2.6.65",
version: "2.6.66",
build_path: "../../_build",
config_path: "../../config/config.exs",
deps_path: "../../deps",
Expand Down
7 changes: 7 additions & 0 deletions apps/andi/test/integration/andi_web/access_level_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,13 @@ defmodule AndiWeb.AccessLevelTest do
end
end

describe "audit logs" do
test "does not allow access to audit logs without curator role", %{curator_conn: conn} do
assert get(conn, "/audit")
|> response(404)
end
end

defp andi_children() do
Supervisor.which_children(Andi.Supervisor)
|> Enum.map(&elem(&1, 0))
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
defmodule AndiWeb.EditControllerTest do
use ExUnit.Case
use Andi.DataCase
use AndiWeb.Test.AuthConnCase.IntegrationCase

@moduletag shared_data_connection: true

alias Andi.Schemas.AuditEvents
alias SmartCity.TestDataGenerator, as: TDG

@url_path "/api/v1/audit"

describe "Audit Log controller" do
setup do
AuditEvents.log_audit_event(:api, :event_type, %{data: "data"})
end

test "Returns 200 when for users with curator role", %{curator_conn: curator_conn, andi_dataset: andi_dataset} do
conn = get(curator_conn, "#{@url_path}/")

assert response(conn, 200)
assert redirected_to(conn) =~ "data"
end

test "displays error for users without a curator role", %{public_conn: public_conn, andi_dataset: andi_dataset} do
conn = get(public_conn, "#{@url_path}/")
assert response(conn, 403)
end
end
end
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
defmodule AndiWeb.API.AuditLogControllerTest do
use AndiWeb.Test.AuthConnCase.UnitCase
use Placebo

alias Andi.Schemas.AuditEvents
alias Andi.Schemas.AuditEvent
@route "/api/v1/audit"
@error_text "Unsupported request. Only one filter can be used at a time - 'user_id', 'audit_id', 'type', 'event_id.'" <>
"For time, exactly 'start_date' and 'end_date' must be used and formatted in ISO-8601. ex. /start_date=2020-12-31&end-date=2021-01-01"

setup %{} do
logs = [
struct(AuditEvent, %{
id: "id",
user_id: "user_id",
event_type: "event_type",
event: %{
id: "event id",
list: [
deeply_nested_data: %{
deep_data: "deep"
},
list_data: "list"
]
}
}),
struct(AuditEvent, %{
id: "id",
user_id: "user_id",
event_type: "event_type",
event: %{
data: "some data",
other_data: "some other_data"
}
})
]

logs_as_text = """
%{event: %{id: \"event id\", list: [deeply_nested_data: %{deep_data: \"deep\"}, list_data: \"list\"]}, event_type: \"event_type\", id: \"id\", inserted_at: nil, updated_at: nil, user_id: \"user_id\"}
%{event: %{data: \"some data\", other_data: \"some other_data\"}, event_type: \"event_type\", id: \"id\", inserted_at: nil, updated_at: nil, user_id: \"user_id\"}
"""

[logs: logs, logs_as_text: logs_as_text]
end

test "get all audit logs", %{conn: conn, logs: logs, logs_as_text: logs_as_text} do
allow(AuditEvents.get_all(), return: logs)
conn = get(conn, "#{@route}")

assert_called(AuditEvents.get_all())

assert response(conn, 200) =~ logs_as_text
end

test "get by audit id", %{conn: conn, logs: logs, logs_as_text: logs_as_text} do
audit_id = "12345"
allow(AuditEvents.get(audit_id), return: logs)
conn = get(conn, "#{@route}?audit_id=#{audit_id}")

assert_called(AuditEvents.get(audit_id))

assert response(conn, 200) =~ logs_as_text
end

test "get by user id", %{conn: conn, logs: logs, logs_as_text: logs_as_text} do
user_id = "user_id"
allow(AuditEvents.get_all_for_user(user_id), return: logs)
conn = get(conn, "#{@route}?user_id=#{user_id}")

assert_called(AuditEvents.get_all_for_user(user_id))

assert response(conn, 200) =~ logs_as_text
end

test "get by type", %{conn: conn, logs: logs, logs_as_text: logs_as_text} do
type = "some_type"
allow(AuditEvents.get_all_of_type(type), return: logs)
conn = get(conn, "#{@route}?type=#{type}")

assert_called(AuditEvents.get_all_of_type(type))

assert response(conn, 200) =~ logs_as_text
end

test "get by event id", %{conn: conn, logs: logs, logs_as_text: logs_as_text} do
event_id = "54321"
allow(AuditEvents.get_all_by_event_id(event_id), return: logs)
conn = get(conn, "#{@route}?event_id=#{event_id}")

assert_called(AuditEvents.get_all_by_event_id(event_id))

assert response(conn, 200) =~ logs_as_text
end

test "filter by dates", %{conn: conn, logs: logs, logs_as_text: logs_as_text} do
{:ok, start_date_struct} = Date.new(2020, 6, 5)
{:ok, end_date_struct} = Date.new(2020, 10, 9)
allow(AuditEvents.get_all_in_range(start_date_struct, end_date_struct), return: logs)

conn = get(conn, "#{@route}?start_date=2020-06-05&end_date=2020-10-09")

assert_called(AuditEvents.get_all_in_range(start_date_struct, end_date_struct))
assert response(conn, 200) =~ logs_as_text
end

test "returns error when given multiple arguments", %{conn: conn} do
conn = get(conn, "#{@route}?event_id=event&type=type")

assert response(conn, 400) =~ @error_text
end

test "returns error when given unsupported argument", %{conn: conn} do
conn = get(conn, "#{@route}?foobar=foo")

assert response(conn, 400) =~ @error_text
end
end
Original file line number Diff line number Diff line change
Expand Up @@ -39,60 +39,7 @@ defmodule AndiWeb.API.DatasetControllerTest do
meck_options: [:passthrough]
)

uuid = Faker.UUID.v4()

request = %{
"id" => uuid,
"technical" => %{
"dataName" => "dataset",
"orgId" => "org-123-456",
"orgName" => "org",
"stream" => false,
"sourceUrl" => "https://example.com",
"sourceType" => "stream",
"sourceFormat" => "gtfs",
"cadence" => "9000",
"schema" => [%{name: "billy", type: "writer"}],
"private" => false,
"headers" => %{
"accepts" => "application/foobar"
},
"sourceQueryParams" => %{
"apiKey" => "foobar"
},
"systemName" => "org__dataset",
"transformations" => [],
"validations" => []
},
"business" => %{
"benefitRating" => 0.5,
"dataTitle" => "dataset title",
"description" => "description",
"modifiedDate" => "",
"orgTitle" => "org title",
"contactName" => "contact name",
"contactEmail" => "[email protected]",
"license" => "https://www.test.net",
"rights" => "rights information",
"homepage" => "",
"keywords" => [],
"issuedDate" => "2020-01-01T00:00:00Z",
"publishFrequency" => "all day, ey'r day",
"riskRating" => 1.0
},
"_metadata" => %{
"intendedUse" => [],
"expectedBenefit" => []
}
}

message =
request
|> SmartCity.Helpers.to_atom_keys()
|> TDG.create_dataset()
|> struct_to_map_with_string_keys()

{:ok, request: request, message: message, example_datasets: example_datasets}
{:ok, example_datasets: example_datasets}
end

describe "POST /dataset/disable" do
Expand Down Expand Up @@ -225,7 +172,7 @@ defmodule AndiWeb.API.DatasetControllerTest do
end

@tag capture_log: true
test "PUT /api/ creating a dataset with a set id returns a 400", %{conn: conn, example_datasets: example_datasets} do
test "PUT /api/ creating a dataset with a set id returns a 400", %{conn: conn} do
dataset = TDG.create_dataset(%{}) |> struct_to_map_with_string_keys()

allow(Brook.Event.send(@instance_name, any(), any(), any()), return: :ok)
Expand Down

0 comments on commit de769ec

Please sign in to comment.