Skip to content
Closed
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
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ Changes can also be flagged with a GitHub label for tracking purposes. The URL o
- Added sorting to new request manager screen [#7138](https://github.com/ethyca/fides/pull/7138)
- Added DB models for monitor stewards [#7131](https://github.com/ethyca/fides/pull/7131) https://github.com/ethyca/fides/labels/db-migration
- Added ignore and restore actions to schema explorer tree in Action Center [#7156](https://github.com/ethyca/fides/pull/7156)
- Added recommended security headers to Admin UI & Fides API [#7134](https://github.com/ethyca/fides/pull/7134)

### Changed
- Bulk privacy request actions now accept filter sets as well as lists, enables select all functionality. [#7027](https://github.com/ethyca/fides/pull/7027)
Expand All @@ -59,6 +60,7 @@ Changes can also be flagged with a GitHub label for tracking purposes. The URL o
- Fixed incorrect date format string [#7143](https://github.com/ethyca/fides/pull/7143)
- Fixed dsr error toast staying while navigating [#7149](https://github.com/ethyca/fides/pull/7149)
- Fixed repeatedly clicking "Delete" on custom fields causing multiple errors [#7115](https://github.com/ethyca/fides/pull/7115)
- Fixed multi-select tree action counts [7182](https://github.com/ethyca/fides/pull/7182)


## [2.76.1](https://github.com/ethyca/fides/compare/2.76.0..2.76.1)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -113,7 +113,7 @@ export const MonitorTreeDataTitle = ({
onClick: ({ key, domEvent }) => {
domEvent.preventDefault();
domEvent.stopPropagation();
actions[key]?.callback(node.key, [node]);
actions[key]?.callback([node.key], [node]);
},
}}
destroyOnHidden
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,6 @@ import {
} from "./MonitorFields.const";
import MonitorTree, { MonitorTreeRef } from "./MonitorTree";
import { ResourceDetailsDrawer } from "./ResourceDetailsDrawer";
import { collectNodeUrns } from "./treeUtils";
import type { MonitorResource } from "./types";
import { useBulkActions } from "./useBulkActions";
import { useBulkListSelect } from "./useBulkListSelect";
Expand Down Expand Up @@ -123,9 +122,11 @@ const ActionCenterFields: NextPage = () => {
const bulkActions = useBulkActions(monitorId, async (urns: string[]) => {
await monitorTreeRef.current?.refreshResourcesAndAncestors(urns);
});

const fieldActions = useFieldActions(monitorId, async (urns: string[]) => {
await monitorTreeRef.current?.refreshResourcesAndAncestors(urns);
});

const {
listQuery: { nodes: listNodes, ...listQueryMeta },
detailsQuery: { data: resource },
Expand Down Expand Up @@ -273,7 +274,8 @@ const ActionCenterFields: NextPage = () => {
node.status ===
TreeResourceChangeIndicator.REMOVAL) ||
(action === FieldActionType.CLASSIFY &&
node.classifyable) ||
node.classifyable &&
node.diffStatus !== DiffStatus.MUTED) ||
(action === FieldActionType.MUTE &&
node.diffStatus !== DiffStatus.MUTED) ||
(action === FieldActionType.UN_MUTE &&
Expand All @@ -285,9 +287,8 @@ const ActionCenterFields: NextPage = () => {
return true;
})
.some((d) => d === true),
callback: (keys, nodes) => {
const allUrns = collectNodeUrns(nodes);
fieldActions[action](allUrns, false);
callback: (keys) => {
fieldActions[action](keys, false);
},
},
]),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ export interface MonitorFieldParameters {
export type NodeAction<N extends Node> = {
label: string;
/** TODO: should be generically typed * */
callback: (key: Key, nodes: N[]) => void;
callback: (key: Key[], nodes: N[]) => void;
disabled: (nodes: N[]) => boolean;
};

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,21 +7,18 @@ import {
AntSkeleton as Skeleton,
AntSpin as Spin,
Icons,
useMessage,
} from "fidesui";
import React, { useEffect, useMemo } from "react";

import { BulkActionsDropdown } from "~/features/common/BulkActionsDropdown";
import { useSelection } from "~/features/common/hooks/useSelection";
import { ResultsSelectedCount } from "~/features/common/ResultsSelectedCount";
import {
useLazyDownloadPrivacyRequestCsvV2Query,
useSearchPrivacyRequestsQuery,
} from "~/features/privacy-requests/privacy-requests.slice";
import { useSearchPrivacyRequestsQuery } from "~/features/privacy-requests/privacy-requests.slice";
import { PrivacyRequestResponse } from "~/types/api";

import { useAntPagination } from "../../common/pagination/useAntPagination";
import { DuplicateRequestsButton } from "./DuplicateRequestsButton";
import useDownloadPrivacyRequestReport from "./hooks/useDownloadPrivacyRequestReport";
import { usePrivacyRequestBulkActions } from "./hooks/usePrivacyRequestBulkActions";
import usePrivacyRequestsFilters from "./hooks/usePrivacyRequestsFilters";
import { ListItem } from "./list-item/ListItem";
Expand All @@ -34,8 +31,6 @@ export const PrivacyRequestsDashboard = () => {
pagination,
});

const messageApi = useMessage();

const { data, isLoading, isFetching, refetch } =
useSearchPrivacyRequestsQuery({
...filterQueryParams,
Expand Down Expand Up @@ -70,23 +65,8 @@ export const PrivacyRequestsDashboard = () => {
clearSelectedIds();
}, [requests, clearSelectedIds]);

const [downloadReport] = useLazyDownloadPrivacyRequestCsvV2Query();

const handleExport = async () => {
let messageStr;
try {
await downloadReport(filterQueryParams);
} catch (error) {
if (error instanceof Error) {
messageStr = error.message;
} else {
messageStr = "Unknown error occurred";
}
}
if (messageStr) {
messageApi.error(messageStr, 5000);
}
};
const { downloadReport, isDownloadingReport } =
useDownloadPrivacyRequestReport();

const { bulkActionMenuItems } = usePrivacyRequestBulkActions({
requests,
Expand Down Expand Up @@ -143,7 +123,8 @@ export const PrivacyRequestsDashboard = () => {
aria-label="Export report"
data-testid="export-btn"
icon={<Icons.Download />}
onClick={handleExport}
onClick={() => downloadReport(filterQueryParams)}
loading={isDownloadingReport}
/>
</Flex>
</Flex>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import { useMessage } from "fidesui";

import { getErrorMessage } from "~/features/common/helpers";

import {
SearchFilterParams,
useLazyDownloadPrivacyRequestCsvV2Query,
} from "../../privacy-requests.slice";

const useDownloadPrivacyRequestReport = () => {
const messageApi = useMessage();

const [download, { isFetching }] = useLazyDownloadPrivacyRequestCsvV2Query();

const downloadReport = async (args: SearchFilterParams) => {
const result = await download(args);
if (result.isError) {
const message = getErrorMessage(
result.error,
"A problem occurred while generating your privacy request report. Please try again.",
);
messageApi.error(message);
} else {
const a = document.createElement("a");
const csvBlob = new Blob([result.data], { type: "text/csv" });
const csvUrl = window.URL.createObjectURL(csvBlob);
a.href = csvUrl;
a.download = `privacy-request-report.csv`;
a.click();
a.remove();
window.URL.revokeObjectURL(csvUrl);
messageApi.success("Successfully downloaded privacy request report");
}
};

return { downloadReport, isDownloadingReport: isFetching };
};

export default useDownloadPrivacyRequestReport;
Original file line number Diff line number Diff line change
Expand Up @@ -116,7 +116,7 @@ interface DateRangeParams {
to?: string | null;
}

interface SearchFilterParams
export interface SearchFilterParams
extends Partial<PrivacyRequestFilter>,
Partial<DateRangeParams> {}

Expand Down
2 changes: 1 addition & 1 deletion clients/admin-ui/src/features/taxonomy/taxonomy.slice.ts
Original file line number Diff line number Diff line change
Expand Up @@ -128,7 +128,7 @@ const taxonomyApi = baseApi.injectEndpoints({
{ fides_key: string; page?: number; size?: number }
>({
query: ({ fides_key, page, size }) => ({
url: `taxonomies/${fides_key}/history`,
url: `/plus/taxonomies/${fides_key}/history`,
params: {
page,
size,
Expand Down
2 changes: 2 additions & 0 deletions src/fides/api/app_setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@
is_rate_limit_enabled,
)
from fides.api.util.saas_config_updater import update_saas_configs
from fides.api.util.security_headers import SecurityHeadersMiddleware
from fides.config import CONFIG
from fides.config.config_proxy import ConfigProxy

Expand Down Expand Up @@ -87,6 +88,7 @@ def create_fides_app(
fastapi_app.state.limiter = fides_limiter
# Starlette bug causing this to fail mypy
fastapi_app.add_exception_handler(RateLimitExceeded, _rate_limit_exceeded_handler) # type: ignore
fastapi_app.add_middleware(SecurityHeadersMiddleware)
for handler in ExceptionHandlers.get_handlers():
# Starlette bug causing this to fail mypy
fastapi_app.add_exception_handler(RedisNotConfigured, handler) # type: ignore
Expand Down
18 changes: 14 additions & 4 deletions src/fides/api/service/connectors/saas_connector.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
from fides.api.models.connectionconfig import ConnectionConfig, ConnectionTestStatus
from fides.api.models.policy import Policy
from fides.api.models.privacy_request import PrivacyRequest, RequestTask
from fides.api.models.privacy_request.request_task import AsyncTaskType
from fides.api.schemas.consentable_item import (
ConsentableItem,
build_consent_item_hierarchy,
Expand Down Expand Up @@ -277,11 +278,20 @@ def retrieve_data(

# Delegate async requests
with get_db() as db:
# Guard clause to ensure we only run async access requests for access requests
if self.guard_access_request(policy):
if async_dsr_strategy := _get_async_dsr_strategy(
db, request_task, query_config, ActionType.access
if async_dsr_strategy := _get_async_dsr_strategy(
db, request_task, query_config, ActionType.access
):
check_guard_access_request = self.guard_access_request(policy)
# Guard clause only applies to polling requests
# Callback requests should always proceed
if (async_dsr_strategy.type == AsyncTaskType.polling) and (
not check_guard_access_request
):
logger.info(
f"Skipping async access request for policy: {policy.name}"
)
return []
if check_guard_access_request:
return async_dsr_strategy.async_retrieve_data(
client=self.create_client(),
request_task_id=request_task.id,
Expand Down
101 changes: 101 additions & 0 deletions src/fides/api/util/security_headers.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
import re
from dataclasses import dataclass

from fastapi import Request, Response
from starlette.middleware.base import BaseHTTPMiddleware

from fides.config import CONFIG

apply_recommended_headers = CONFIG.security.headers_mode == "recommended"


def is_exact_match(matcher: re.Pattern[str], path_name: str) -> bool:
matched_content = re.fullmatch(matcher, path_name)
return matched_content is not None


HeaderDefinition = tuple[str, str]


@dataclass
class HeaderRule:
matcher: re.Pattern[str]
headers: list[HeaderDefinition]


recommended_csp_header_value = re.sub(
r"\s{2,}",
" ",
"""
default-src 'self';
script-src 'self' 'unsafe-inline';
style-src 'self' 'unsafe-inline';
connect-src 'self';
img-src 'self' blob: data:;
font-src 'self';
object-src 'none';
base-uri 'self';
form-action 'self';
frame-ancestors 'self';
upgrade-insecure-requests;
""",
).strip()

recommended_headers: list[HeaderRule] = [
HeaderRule(
matcher=re.compile(r"/.*"),
headers=[
("X-Content-Type-Options", "nosniff"),
("Strict-Transport-Security", "max-age=31536000"),
],
),
HeaderRule(
matcher=re.compile(r"^/((?!api|health).*)"),
headers=[
(
"Content-Security-Policy",
recommended_csp_header_value,
),
("X-Frame-Options", "SAMEORIGIN"),
],
),
]


def get_applicable_header_rules(
path: str, header_rules: list[HeaderRule]
) -> list[HeaderDefinition]:
header_names: set[str] = set()
header_definitions: list[HeaderDefinition] = []

for rule in header_rules:
if is_exact_match(rule.matcher, path):
for header in rule.headers:
[header_name, _] = header
if header_name not in header_names:
header_names.add(header_name)
header_definitions.append(header)

return header_definitions


def apply_headers_to_response(
headers: list[HeaderRule], request: Request, response: Response
) -> None:
applicable_headers = get_applicable_header_rules(request.url.path, headers)
for [header_name, header_value] in applicable_headers:
response.headers.append(header_name, header_value)


class SecurityHeadersMiddleware(BaseHTTPMiddleware):
"""
Controls what security headers are included in Fides API responses
"""

async def dispatch(self, request: Request, call_next): # type: ignore
response = await call_next(request)

if apply_recommended_headers:
apply_headers_to_response(recommended_headers, request, response)

return response
6 changes: 5 additions & 1 deletion src/fides/config/security_settings.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
"""This module handles finding and parsing fides configuration files."""

# pylint: disable=C0115,C0116, E0213
from typing import List, Optional, Pattern, Tuple, Union
from typing import List, Literal, Optional, Pattern, Tuple, Union

from pydantic import Field, SerializeAsAny, ValidationInfo, field_validator
from pydantic_settings import SettingsConfigDict
Expand Down Expand Up @@ -97,6 +97,10 @@ class SecuritySettings(FidesSettings):
default=None,
description="The header used to determine the client IP address for rate limiting. If not set or set to empty string, rate limiting will be disabled.",
)
headers_mode: Union[Literal["none"], Literal["recommended"]] = Field(
default="none",
description="Controls what security headers are included in Fides server responses.",
)
request_rate_limit: str = Field(
default="2000/minute",
description="The number of requests from a single IP address allowed to hit an endpoint within a rolling 60 second period.",
Expand Down
Loading
Loading