Skip to content
This repository has been archived by the owner on Nov 30, 2022. It is now read-only.

559-endpoint-log-events - Adds middleware for calling analytics events for each endpoint #622

Merged
merged 33 commits into from
Jun 21, 2022
Merged
Show file tree
Hide file tree
Changes from 28 commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
2a4dd72
559-endpoint-log-events draft middleware for calling analytics events…
eastandwestwind Jun 6, 2022
aa9dde6
use true source instead of boolean
eastandwestwind Jun 7, 2022
a97f1d0
adds fides source to front end api calls
eastandwestwind Jun 7, 2022
da6e363
remove comment
eastandwestwind Jun 8, 2022
6efda7b
Merge branch 'main' of github.com:ethyca/fidesops into 559-endpoint-l…
eastandwestwind Jun 8, 2022
5bf8937
adds enums
eastandwestwind Jun 9, 2022
f57d6df
adds http middleware, hard-code dev mode in docker-compose
eastandwestwind Jun 13, 2022
3810e85
adds missing source header to login of admin-ui
eastandwestwind Jun 13, 2022
358ad7c
fix conflicts after latest pull
eastandwestwind Jun 14, 2022
70633a9
fix additional front-end admin ui API calls after latest pull
eastandwestwind Jun 14, 2022
4e950c7
pull lates, fix conflicts
eastandwestwind Jun 14, 2022
e385ebd
fix circular imports
eastandwestwind Jun 14, 2022
3d0480d
fix build issues in admin ui
eastandwestwind Jun 14, 2022
d768f13
mypy return type
eastandwestwind Jun 14, 2022
ca8c37e
set dev mode to false in graph test
eastandwestwind Jun 14, 2022
8d2c3e8
pull latest, fix conflicts
eastandwestwind Jun 14, 2022
26ce57e
unintended eslint change
eastandwestwind Jun 14, 2022
1b1af46
adds dev mode false to additional tests
eastandwestwind Jun 15, 2022
4c661c9
turn dev mode on for test_manual_task
eastandwestwind Jun 15, 2022
f3baadf
adds dev mode false to test_execution
eastandwestwind Jun 15, 2022
1cac354
pull latest, fix conflicts
eastandwestwind Jun 15, 2022
8f9d1e2
prettier
eastandwestwind Jun 15, 2022
53f414e
adds unit test for middleware, removes dev mode checks
eastandwestwind Jun 16, 2022
e95ca1d
remove some tests for now, formatting
eastandwestwind Jun 17, 2022
362acd1
pull latest, fix conflicts
eastandwestwind Jun 17, 2022
0397684
bump fideslog version, move main.py test to existing file, add str re…
eastandwestwind Jun 17, 2022
c5e7143
pull latest, fix conflicts
eastandwestwind Jun 17, 2022
0a8f94b
prevents AnalyticsEvent from being called if analytics opt out is true
eastandwestwind Jun 17, 2022
1ec7db7
bump fideslog version, add docstrings, refactor local host method, ad…
eastandwestwind Jun 20, 2022
8adf0e6
format
eastandwestwind Jun 20, 2022
a07880a
isort
eastandwestwind Jun 20, 2022
209c971
unused import
eastandwestwind Jun 20, 2022
8cda76a
hostname is optional type
eastandwestwind Jun 20, 2022
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
3 changes: 2 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,8 @@ The types of changes are:
* Prettier formatting CI check for frontend code [#655](https://github.com/ethyca/fidesops/pull/655)
* Adds default policies [#654](https://github.com/ethyca/fidesops/pull/654)
* Added ConnectionConfig `connection_type` and `disabled` filters [#675](https://github.com/ethyca/fidesops/pull/675)
* Adds Fideslog integration [#541](https://github.com/ethyca/fidesops/pull/541)
* Adds endpoint analytics events [#622](https://github.com/ethyca/fidesops/pull/622)

### Changed

Expand Down Expand Up @@ -143,7 +145,6 @@ The types of changes are:
* Frontend for privacy request denial reaons [#480](https://github.com/ethyca/fidesops/pull/480)
* Publish Fidesops to Pypi [#491](https://github.com/ethyca/fidesops/pull/491)
* DRP data rights endpoint [#526](https://github.com/ethyca/fidesops/pull/526)
* ADDS Fideslog integration [#541](https://github.com/ethyca/fidesops/pull/541)


### Changed
Expand Down
1 change: 1 addition & 0 deletions clients/admin-ui/.eslintrc.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ module.exports = {
// causes bug in re-exporting default exports, see
// https://github.com/eslint/eslint/issues/15617
'no-restricted-exports': [0],
'import/prefer-default-export': 'off',
Copy link
Contributor Author

@eastandwestwind eastandwestwind Jun 14, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

allows us to directly export function here- https://github.com/ethyca/fidesops/pull/622/files#diff-adfcb02b29653b385133da876800f29f50b8c5bc39875695ebdf2324c63525f7R4

We also have other instances throughout admin-ui where we don't use default exports, so I'm unsure why this wasn't caught before.

'react/function-component-definition': [
2,
{
Expand Down
346 changes: 71 additions & 275 deletions clients/admin-ui/package-lock.json

Large diffs are not rendered by default.

11 changes: 4 additions & 7 deletions clients/admin-ui/src/features/auth/auth.slice.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { createApi, fetchBaseQuery } from "@reduxjs/toolkit/query/react";

import type { RootState } from "../../app/store";
import { BASE_API_URN, STORED_CREDENTIALS_KEY } from "../../constants";
import { addCommonHeaders } from "../common/CommonHeaders";
import { User } from "../user-management/types";
import { LoginRequest, LoginResponse } from "./types";

Expand Down Expand Up @@ -71,17 +72,13 @@ credentialStorage.startListening({
});

// Auth API
export const authApi = createApi({
export const authApi: any = createApi({
reducerPath: "authApi",
baseQuery: fetchBaseQuery({
baseUrl: BASE_API_URN,
prepareHeaders: (headers, { getState }) => {
const token = selectToken(getState() as RootState);
headers.set("Access-Control-Allow-Origin", "*");
if (token) {
headers.set("authorization", `Bearer ${token}`);
}
return headers;
const token: string | null = selectToken(getState() as RootState);
return addCommonHeaders(headers, token);
},
}),
tagTypes: ["Auth"],
Expand Down
11 changes: 11 additions & 0 deletions clients/admin-ui/src/features/common/CommonHeaders.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
/**
* Adds common headers to all api calls to fidesops
*/
export function addCommonHeaders(headers: Headers, token: string | null) {
headers.set("Access-Control-Allow-Origin", "*");
headers.set("X-Fides-Source", "fidesops-admin-ui");
if (token) {
headers.set("authorization", `Bearer ${token}`);
}
return headers;
}
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@ const RequestTable: React.FC<RequestTableProps> = () => {
</Tr>
</Thead>
<Tbody>
{requests.map((request) => (
{requests.map((request: any) => (
<RequestRow request={request} key={request.id} />
))}
</Tbody>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { createApi, fetchBaseQuery } from "@reduxjs/toolkit/query/react";
import type { RootState } from "../../app/store";
import { BASE_API_URN } from "../../constants";
import { selectToken } from "../auth";
import { addCommonHeaders } from "../common/CommonHeaders";
import {
DenyPrivacyRequest,
PrivacyRequest,
Expand Down Expand Up @@ -47,17 +48,13 @@ export function mapFiltersToSearchParams({
}

// Subject requests API
export const privacyRequestApi = createApi({
export const privacyRequestApi: any = createApi({
reducerPath: "privacyRequestApi",
baseQuery: fetchBaseQuery({
baseUrl: BASE_API_URN,
prepareHeaders: (headers, { getState }) => {
const token = selectToken(getState() as RootState);
headers.set("Access-Control-Allow-Origin", "*");
if (token) {
headers.set("authorization", `Bearer ${token}`);
}
return headers;
const token: string | null = selectToken(getState() as RootState);
return addCommonHeaders(headers, token);
},
}),
tagTypes: ["Request"],
Expand Down Expand Up @@ -130,6 +127,7 @@ export const requestCSVDownload = async ({
headers: {
"Access-Control-Allow-Origin": "*",
Authorization: `Bearer ${token}`,
"X-Fides-Source": "fidesops-admin-ui",
},
}
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@ const UserManagementTable: React.FC<UsersTableProps> = () => {
</Tr>
</Thead>
<Tbody>
{users?.map((user) => (
{users?.map((user: any) => (
<UserManagementRow user={user} key={user.id} />
))}
</Tbody>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { createApi, fetchBaseQuery } from "@reduxjs/toolkit/query/react";
import type { RootState } from "../../app/store";
import { BASE_API_URN } from "../../constants";
import { selectToken } from "../auth";
import { addCommonHeaders } from "../common/CommonHeaders";
import {
User,
UserPasswordUpdate,
Expand Down Expand Up @@ -76,17 +77,13 @@ export const mapFiltersToSearchParams = ({
...(username ? { username } : {}),
});

export const userApi = createApi({
export const userApi: any = createApi({
reducerPath: "userApi",
baseQuery: fetchBaseQuery({
baseUrl: BASE_API_URN,
prepareHeaders: (headers, { getState }) => {
const token = selectToken(getState() as RootState);
headers.set("Access-Control-Allow-Origin", "*");
if (token) {
headers.set("authorization", `Bearer ${token}`);
}
return headers;
const token: string | null = selectToken(getState() as RootState);
return addCommonHeaders(headers, token);
},
}),
tagTypes: ["User"],
Expand Down
1 change: 1 addition & 0 deletions clients/admin-ui/src/pages/api/auth/[...nextauth].ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export {};
1 change: 1 addition & 0 deletions clients/privacy-center/components/RequestModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,7 @@ const useRequestForm = ({
headers: {
Accept: "application/json",
"Content-Type": "application/json",
"X-Fides-Source": "fidesops-privacy-center",
},
body: JSON.stringify(body),
});
Expand Down
2 changes: 1 addition & 1 deletion docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ services:
read_only: False
- /fidesops/src/fidesops.egg-info
environment:
- FIDESOPS__DEV_MODE=${FIDESOPS__DEV_MODE}
- FIDESOPS__DEV_MODE=True
eastandwestwind marked this conversation as resolved.
Show resolved Hide resolved
- FIDESOPS__LOG_PII=${FIDESOPS__LOG_PII}
- FIDESOPS__HOT_RELOAD=${FIDESOPS__HOT_RELOAD}
- FIDESOPS__ROOT_USER__ANALYTICS_ID=${FIDESOPS__ROOT_USER__ANALYTICS_ID}
Expand Down
2 changes: 1 addition & 1 deletion requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ fastapi-pagination[sqlalchemy]~= 0.8.3
fastapi[all]==0.78.0
fideslang==1.0.0
fideslib==2.0.4
fideslog==1.1.5
fideslog==1.2.0
multidimensional_urlencode==0.0.4
pandas==1.3.3
passlib[bcrypt]==1.7.4
Expand Down
67 changes: 64 additions & 3 deletions src/fidesops/main.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
import logging
from datetime import datetime, timezone
from pathlib import Path
from typing import Callable, Optional

import uvicorn
from fastapi import FastAPI
from fastapi import FastAPI, Request, Response
from fastapi.staticfiles import StaticFiles
from fideslog.sdk.python.event import AnalyticsEvent
from starlette.background import BackgroundTask
from starlette.middleware.cors import CORSMiddleware

from fidesops.analytics import (
Expand All @@ -19,7 +21,7 @@
from fidesops.common_exceptions import FunctionalityNotConfigured
from fidesops.core.config import config
from fidesops.db.database import init_db
from fidesops.schemas.analytics import EVENT
from fidesops.schemas.analytics import Event, ExtraData
from fidesops.tasks.scheduled.scheduler import scheduler
from fidesops.tasks.scheduled.tasks import initiate_scheduled_request_intake
from fidesops.util.logger import get_fides_log_record_factory
Expand All @@ -40,6 +42,65 @@
allow_headers=["*"],
)


@app.middleware("http")
async def dispatch_log_request(request: Request, call_next: Callable) -> Response:
pattisdr marked this conversation as resolved.
Show resolved Hide resolved
pattisdr marked this conversation as resolved.
Show resolved Hide resolved
fides_source: Optional[str] = request.headers.get("X-Fides-Source")
pattisdr marked this conversation as resolved.
Show resolved Hide resolved
now: datetime = datetime.now(tz=timezone.utc)
valid_hostname = request.url.hostname
# only needed until we implement validator fix in fideslog
if valid_hostname == "0.0.0.0":
valid_hostname = "localhost"
endpoint = (
f"{request.method}: {request.url.scheme}://{valid_hostname}{request.url.path}"
)

try:
response = await call_next(request)
# HTTPExceptions are considered a handled err by default so are not thrown here.
# Accepted workaround is to inspect status code of response.
# More context- https://github.com/tiangolo/fastapi/issues/1840
response.background = BackgroundTask(
prepare_and_log_request,
endpoint,
response.status_code,
now,
fides_source,
"HTTPException" if response.status_code >= 400 else None,
)
return response

except Exception as e:
prepare_and_log_request(endpoint, 500, now, fides_source, e.__class__.__name__)
raise


def prepare_and_log_request(
endpoint: str,
status_code: int,
event_created_at: datetime,
fides_source: Optional[str],
error_class: Optional[str],
) -> None:
pattisdr marked this conversation as resolved.
Show resolved Hide resolved
# this check prevents AnalyticsEvent from being called with invalid endpoint during unit tests
if config.root_user.ANALYTICS_OPT_OUT:
return
send_analytics_event(
AnalyticsEvent(
docker=in_docker_container(),
event=Event.endpoint_call.value,
event_created_at=event_created_at,
local_host=running_on_local_host(),
endpoint=endpoint,
status_code=status_code,
error=error_class or None,
extra_data={ExtraData.fides_source.value: fides_source}
if fides_source
else None,
)
)


app.include_router(api_router)
for handler in ExceptionHandlers.get_handlers():
app.add_exception_handler(FunctionalityNotConfigured, handler)
Expand Down Expand Up @@ -84,7 +145,7 @@ def start_webserver() -> None:
send_analytics_event(
AnalyticsEvent(
docker=in_docker_container(),
event=EVENT.server_start.value,
event=Event.server_start.value,
event_created_at=datetime.now(tz=timezone.utc),
local_host=running_on_local_host(),
)
Expand Down
9 changes: 8 additions & 1 deletion src/fidesops/schemas/analytics.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,14 @@
from enum import Enum


class EVENT(str, Enum):
class Event(str, Enum):
"""Enum to hold analytics event names"""

server_start = "server_start"
endpoint_call = "endpoint_call"


class ExtraData(str, Enum):
"""Enum to hold keys for extra data"""

fides_source = "fides_source"
3 changes: 0 additions & 3 deletions src/fidesops/task/graph_task.py
Original file line number Diff line number Diff line change
Expand Up @@ -63,9 +63,6 @@ def result(*args: Any, **kwargs: Any) -> List[Optional[Row]]:
self = args[0]

raised_ex = None
if config.dev_mode:
# If dev mode, return here so exception isn't caught
return func(*args, **kwargs)
for attempt in range(config.execution.TASK_RETRY_COUNT + 1):
try:
self.skip_if_disabled()
Expand Down
6 changes: 6 additions & 0 deletions tests/api/v1/test_main.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,12 @@
from fidesops.api.v1.urn_registry import V1_URL_PREFIX


class CustomTestException(BaseException):
"""Mock Non-HTTP Exception"""

pass


def test_read_autogenerated_docs(api_client: TestClient):
"""Test to ensure automatically generated docs build properly"""
response = api_client.get(f"{V1_URL_PREFIX}/openapi.json")
Expand Down
9 changes: 9 additions & 0 deletions tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -162,3 +162,12 @@ def integration_config() -> MutableMapping[str, Any]:
def celery_enable_logging():
"""Turns on celery output logs."""
return True


@pytest.fixture(autouse=True, scope="session")
def analytics_opt_out():
"""Disable sending analytics when running tests."""
original_value = config.root_user.ANALYTICS_OPT_OUT
config.root_user.ANALYTICS_OPT_OUT = True
yield
config.root_user.ANALYTICS_OPT_OUT = original_value
1 change: 1 addition & 0 deletions tests/integration_tests/test_execution.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
from pydantic import ValidationError
from sqlalchemy.exc import InvalidRequestError

from fidesops.core.config import config
from fidesops.db.session import get_db_session
from fidesops.graph.config import CollectionAddress
from fidesops.graph.graph import DatasetGraph
Expand Down
1 change: 1 addition & 0 deletions tests/integration_tests/test_manual_task.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import pytest

from fidesops.common_exceptions import PrivacyRequestPaused
from fidesops.core.config import config
from fidesops.graph.config import CollectionAddress
from fidesops.models.policy import PausedStep
from fidesops.models.privacy_request import (
Expand Down