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

Commit

Permalink
559-endpoint-log-events - Adds middleware for calling analytics event…
Browse files Browse the repository at this point in the history
…s for each endpoint (#622)
  • Loading branch information
eastandwestwind authored Jun 21, 2022
1 parent 44745f1 commit f814913
Show file tree
Hide file tree
Showing 22 changed files with 265 additions and 313 deletions.
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',
'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
- 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.1
multidimensional_urlencode==0.0.4
pandas==1.3.3
passlib[bcrypt]==1.7.4
Expand Down
9 changes: 6 additions & 3 deletions src/fidesops/analytics.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import logging
import os
from platform import system
from typing import Optional

from fideslog.sdk.python.client import AnalyticsClient
from fideslog.sdk.python.event import AnalyticsEvent
Expand All @@ -17,9 +18,11 @@ def in_docker_container() -> bool:
return bool(os.getenv("RUNNING_IN_DOCKER") == "true")


def running_on_local_host() -> bool:
"""For events submitted as a result of making API server requests, `True` if the API server is running on the user's local host. Default: `False`."""
return True
def accessed_through_local_host(hostname: Optional[str]) -> bool:
"""`True`if the event was submitted through a local host, e,g, 127.0.0.1."""
# testserver is hostname in unit tests
LOCAL_HOSTS = ["localhost", "0.0.0.0", "127.0.0.1", "testserver"]
return hostname in LOCAL_HOSTS


analytics_client = AnalyticsClient(
Expand Down
78 changes: 73 additions & 5 deletions src/fidesops/main.py
Original file line number Diff line number Diff line change
@@ -1,16 +1,18 @@
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 (
accessed_through_local_host,
in_docker_container,
running_on_local_host,
send_analytics_event,
)
from fidesops.api.v1.api import api_router
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,73 @@
allow_headers=["*"],
)


@app.middleware("http")
async def dispatch_log_request(request: Request, call_next: Callable) -> Response:
"""
HTTP Middleware that logs analytics events for each call to Fidesops endpoints.
:param request: Request to fidesops api
:param call_next: Callable api endpoint
:return: Response
"""
fides_source: Optional[str] = request.headers.get("X-Fides-Source")
now: datetime = datetime.now(tz=timezone.utc)
endpoint = f"{request.method}: {request.url}"

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,
request.url.hostname,
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, request.url.hostname, 500, now, fides_source, e.__class__.__name__
)
raise


def prepare_and_log_request(
endpoint: str,
hostname: Optional[str],
status_code: int,
event_created_at: datetime,
fides_source: Optional[str],
error_class: Optional[str],
) -> None:
"""
Prepares and sends analytics event provided the user is not opted out of analytics.
"""

# 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=accessed_through_local_host(hostname),
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,9 +153,8 @@ 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
Loading

0 comments on commit f814913

Please sign in to comment.