Add processing activity exports
Signed-off-by: Sacha Al Himdani <sacha@getprobo.com>
This commit is contained in:
@@ -16,6 +16,10 @@ import {
|
|||||||
useConfirm,
|
useConfirm,
|
||||||
Tabs,
|
Tabs,
|
||||||
TabItem,
|
TabItem,
|
||||||
|
IconArrowDown,
|
||||||
|
IconChevronDown,
|
||||||
|
Spinner,
|
||||||
|
Dropdown,
|
||||||
} from "@probo/ui";
|
} from "@probo/ui";
|
||||||
import { useTranslate } from "@probo/i18n";
|
import { useTranslate } from "@probo/i18n";
|
||||||
import { usePageTitle } from "@probo/hooks";
|
import { usePageTitle } from "@probo/hooks";
|
||||||
@@ -32,7 +36,8 @@ import { useOrganizationId } from "/hooks/useOrganizationId";
|
|||||||
import { useParams } from "react-router";
|
import { useParams } from "react-router";
|
||||||
import { CreateProcessingActivityDialog } from "./dialogs/CreateProcessingActivityDialog";
|
import { CreateProcessingActivityDialog } from "./dialogs/CreateProcessingActivityDialog";
|
||||||
import { deleteProcessingActivityMutation, ProcessingActivitiesConnectionKey, processingActivitiesQuery } from "../../../hooks/graph/ProcessingActivityGraph";
|
import { deleteProcessingActivityMutation, ProcessingActivitiesConnectionKey, processingActivitiesQuery } from "../../../hooks/graph/ProcessingActivityGraph";
|
||||||
import { sprintf, promisifyMutation } from "@probo/helpers";
|
import { sprintf, promisifyMutation, downloadFile, toDateInput } from "@probo/helpers";
|
||||||
|
import { useMutationWithToasts } from "/hooks/useMutationWithToasts";
|
||||||
import { SnapshotBanner } from "/components/SnapshotBanner";
|
import { SnapshotBanner } from "/components/SnapshotBanner";
|
||||||
import type { NodeOf } from "/types";
|
import type { NodeOf } from "/types";
|
||||||
import type {
|
import type {
|
||||||
@@ -174,6 +179,36 @@ const tiaListPageFragment = graphql`
|
|||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
const exportProcessingActivitiesPDFMutation = graphql`
|
||||||
|
mutation ProcessingActivitiesPageExportPDFMutation(
|
||||||
|
$input: ExportProcessingActivitiesPDFInput!
|
||||||
|
) {
|
||||||
|
exportProcessingActivitiesPDF(input: $input) {
|
||||||
|
data
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
const exportDataProtectionImpactAssessmentsPDFMutation = graphql`
|
||||||
|
mutation ProcessingActivitiesPageExportDPIAPDFMutation(
|
||||||
|
$input: ExportDataProtectionImpactAssessmentsPDFInput!
|
||||||
|
) {
|
||||||
|
exportDataProtectionImpactAssessmentsPDF(input: $input) {
|
||||||
|
data
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
const exportTransferImpactAssessmentsPDFMutation = graphql`
|
||||||
|
mutation ProcessingActivitiesPageExportTIAPDFMutation(
|
||||||
|
$input: ExportTransferImpactAssessmentsPDFInput!
|
||||||
|
) {
|
||||||
|
exportTransferImpactAssessmentsPDF(input: $input) {
|
||||||
|
data
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
export default function ProcessingActivitiesPage({ queryRef }: ProcessingActivitiesPageProps) {
|
export default function ProcessingActivitiesPage({ queryRef }: ProcessingActivitiesPageProps) {
|
||||||
const { __ } = useTranslate();
|
const { __ } = useTranslate();
|
||||||
const organizationId = useOrganizationId();
|
const organizationId = useOrganizationId();
|
||||||
@@ -233,12 +268,169 @@ export default function ProcessingActivitiesPage({ queryRef }: ProcessingActivit
|
|||||||
isAuthorized("ProcessingActivity", "deleteProcessingActivity")
|
isAuthorized("ProcessingActivity", "deleteProcessingActivity")
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const canExportPDF = isAuthorized("ProcessingActivity", "exportProcessingActivitiesPDF");
|
||||||
|
const [exportPDF, isExportingPDF] = useMutationWithToasts<{
|
||||||
|
response: {
|
||||||
|
exportProcessingActivitiesPDF?: {
|
||||||
|
data: string;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
variables: {
|
||||||
|
input: {
|
||||||
|
organizationId: string;
|
||||||
|
filter: { snapshotId: string } | null;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}>(
|
||||||
|
exportProcessingActivitiesPDFMutation,
|
||||||
|
{
|
||||||
|
successMessage: __("PDF download started."),
|
||||||
|
errorMessage: __("Failed to generate PDF"),
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleExportPDF = () => {
|
||||||
|
exportPDF({
|
||||||
|
variables: {
|
||||||
|
input: {
|
||||||
|
organizationId: organizationId,
|
||||||
|
filter: snapshotId ? { snapshotId } : null,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
onCompleted: (data) => {
|
||||||
|
if (data.exportProcessingActivitiesPDF?.data) {
|
||||||
|
downloadFile(
|
||||||
|
data.exportProcessingActivitiesPDF.data,
|
||||||
|
`processing-activities-${toDateInput(new Date().toISOString())}.pdf`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const canExportDPIAPDF = isAuthorized("Organization", "exportDataProtectionImpactAssessmentsPDF");
|
||||||
|
const [exportDPIAPDF, isExportingDPIAPDF] = useMutationWithToasts<{
|
||||||
|
response: {
|
||||||
|
exportDataProtectionImpactAssessmentsPDF?: {
|
||||||
|
data: string;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
variables: {
|
||||||
|
input: {
|
||||||
|
organizationId: string;
|
||||||
|
filter: { snapshotId: string } | null;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}>(
|
||||||
|
exportDataProtectionImpactAssessmentsPDFMutation,
|
||||||
|
{
|
||||||
|
successMessage: __("PDF download started."),
|
||||||
|
errorMessage: __("Failed to generate PDF"),
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleExportDPIAPDF = () => {
|
||||||
|
exportDPIAPDF({
|
||||||
|
variables: {
|
||||||
|
input: {
|
||||||
|
organizationId: organizationId,
|
||||||
|
filter: snapshotId ? { snapshotId } : null,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
onCompleted: (data) => {
|
||||||
|
if (data.exportDataProtectionImpactAssessmentsPDF?.data) {
|
||||||
|
downloadFile(
|
||||||
|
data.exportDataProtectionImpactAssessmentsPDF.data,
|
||||||
|
`data-protection-impact-assessments-${toDateInput(new Date().toISOString())}.pdf`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const canExportTIAPDF = isAuthorized("Organization", "exportTransferImpactAssessmentsPDF");
|
||||||
|
const [exportTIAPDF, isExportingTIAPDF] = useMutationWithToasts<{
|
||||||
|
response: {
|
||||||
|
exportTransferImpactAssessmentsPDF?: {
|
||||||
|
data: string;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
variables: {
|
||||||
|
input: {
|
||||||
|
organizationId: string;
|
||||||
|
filter: { snapshotId: string } | null;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}>(
|
||||||
|
exportTransferImpactAssessmentsPDFMutation,
|
||||||
|
{
|
||||||
|
successMessage: __("PDF download started."),
|
||||||
|
errorMessage: __("Failed to generate PDF"),
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleExportTIAPDF = () => {
|
||||||
|
exportTIAPDF({
|
||||||
|
variables: {
|
||||||
|
input: {
|
||||||
|
organizationId: organizationId,
|
||||||
|
filter: snapshotId ? { snapshotId } : null,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
onCompleted: (data) => {
|
||||||
|
if (data.exportTransferImpactAssessmentsPDF?.data) {
|
||||||
|
downloadFile(
|
||||||
|
data.exportTransferImpactAssessmentsPDF.data,
|
||||||
|
`transfer-impact-assessments-${toDateInput(new Date().toISOString())}.pdf`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-6">
|
<div className="space-y-6">
|
||||||
{isSnapshotMode && snapshotId && (
|
{isSnapshotMode && snapshotId && (
|
||||||
<SnapshotBanner snapshotId={snapshotId} />
|
<SnapshotBanner snapshotId={snapshotId} />
|
||||||
)}
|
)}
|
||||||
<PageHeader title={__("Processing Activities")} description={__("Manage your processing activities under GDPR")}>
|
<PageHeader title={__("Processing Activities")} description={__("Manage your processing activities under GDPR")}>
|
||||||
|
{(canExportPDF || canExportDPIAPDF || canExportTIAPDF) && (
|
||||||
|
<Dropdown
|
||||||
|
toggle={
|
||||||
|
<Button variant="secondary" icon={IconArrowDown} iconAfter={IconChevronDown}>
|
||||||
|
{__("Export")}
|
||||||
|
</Button>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{canExportPDF && (
|
||||||
|
<DropdownItem
|
||||||
|
onClick={handleExportPDF}
|
||||||
|
disabled={isExportingPDF}
|
||||||
|
icon={isExportingPDF ? Spinner : undefined}
|
||||||
|
>
|
||||||
|
{__("Processing Activities")}
|
||||||
|
</DropdownItem>
|
||||||
|
)}
|
||||||
|
{canExportDPIAPDF && (
|
||||||
|
<DropdownItem
|
||||||
|
onClick={handleExportDPIAPDF}
|
||||||
|
disabled={isExportingDPIAPDF}
|
||||||
|
icon={isExportingDPIAPDF ? Spinner : undefined}
|
||||||
|
>
|
||||||
|
{__("Data Protection Impact Assessments")}
|
||||||
|
</DropdownItem>
|
||||||
|
)}
|
||||||
|
{canExportTIAPDF && (
|
||||||
|
<DropdownItem
|
||||||
|
onClick={handleExportTIAPDF}
|
||||||
|
disabled={isExportingTIAPDF}
|
||||||
|
icon={isExportingTIAPDF ? Spinner : undefined}
|
||||||
|
>
|
||||||
|
{__("Transfer Impact Assessments")}
|
||||||
|
</DropdownItem>
|
||||||
|
)}
|
||||||
|
</Dropdown>
|
||||||
|
)}
|
||||||
{!isSnapshotMode && activeTab === "activities" && (
|
{!isSnapshotMode && activeTab === "activities" && (
|
||||||
isAuthorized("Organization", "createProcessingActivity") && (
|
isAuthorized("Organization", "createProcessingActivity") && (
|
||||||
<CreateProcessingActivityDialog
|
<CreateProcessingActivityDialog
|
||||||
@@ -272,13 +464,13 @@ export default function ProcessingActivitiesPage({ queryRef }: ProcessingActivit
|
|||||||
<Table>
|
<Table>
|
||||||
<Thead>
|
<Thead>
|
||||||
<Tr>
|
<Tr>
|
||||||
<Th>{__("Name")}</Th>
|
<Th className="px-3">{__("Name")}</Th>
|
||||||
<Th>{__("Purpose")}</Th>
|
<Th className="px-3">{__("Purpose")}</Th>
|
||||||
<Th>{__("Data Subject")}</Th>
|
<Th className="px-3">{__("Data Subject")}</Th>
|
||||||
<Th>{__("Lawful Basis")}</Th>
|
<Th className="px-3">{__("Lawful Basis")}</Th>
|
||||||
<Th>{__("Location")}</Th>
|
<Th className="px-3">{__("Location")}</Th>
|
||||||
<Th>{__("International Transfers")}</Th>
|
<Th className="px-3">{__("International Transfers")}</Th>
|
||||||
{hasAnyAction && <Th>{__("Actions")}</Th>}
|
{hasAnyAction && <Th className="px-3">{__("Actions")}</Th>}
|
||||||
</Tr>
|
</Tr>
|
||||||
</Thead>
|
</Thead>
|
||||||
<Tbody>
|
<Tbody>
|
||||||
|
|||||||
@@ -0,0 +1,96 @@
|
|||||||
|
/**
|
||||||
|
* @generated SignedSource<<2f43f6874d8b6d515cf244934b7b19ea>>
|
||||||
|
* @lightSyntaxTransform
|
||||||
|
* @nogrep
|
||||||
|
*/
|
||||||
|
|
||||||
|
/* tslint:disable */
|
||||||
|
/* eslint-disable */
|
||||||
|
// @ts-nocheck
|
||||||
|
|
||||||
|
import { ConcreteRequest } from 'relay-runtime';
|
||||||
|
export type ExportDataProtectionImpactAssessmentsPDFInput = {
|
||||||
|
filter?: DataProtectionImpactAssessmentFilter | null | undefined;
|
||||||
|
organizationId: string;
|
||||||
|
};
|
||||||
|
export type DataProtectionImpactAssessmentFilter = {
|
||||||
|
snapshotId?: string | null | undefined;
|
||||||
|
};
|
||||||
|
export type ProcessingActivitiesPageExportDPIAPDFMutation$variables = {
|
||||||
|
input: ExportDataProtectionImpactAssessmentsPDFInput;
|
||||||
|
};
|
||||||
|
export type ProcessingActivitiesPageExportDPIAPDFMutation$data = {
|
||||||
|
readonly exportDataProtectionImpactAssessmentsPDF: {
|
||||||
|
readonly data: string;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
export type ProcessingActivitiesPageExportDPIAPDFMutation = {
|
||||||
|
response: ProcessingActivitiesPageExportDPIAPDFMutation$data;
|
||||||
|
variables: ProcessingActivitiesPageExportDPIAPDFMutation$variables;
|
||||||
|
};
|
||||||
|
|
||||||
|
const node: ConcreteRequest = (function(){
|
||||||
|
var v0 = [
|
||||||
|
{
|
||||||
|
"defaultValue": null,
|
||||||
|
"kind": "LocalArgument",
|
||||||
|
"name": "input"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
v1 = [
|
||||||
|
{
|
||||||
|
"alias": null,
|
||||||
|
"args": [
|
||||||
|
{
|
||||||
|
"kind": "Variable",
|
||||||
|
"name": "input",
|
||||||
|
"variableName": "input"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"concreteType": "ExportDataProtectionImpactAssessmentsPDFPayload",
|
||||||
|
"kind": "LinkedField",
|
||||||
|
"name": "exportDataProtectionImpactAssessmentsPDF",
|
||||||
|
"plural": false,
|
||||||
|
"selections": [
|
||||||
|
{
|
||||||
|
"alias": null,
|
||||||
|
"args": null,
|
||||||
|
"kind": "ScalarField",
|
||||||
|
"name": "data",
|
||||||
|
"storageKey": null
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"storageKey": null
|
||||||
|
}
|
||||||
|
];
|
||||||
|
return {
|
||||||
|
"fragment": {
|
||||||
|
"argumentDefinitions": (v0/*: any*/),
|
||||||
|
"kind": "Fragment",
|
||||||
|
"metadata": null,
|
||||||
|
"name": "ProcessingActivitiesPageExportDPIAPDFMutation",
|
||||||
|
"selections": (v1/*: any*/),
|
||||||
|
"type": "Mutation",
|
||||||
|
"abstractKey": null
|
||||||
|
},
|
||||||
|
"kind": "Request",
|
||||||
|
"operation": {
|
||||||
|
"argumentDefinitions": (v0/*: any*/),
|
||||||
|
"kind": "Operation",
|
||||||
|
"name": "ProcessingActivitiesPageExportDPIAPDFMutation",
|
||||||
|
"selections": (v1/*: any*/)
|
||||||
|
},
|
||||||
|
"params": {
|
||||||
|
"cacheID": "851a1c0def7719185cf3a67954696231",
|
||||||
|
"id": null,
|
||||||
|
"metadata": {},
|
||||||
|
"name": "ProcessingActivitiesPageExportDPIAPDFMutation",
|
||||||
|
"operationKind": "mutation",
|
||||||
|
"text": "mutation ProcessingActivitiesPageExportDPIAPDFMutation(\n $input: ExportDataProtectionImpactAssessmentsPDFInput!\n) {\n exportDataProtectionImpactAssessmentsPDF(input: $input) {\n data\n }\n}\n"
|
||||||
|
}
|
||||||
|
};
|
||||||
|
})();
|
||||||
|
|
||||||
|
(node as any).hash = "3c2a96fb29c61791e7df39c7b8174c90";
|
||||||
|
|
||||||
|
export default node;
|
||||||
@@ -0,0 +1,96 @@
|
|||||||
|
/**
|
||||||
|
* @generated SignedSource<<82ece1ab5e9585bfcce605e38fe9563a>>
|
||||||
|
* @lightSyntaxTransform
|
||||||
|
* @nogrep
|
||||||
|
*/
|
||||||
|
|
||||||
|
/* tslint:disable */
|
||||||
|
/* eslint-disable */
|
||||||
|
// @ts-nocheck
|
||||||
|
|
||||||
|
import { ConcreteRequest } from 'relay-runtime';
|
||||||
|
export type ExportProcessingActivitiesPDFInput = {
|
||||||
|
filter?: ProcessingActivityFilter | null | undefined;
|
||||||
|
organizationId: string;
|
||||||
|
};
|
||||||
|
export type ProcessingActivityFilter = {
|
||||||
|
snapshotId?: string | null | undefined;
|
||||||
|
};
|
||||||
|
export type ProcessingActivitiesPageExportPDFMutation$variables = {
|
||||||
|
input: ExportProcessingActivitiesPDFInput;
|
||||||
|
};
|
||||||
|
export type ProcessingActivitiesPageExportPDFMutation$data = {
|
||||||
|
readonly exportProcessingActivitiesPDF: {
|
||||||
|
readonly data: string;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
export type ProcessingActivitiesPageExportPDFMutation = {
|
||||||
|
response: ProcessingActivitiesPageExportPDFMutation$data;
|
||||||
|
variables: ProcessingActivitiesPageExportPDFMutation$variables;
|
||||||
|
};
|
||||||
|
|
||||||
|
const node: ConcreteRequest = (function(){
|
||||||
|
var v0 = [
|
||||||
|
{
|
||||||
|
"defaultValue": null,
|
||||||
|
"kind": "LocalArgument",
|
||||||
|
"name": "input"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
v1 = [
|
||||||
|
{
|
||||||
|
"alias": null,
|
||||||
|
"args": [
|
||||||
|
{
|
||||||
|
"kind": "Variable",
|
||||||
|
"name": "input",
|
||||||
|
"variableName": "input"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"concreteType": "ExportProcessingActivitiesPDFPayload",
|
||||||
|
"kind": "LinkedField",
|
||||||
|
"name": "exportProcessingActivitiesPDF",
|
||||||
|
"plural": false,
|
||||||
|
"selections": [
|
||||||
|
{
|
||||||
|
"alias": null,
|
||||||
|
"args": null,
|
||||||
|
"kind": "ScalarField",
|
||||||
|
"name": "data",
|
||||||
|
"storageKey": null
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"storageKey": null
|
||||||
|
}
|
||||||
|
];
|
||||||
|
return {
|
||||||
|
"fragment": {
|
||||||
|
"argumentDefinitions": (v0/*: any*/),
|
||||||
|
"kind": "Fragment",
|
||||||
|
"metadata": null,
|
||||||
|
"name": "ProcessingActivitiesPageExportPDFMutation",
|
||||||
|
"selections": (v1/*: any*/),
|
||||||
|
"type": "Mutation",
|
||||||
|
"abstractKey": null
|
||||||
|
},
|
||||||
|
"kind": "Request",
|
||||||
|
"operation": {
|
||||||
|
"argumentDefinitions": (v0/*: any*/),
|
||||||
|
"kind": "Operation",
|
||||||
|
"name": "ProcessingActivitiesPageExportPDFMutation",
|
||||||
|
"selections": (v1/*: any*/)
|
||||||
|
},
|
||||||
|
"params": {
|
||||||
|
"cacheID": "85140bef5e55c8ae4fdb139965375f13",
|
||||||
|
"id": null,
|
||||||
|
"metadata": {},
|
||||||
|
"name": "ProcessingActivitiesPageExportPDFMutation",
|
||||||
|
"operationKind": "mutation",
|
||||||
|
"text": "mutation ProcessingActivitiesPageExportPDFMutation(\n $input: ExportProcessingActivitiesPDFInput!\n) {\n exportProcessingActivitiesPDF(input: $input) {\n data\n }\n}\n"
|
||||||
|
}
|
||||||
|
};
|
||||||
|
})();
|
||||||
|
|
||||||
|
(node as any).hash = "cc777bb4440f6aed36c2fb92c1bc7e0b";
|
||||||
|
|
||||||
|
export default node;
|
||||||
@@ -0,0 +1,96 @@
|
|||||||
|
/**
|
||||||
|
* @generated SignedSource<<b8fb35bae12056fe9451875e34aab319>>
|
||||||
|
* @lightSyntaxTransform
|
||||||
|
* @nogrep
|
||||||
|
*/
|
||||||
|
|
||||||
|
/* tslint:disable */
|
||||||
|
/* eslint-disable */
|
||||||
|
// @ts-nocheck
|
||||||
|
|
||||||
|
import { ConcreteRequest } from 'relay-runtime';
|
||||||
|
export type ExportTransferImpactAssessmentsPDFInput = {
|
||||||
|
filter?: TransferImpactAssessmentFilter | null | undefined;
|
||||||
|
organizationId: string;
|
||||||
|
};
|
||||||
|
export type TransferImpactAssessmentFilter = {
|
||||||
|
snapshotId?: string | null | undefined;
|
||||||
|
};
|
||||||
|
export type ProcessingActivitiesPageExportTIAPDFMutation$variables = {
|
||||||
|
input: ExportTransferImpactAssessmentsPDFInput;
|
||||||
|
};
|
||||||
|
export type ProcessingActivitiesPageExportTIAPDFMutation$data = {
|
||||||
|
readonly exportTransferImpactAssessmentsPDF: {
|
||||||
|
readonly data: string;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
export type ProcessingActivitiesPageExportTIAPDFMutation = {
|
||||||
|
response: ProcessingActivitiesPageExportTIAPDFMutation$data;
|
||||||
|
variables: ProcessingActivitiesPageExportTIAPDFMutation$variables;
|
||||||
|
};
|
||||||
|
|
||||||
|
const node: ConcreteRequest = (function(){
|
||||||
|
var v0 = [
|
||||||
|
{
|
||||||
|
"defaultValue": null,
|
||||||
|
"kind": "LocalArgument",
|
||||||
|
"name": "input"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
v1 = [
|
||||||
|
{
|
||||||
|
"alias": null,
|
||||||
|
"args": [
|
||||||
|
{
|
||||||
|
"kind": "Variable",
|
||||||
|
"name": "input",
|
||||||
|
"variableName": "input"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"concreteType": "ExportTransferImpactAssessmentsPDFPayload",
|
||||||
|
"kind": "LinkedField",
|
||||||
|
"name": "exportTransferImpactAssessmentsPDF",
|
||||||
|
"plural": false,
|
||||||
|
"selections": [
|
||||||
|
{
|
||||||
|
"alias": null,
|
||||||
|
"args": null,
|
||||||
|
"kind": "ScalarField",
|
||||||
|
"name": "data",
|
||||||
|
"storageKey": null
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"storageKey": null
|
||||||
|
}
|
||||||
|
];
|
||||||
|
return {
|
||||||
|
"fragment": {
|
||||||
|
"argumentDefinitions": (v0/*: any*/),
|
||||||
|
"kind": "Fragment",
|
||||||
|
"metadata": null,
|
||||||
|
"name": "ProcessingActivitiesPageExportTIAPDFMutation",
|
||||||
|
"selections": (v1/*: any*/),
|
||||||
|
"type": "Mutation",
|
||||||
|
"abstractKey": null
|
||||||
|
},
|
||||||
|
"kind": "Request",
|
||||||
|
"operation": {
|
||||||
|
"argumentDefinitions": (v0/*: any*/),
|
||||||
|
"kind": "Operation",
|
||||||
|
"name": "ProcessingActivitiesPageExportTIAPDFMutation",
|
||||||
|
"selections": (v1/*: any*/)
|
||||||
|
},
|
||||||
|
"params": {
|
||||||
|
"cacheID": "b41ab5f56ee5dc4436069c4e2a97f8d8",
|
||||||
|
"id": null,
|
||||||
|
"metadata": {},
|
||||||
|
"name": "ProcessingActivitiesPageExportTIAPDFMutation",
|
||||||
|
"operationKind": "mutation",
|
||||||
|
"text": "mutation ProcessingActivitiesPageExportTIAPDFMutation(\n $input: ExportTransferImpactAssessmentsPDFInput!\n) {\n exportTransferImpactAssessmentsPDF(input: $input) {\n data\n }\n}\n"
|
||||||
|
}
|
||||||
|
};
|
||||||
|
})();
|
||||||
|
|
||||||
|
(node as any).hash = "3fe289d5564d0617074825d8c58f88f8";
|
||||||
|
|
||||||
|
export default node;
|
||||||
@@ -2596,3 +2596,392 @@ func TestProcessingActivity_Snapshot_DPIA_TIA(t *testing.T) {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestProcessingActivity_ExportPDF(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
owner := testutil.NewClient(t, testutil.RoleOwner)
|
||||||
|
|
||||||
|
t.Run("export processing activities PDF", func(t *testing.T) {
|
||||||
|
_ = factory.NewProcessingActivity(owner).
|
||||||
|
WithName("PA Export Test 1").
|
||||||
|
WithLawfulBasis("CONSENT").
|
||||||
|
Create()
|
||||||
|
_ = factory.NewProcessingActivity(owner).
|
||||||
|
WithName("PA Export Test 2").
|
||||||
|
WithLawfulBasis("LEGITIMATE_INTEREST").
|
||||||
|
Create()
|
||||||
|
|
||||||
|
query := `
|
||||||
|
mutation ExportProcessingActivitiesPDF($input: ExportProcessingActivitiesPDFInput!) {
|
||||||
|
exportProcessingActivitiesPDF(input: $input) {
|
||||||
|
data
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`
|
||||||
|
|
||||||
|
var result struct {
|
||||||
|
ExportProcessingActivitiesPDF struct {
|
||||||
|
Data string `json:"data"`
|
||||||
|
} `json:"exportProcessingActivitiesPDF"`
|
||||||
|
}
|
||||||
|
|
||||||
|
err := owner.Execute(query, map[string]any{
|
||||||
|
"input": map[string]any{
|
||||||
|
"organizationId": owner.GetOrganizationID().String(),
|
||||||
|
"filter": nil,
|
||||||
|
},
|
||||||
|
}, &result)
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.NotEmpty(t, result.ExportProcessingActivitiesPDF.Data)
|
||||||
|
assert.Contains(t, result.ExportProcessingActivitiesPDF.Data, "data:application/pdf;base64,")
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("export processing activities PDF with snapshot filter", func(t *testing.T) {
|
||||||
|
_ = factory.NewProcessingActivity(owner).
|
||||||
|
WithName("PA Snapshot Export Test").
|
||||||
|
Create()
|
||||||
|
|
||||||
|
// Create snapshot
|
||||||
|
var snapshotResult struct {
|
||||||
|
CreateSnapshot struct {
|
||||||
|
SnapshotEdge struct {
|
||||||
|
Node struct {
|
||||||
|
ID string `json:"id"`
|
||||||
|
} `json:"node"`
|
||||||
|
} `json:"snapshotEdge"`
|
||||||
|
} `json:"createSnapshot"`
|
||||||
|
}
|
||||||
|
err := owner.Execute(`
|
||||||
|
mutation($input: CreateSnapshotInput!) {
|
||||||
|
createSnapshot(input: $input) {
|
||||||
|
snapshotEdge {
|
||||||
|
node { id }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`, map[string]any{
|
||||||
|
"input": map[string]any{
|
||||||
|
"organizationId": owner.GetOrganizationID().String(),
|
||||||
|
"name": fmt.Sprintf("PA Export Snapshot Test %d", time.Now().UnixNano()),
|
||||||
|
"type": "PROCESSING_ACTIVITIES",
|
||||||
|
},
|
||||||
|
}, &snapshotResult)
|
||||||
|
require.NoError(t, err)
|
||||||
|
snapshotID := snapshotResult.CreateSnapshot.SnapshotEdge.Node.ID
|
||||||
|
|
||||||
|
query := `
|
||||||
|
mutation ExportProcessingActivitiesPDF($input: ExportProcessingActivitiesPDFInput!) {
|
||||||
|
exportProcessingActivitiesPDF(input: $input) {
|
||||||
|
data
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`
|
||||||
|
|
||||||
|
var result struct {
|
||||||
|
ExportProcessingActivitiesPDF struct {
|
||||||
|
Data string `json:"data"`
|
||||||
|
} `json:"exportProcessingActivitiesPDF"`
|
||||||
|
}
|
||||||
|
|
||||||
|
err = owner.Execute(query, map[string]any{
|
||||||
|
"input": map[string]any{
|
||||||
|
"organizationId": owner.GetOrganizationID().String(),
|
||||||
|
"filter": map[string]any{
|
||||||
|
"snapshotId": snapshotID,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}, &result)
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.NotEmpty(t, result.ExportProcessingActivitiesPDF.Data)
|
||||||
|
assert.Contains(t, result.ExportProcessingActivitiesPDF.Data, "data:application/pdf;base64,")
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("export fails with no processing activities", func(t *testing.T) {
|
||||||
|
newOwner := testutil.NewClient(t, testutil.RoleOwner)
|
||||||
|
|
||||||
|
query := `
|
||||||
|
mutation ExportProcessingActivitiesPDF($input: ExportProcessingActivitiesPDFInput!) {
|
||||||
|
exportProcessingActivitiesPDF(input: $input) {
|
||||||
|
data
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`
|
||||||
|
|
||||||
|
_, err := newOwner.Do(query, map[string]any{
|
||||||
|
"input": map[string]any{
|
||||||
|
"organizationId": newOwner.GetOrganizationID().String(),
|
||||||
|
"filter": nil,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
testutil.RequireErrorCode(t, err, "NOT_FOUND")
|
||||||
|
assert.Contains(t, err.Error(), "no processing activities found")
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDataProtectionImpactAssessment_ExportPDF(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
owner := testutil.NewClient(t, testutil.RoleOwner)
|
||||||
|
|
||||||
|
t.Run("export DPIA PDF", func(t *testing.T) {
|
||||||
|
pa1ID := factory.NewProcessingActivity(owner).
|
||||||
|
WithName("DPIA Export Test PA 1").
|
||||||
|
Create()
|
||||||
|
pa2ID := factory.NewProcessingActivity(owner).
|
||||||
|
WithName("DPIA Export Test PA 2").
|
||||||
|
Create()
|
||||||
|
|
||||||
|
_, err := owner.Do(`
|
||||||
|
mutation($input: CreateDataProtectionImpactAssessmentInput!) {
|
||||||
|
createDataProtectionImpactAssessment(input: $input) {
|
||||||
|
dataProtectionImpactAssessment { id }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`, map[string]any{
|
||||||
|
"input": map[string]any{
|
||||||
|
"processingActivityId": pa1ID,
|
||||||
|
"description": "DPIA 1 description",
|
||||||
|
"residualRisk": "LOW",
|
||||||
|
},
|
||||||
|
})
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
_, err = owner.Do(`
|
||||||
|
mutation($input: CreateDataProtectionImpactAssessmentInput!) {
|
||||||
|
createDataProtectionImpactAssessment(input: $input) {
|
||||||
|
dataProtectionImpactAssessment { id }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`, map[string]any{
|
||||||
|
"input": map[string]any{
|
||||||
|
"processingActivityId": pa2ID,
|
||||||
|
"description": "DPIA 2 description",
|
||||||
|
"residualRisk": "MEDIUM",
|
||||||
|
},
|
||||||
|
})
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
query := `
|
||||||
|
mutation ExportDataProtectionImpactAssessmentsPDF($input: ExportDataProtectionImpactAssessmentsPDFInput!) {
|
||||||
|
exportDataProtectionImpactAssessmentsPDF(input: $input) {
|
||||||
|
data
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`
|
||||||
|
|
||||||
|
var result struct {
|
||||||
|
ExportDataProtectionImpactAssessmentsPDF struct {
|
||||||
|
Data string `json:"data"`
|
||||||
|
} `json:"exportDataProtectionImpactAssessmentsPDF"`
|
||||||
|
}
|
||||||
|
|
||||||
|
err = owner.Execute(query, map[string]any{
|
||||||
|
"input": map[string]any{
|
||||||
|
"organizationId": owner.GetOrganizationID().String(),
|
||||||
|
"filter": nil,
|
||||||
|
},
|
||||||
|
}, &result)
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.NotEmpty(t, result.ExportDataProtectionImpactAssessmentsPDF.Data)
|
||||||
|
assert.Contains(t, result.ExportDataProtectionImpactAssessmentsPDF.Data, "data:application/pdf;base64,")
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("export fails with no DPIAs", func(t *testing.T) {
|
||||||
|
newOwner := testutil.NewClient(t, testutil.RoleOwner)
|
||||||
|
|
||||||
|
query := `
|
||||||
|
mutation ExportDataProtectionImpactAssessmentsPDF($input: ExportDataProtectionImpactAssessmentsPDFInput!) {
|
||||||
|
exportDataProtectionImpactAssessmentsPDF(input: $input) {
|
||||||
|
data
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`
|
||||||
|
|
||||||
|
_, err := newOwner.Do(query, map[string]any{
|
||||||
|
"input": map[string]any{
|
||||||
|
"organizationId": newOwner.GetOrganizationID().String(),
|
||||||
|
"filter": nil,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
testutil.RequireErrorCode(t, err, "NOT_FOUND")
|
||||||
|
assert.Contains(t, err.Error(), "no data protection impact assessments found")
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestTransferImpactAssessment_ExportPDF(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
owner := testutil.NewClient(t, testutil.RoleOwner)
|
||||||
|
|
||||||
|
t.Run("export TIA PDF", func(t *testing.T) {
|
||||||
|
pa1ID := factory.NewProcessingActivity(owner).
|
||||||
|
WithName("TIA Export Test PA 1").
|
||||||
|
Create()
|
||||||
|
pa2ID := factory.NewProcessingActivity(owner).
|
||||||
|
WithName("TIA Export Test PA 2").
|
||||||
|
Create()
|
||||||
|
|
||||||
|
_, err := owner.Do(`
|
||||||
|
mutation($input: CreateTransferImpactAssessmentInput!) {
|
||||||
|
createTransferImpactAssessment(input: $input) {
|
||||||
|
transferImpactAssessment { id }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`, map[string]any{
|
||||||
|
"input": map[string]any{
|
||||||
|
"processingActivityId": pa1ID,
|
||||||
|
"dataSubjects": "TIA 1 subjects",
|
||||||
|
"transfer": "EU to US",
|
||||||
|
},
|
||||||
|
})
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
_, err = owner.Do(`
|
||||||
|
mutation($input: CreateTransferImpactAssessmentInput!) {
|
||||||
|
createTransferImpactAssessment(input: $input) {
|
||||||
|
transferImpactAssessment { id }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`, map[string]any{
|
||||||
|
"input": map[string]any{
|
||||||
|
"processingActivityId": pa2ID,
|
||||||
|
"dataSubjects": "TIA 2 subjects",
|
||||||
|
"transfer": "EU to UK",
|
||||||
|
},
|
||||||
|
})
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
query := `
|
||||||
|
mutation ExportTransferImpactAssessmentsPDF($input: ExportTransferImpactAssessmentsPDFInput!) {
|
||||||
|
exportTransferImpactAssessmentsPDF(input: $input) {
|
||||||
|
data
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`
|
||||||
|
|
||||||
|
var result struct {
|
||||||
|
ExportTransferImpactAssessmentsPDF struct {
|
||||||
|
Data string `json:"data"`
|
||||||
|
} `json:"exportTransferImpactAssessmentsPDF"`
|
||||||
|
}
|
||||||
|
|
||||||
|
err = owner.Execute(query, map[string]any{
|
||||||
|
"input": map[string]any{
|
||||||
|
"organizationId": owner.GetOrganizationID().String(),
|
||||||
|
"filter": nil,
|
||||||
|
},
|
||||||
|
}, &result)
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.NotEmpty(t, result.ExportTransferImpactAssessmentsPDF.Data)
|
||||||
|
assert.Contains(t, result.ExportTransferImpactAssessmentsPDF.Data, "data:application/pdf;base64,")
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("export fails with no TIAs", func(t *testing.T) {
|
||||||
|
newOwner := testutil.NewClient(t, testutil.RoleOwner)
|
||||||
|
|
||||||
|
query := `
|
||||||
|
mutation ExportTransferImpactAssessmentsPDF($input: ExportTransferImpactAssessmentsPDFInput!) {
|
||||||
|
exportTransferImpactAssessmentsPDF(input: $input) {
|
||||||
|
data
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`
|
||||||
|
|
||||||
|
_, err := newOwner.Do(query, map[string]any{
|
||||||
|
"input": map[string]any{
|
||||||
|
"organizationId": newOwner.GetOrganizationID().String(),
|
||||||
|
"filter": nil,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
testutil.RequireErrorCode(t, err, "NOT_FOUND")
|
||||||
|
assert.Contains(t, err.Error(), "no transfer impact assessments found")
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestProcessingActivity_ExportPDF_RBAC(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
t.Run("owner can export PDF", func(t *testing.T) {
|
||||||
|
owner := testutil.NewClient(t, testutil.RoleOwner)
|
||||||
|
_ = factory.NewProcessingActivity(owner).WithName("RBAC Export Test").Create()
|
||||||
|
|
||||||
|
query := `
|
||||||
|
mutation ExportProcessingActivitiesPDF($input: ExportProcessingActivitiesPDFInput!) {
|
||||||
|
exportProcessingActivitiesPDF(input: $input) {
|
||||||
|
data
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`
|
||||||
|
|
||||||
|
var result struct {
|
||||||
|
ExportProcessingActivitiesPDF struct {
|
||||||
|
Data string `json:"data"`
|
||||||
|
} `json:"exportProcessingActivitiesPDF"`
|
||||||
|
}
|
||||||
|
|
||||||
|
err := owner.Execute(query, map[string]any{
|
||||||
|
"input": map[string]any{
|
||||||
|
"organizationId": owner.GetOrganizationID().String(),
|
||||||
|
"filter": nil,
|
||||||
|
},
|
||||||
|
}, &result)
|
||||||
|
require.NoError(t, err, "owner should be able to export PDF")
|
||||||
|
assert.NotEmpty(t, result.ExportProcessingActivitiesPDF.Data)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("admin can export PDF", func(t *testing.T) {
|
||||||
|
owner := testutil.NewClient(t, testutil.RoleOwner)
|
||||||
|
admin := testutil.NewClientInOrg(t, testutil.RoleAdmin, owner)
|
||||||
|
_ = factory.NewProcessingActivity(owner).WithName("RBAC Export Test").Create()
|
||||||
|
|
||||||
|
query := `
|
||||||
|
mutation ExportProcessingActivitiesPDF($input: ExportProcessingActivitiesPDFInput!) {
|
||||||
|
exportProcessingActivitiesPDF(input: $input) {
|
||||||
|
data
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`
|
||||||
|
|
||||||
|
var result struct {
|
||||||
|
ExportProcessingActivitiesPDF struct {
|
||||||
|
Data string `json:"data"`
|
||||||
|
} `json:"exportProcessingActivitiesPDF"`
|
||||||
|
}
|
||||||
|
|
||||||
|
err := admin.Execute(query, map[string]any{
|
||||||
|
"input": map[string]any{
|
||||||
|
"organizationId": admin.GetOrganizationID().String(),
|
||||||
|
"filter": nil,
|
||||||
|
},
|
||||||
|
}, &result)
|
||||||
|
require.NoError(t, err, "admin should be able to export PDF")
|
||||||
|
assert.NotEmpty(t, result.ExportProcessingActivitiesPDF.Data)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("viewer can export PDF", func(t *testing.T) {
|
||||||
|
owner := testutil.NewClient(t, testutil.RoleOwner)
|
||||||
|
viewer := testutil.NewClientInOrg(t, testutil.RoleViewer, owner)
|
||||||
|
_ = factory.NewProcessingActivity(owner).WithName("RBAC Export Test").Create()
|
||||||
|
|
||||||
|
query := `
|
||||||
|
mutation ExportProcessingActivitiesPDF($input: ExportProcessingActivitiesPDFInput!) {
|
||||||
|
exportProcessingActivitiesPDF(input: $input) {
|
||||||
|
data
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`
|
||||||
|
|
||||||
|
var result struct {
|
||||||
|
ExportProcessingActivitiesPDF struct {
|
||||||
|
Data string `json:"data"`
|
||||||
|
} `json:"exportProcessingActivitiesPDF"`
|
||||||
|
}
|
||||||
|
|
||||||
|
err := viewer.Execute(query, map[string]any{
|
||||||
|
"input": map[string]any{
|
||||||
|
"organizationId": viewer.GetOrganizationID().String(),
|
||||||
|
"filter": nil,
|
||||||
|
},
|
||||||
|
}, &result)
|
||||||
|
require.NoError(t, err, "viewer should be able to export PDF")
|
||||||
|
assert.NotEmpty(t, result.ExportProcessingActivitiesPDF.Data)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|||||||
@@ -92,7 +92,7 @@ const (
|
|||||||
ActionListComplianceReports Action = "listComplianceReports"
|
ActionListComplianceReports Action = "listComplianceReports"
|
||||||
ActionListContacts Action = "listContacts"
|
ActionListContacts Action = "listContacts"
|
||||||
ActionListContinualImprovements Action = "listContinualImprovements"
|
ActionListContinualImprovements Action = "listContinualImprovements"
|
||||||
ActionListRightsRequests Action = "listRightsRequests"
|
ActionListRightsRequests Action = "listRightsRequests"
|
||||||
ActionListControls Action = "listControls"
|
ActionListControls Action = "listControls"
|
||||||
ActionListData Action = "listData"
|
ActionListData Action = "listData"
|
||||||
ActionListDocuments Action = "listDocuments"
|
ActionListDocuments Action = "listDocuments"
|
||||||
@@ -235,39 +235,42 @@ const (
|
|||||||
ActionDeleteVendorDataPrivacyAgreement Action = "deleteVendorDataPrivacyAgreement"
|
ActionDeleteVendorDataPrivacyAgreement Action = "deleteVendorDataPrivacyAgreement"
|
||||||
ActionDeleteVendorService Action = "deleteVendorService"
|
ActionDeleteVendorService Action = "deleteVendorService"
|
||||||
|
|
||||||
ActionAcceptInvitation Action = "acceptInvitation"
|
ActionAcceptInvitation Action = "acceptInvitation"
|
||||||
ActionAssessVendor Action = "assessVendor"
|
ActionAssessVendor Action = "assessVendor"
|
||||||
ActionAssignTask Action = "assignTask"
|
ActionAssignTask Action = "assignTask"
|
||||||
ActionBulkDeleteDocuments Action = "bulkDeleteDocuments"
|
ActionBulkDeleteDocuments Action = "bulkDeleteDocuments"
|
||||||
ActionBulkExportDocuments Action = "bulkExportDocuments"
|
ActionBulkExportDocuments Action = "bulkExportDocuments"
|
||||||
ActionBulkPublishDocumentVersions Action = "bulkPublishDocumentVersions"
|
ActionBulkPublishDocumentVersions Action = "bulkPublishDocumentVersions"
|
||||||
ActionBulkRequestSignatures Action = "bulkRequestSignatures"
|
ActionBulkRequestSignatures Action = "bulkRequestSignatures"
|
||||||
ActionCancelSignatureRequest Action = "cancelSignatureRequest"
|
ActionCancelSignatureRequest Action = "cancelSignatureRequest"
|
||||||
ActionSignDocument Action = "signDocument"
|
ActionSignDocument Action = "signDocument"
|
||||||
ActionConfirmEmail Action = "confirmEmail"
|
ActionConfirmEmail Action = "confirmEmail"
|
||||||
ActionDisableSAML Action = "disableSAML"
|
ActionDisableSAML Action = "disableSAML"
|
||||||
ActionEnableSAML Action = "enableSAML"
|
ActionEnableSAML Action = "enableSAML"
|
||||||
ActionExportDocumentVersionPDF Action = "exportDocumentVersionPDF"
|
ActionExportDocumentVersionPDF Action = "exportDocumentVersionPDF"
|
||||||
ActionExportSignableVersionDocumentPDF Action = "exportSignableVersionDocumentPDF"
|
ActionExportSignableVersionDocumentPDF Action = "exportSignableVersionDocumentPDF"
|
||||||
ActionExportFramework Action = "exportFramework"
|
ActionExportProcessingActivitiesPDF Action = "exportProcessingActivitiesPDF"
|
||||||
ActionGenerateDocumentChangelog Action = "generateDocumentChangelog"
|
ActionExportDataProtectionImpactAssessmentsPDF Action = "exportDataProtectionImpactAssessmentsPDF"
|
||||||
ActionGenerateFrameworkStateOfApplicability Action = "generateFrameworkStateOfApplicability"
|
ActionExportTransferImpactAssessmentsPDF Action = "exportTransferImpactAssessmentsPDF"
|
||||||
ActionImportFramework Action = "importFramework"
|
ActionExportFramework Action = "exportFramework"
|
||||||
ActionImportMeasure Action = "importMeasure"
|
ActionGenerateDocumentChangelog Action = "generateDocumentChangelog"
|
||||||
ActionInitiateDomainVerification Action = "initiateDomainVerification"
|
ActionGenerateFrameworkStateOfApplicability Action = "generateFrameworkStateOfApplicability"
|
||||||
ActionInviteUser Action = "inviteUser"
|
ActionImportFramework Action = "importFramework"
|
||||||
ActionPublishDocumentVersion Action = "publishDocumentVersion"
|
ActionImportMeasure Action = "importMeasure"
|
||||||
ActionRemoveMember Action = "removeMember"
|
ActionInitiateDomainVerification Action = "initiateDomainVerification"
|
||||||
ActionRequestSignature Action = "requestSignature"
|
ActionInviteUser Action = "inviteUser"
|
||||||
ActionSendSigningNotifications Action = "sendSigningNotifications"
|
ActionPublishDocumentVersion Action = "publishDocumentVersion"
|
||||||
ActionUnassignTask Action = "unassignTask"
|
ActionRemoveMember Action = "removeMember"
|
||||||
ActionUploadAuditReport Action = "uploadAuditReport"
|
ActionRequestSignature Action = "requestSignature"
|
||||||
ActionUploadMeasureEvidence Action = "uploadMeasureEvidence"
|
ActionSendSigningNotifications Action = "sendSigningNotifications"
|
||||||
ActionUploadTrustCenterNDA Action = "uploadTrustCenterNDA"
|
ActionUnassignTask Action = "unassignTask"
|
||||||
ActionUploadVendorBusinessAssociateAgreement Action = "uploadVendorBusinessAssociateAgreement"
|
ActionUploadAuditReport Action = "uploadAuditReport"
|
||||||
ActionUploadVendorComplianceReport Action = "uploadVendorComplianceReport"
|
ActionUploadMeasureEvidence Action = "uploadMeasureEvidence"
|
||||||
ActionUploadVendorDataPrivacyAgreement Action = "uploadVendorDataPrivacyAgreement"
|
ActionUploadTrustCenterNDA Action = "uploadTrustCenterNDA"
|
||||||
ActionVerifyDomain Action = "verifyDomain"
|
ActionUploadVendorBusinessAssociateAgreement Action = "uploadVendorBusinessAssociateAgreement"
|
||||||
|
ActionUploadVendorComplianceReport Action = "uploadVendorComplianceReport"
|
||||||
|
ActionUploadVendorDataPrivacyAgreement Action = "uploadVendorDataPrivacyAgreement"
|
||||||
|
ActionVerifyDomain Action = "verifyDomain"
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
@@ -287,27 +290,30 @@ var Permissions = map[uint16]map[Action][]Role{
|
|||||||
|
|
||||||
ActionListSignableDocuments: InternalRoles,
|
ActionListSignableDocuments: InternalRoles,
|
||||||
|
|
||||||
ActionListDocuments: NonEmployeeRoles,
|
ActionListDocuments: NonEmployeeRoles,
|
||||||
ActionGetHorizontalLogoUrl: NonEmployeeRoles,
|
ActionGetHorizontalLogoUrl: NonEmployeeRoles,
|
||||||
ActionPeoples: NonEmployeeRoles,
|
ActionPeoples: NonEmployeeRoles,
|
||||||
ActionTotalCount: NonEmployeeRoles,
|
ActionTotalCount: NonEmployeeRoles,
|
||||||
ActionListFrameworks: NonEmployeeRoles,
|
ActionListFrameworks: NonEmployeeRoles,
|
||||||
ActionListControls: NonEmployeeRoles,
|
ActionListControls: NonEmployeeRoles,
|
||||||
ActionListVendors: NonEmployeeRoles,
|
ActionListVendors: NonEmployeeRoles,
|
||||||
ActionListPeople: NonEmployeeRoles,
|
ActionListPeople: NonEmployeeRoles,
|
||||||
ActionListMeasures: NonEmployeeRoles,
|
ActionListMeasures: NonEmployeeRoles,
|
||||||
ActionListRisks: NonEmployeeRoles,
|
ActionListRisks: NonEmployeeRoles,
|
||||||
ActionListAssets: NonEmployeeRoles,
|
ActionListAssets: NonEmployeeRoles,
|
||||||
ActionListData: NonEmployeeRoles,
|
ActionListData: NonEmployeeRoles,
|
||||||
ActionListAudits: NonEmployeeRoles,
|
ActionListAudits: NonEmployeeRoles,
|
||||||
ActionListNonconformities: NonEmployeeRoles,
|
ActionListNonconformities: NonEmployeeRoles,
|
||||||
ActionListObligations: NonEmployeeRoles,
|
ActionListObligations: NonEmployeeRoles,
|
||||||
ActionListContinualImprovements: NonEmployeeRoles,
|
ActionListContinualImprovements: NonEmployeeRoles,
|
||||||
ActionListRightsRequests: NonEmployeeRoles,
|
ActionListRightsRequests: NonEmployeeRoles,
|
||||||
ActionListProcessingActivities: NonEmployeeRoles,
|
ActionListProcessingActivities: NonEmployeeRoles,
|
||||||
ActionListSnapshots: NonEmployeeRoles,
|
ActionExportProcessingActivitiesPDF: NonEmployeeRoles,
|
||||||
ActionConfirmEmail: NonEmployeeRoles,
|
ActionExportDataProtectionImpactAssessmentsPDF: NonEmployeeRoles,
|
||||||
ActionAcceptInvitation: NonEmployeeRoles,
|
ActionExportTransferImpactAssessmentsPDF: NonEmployeeRoles,
|
||||||
|
ActionListSnapshots: NonEmployeeRoles,
|
||||||
|
ActionConfirmEmail: NonEmployeeRoles,
|
||||||
|
ActionAcceptInvitation: NonEmployeeRoles,
|
||||||
|
|
||||||
ActionListTrustCenterFiles: CoreRoles,
|
ActionListTrustCenterFiles: CoreRoles,
|
||||||
ActionGetTrustCenter: CoreRoles,
|
ActionGetTrustCenter: CoreRoles,
|
||||||
@@ -694,6 +700,7 @@ var Permissions = map[uint16]map[Action][]Role{
|
|||||||
ActionGetDataProtectionOfficer: NonEmployeeRoles,
|
ActionGetDataProtectionOfficer: NonEmployeeRoles,
|
||||||
ActionGetDataProtectionImpactAssessment: NonEmployeeRoles,
|
ActionGetDataProtectionImpactAssessment: NonEmployeeRoles,
|
||||||
ActionGetTransferImpactAssessment: NonEmployeeRoles,
|
ActionGetTransferImpactAssessment: NonEmployeeRoles,
|
||||||
|
ActionExportProcessingActivitiesPDF: NonEmployeeRoles,
|
||||||
|
|
||||||
ActionUpdateProcessingActivity: EditRoles,
|
ActionUpdateProcessingActivity: EditRoles,
|
||||||
ActionDeleteProcessingActivity: EditRoles,
|
ActionDeleteProcessingActivity: EditRoles,
|
||||||
|
|||||||
@@ -35,6 +35,12 @@ func (e ErrDataProtectionImpactAssessmentNotFound) Error() string {
|
|||||||
return fmt.Sprintf("data protection impact assessment not found: %q", e.Identifier)
|
return fmt.Sprintf("data protection impact assessment not found: %q", e.Identifier)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type ErrNoDataProtectionImpactAssessmentsFound struct{}
|
||||||
|
|
||||||
|
func (e ErrNoDataProtectionImpactAssessmentsFound) Error() string {
|
||||||
|
return "no data protection impact assessments found"
|
||||||
|
}
|
||||||
|
|
||||||
type (
|
type (
|
||||||
DataProtectionImpactAssessment struct {
|
DataProtectionImpactAssessment struct {
|
||||||
ID gid.GID `db:"id"`
|
ID gid.GID `db:"id"`
|
||||||
@@ -151,6 +157,56 @@ WHERE
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (dpias *DataProtectionImpactAssessments) LoadAllByOrganizationID(
|
||||||
|
ctx context.Context,
|
||||||
|
conn pg.Conn,
|
||||||
|
scope Scoper,
|
||||||
|
organizationID gid.GID,
|
||||||
|
filter *DataProtectionImpactAssessmentFilter,
|
||||||
|
) error {
|
||||||
|
q := `
|
||||||
|
SELECT
|
||||||
|
id,
|
||||||
|
snapshot_id,
|
||||||
|
source_id,
|
||||||
|
organization_id,
|
||||||
|
processing_activity_id,
|
||||||
|
description,
|
||||||
|
necessity_and_proportionality,
|
||||||
|
potential_risk,
|
||||||
|
mitigations,
|
||||||
|
residual_risk,
|
||||||
|
created_at,
|
||||||
|
updated_at
|
||||||
|
FROM
|
||||||
|
processing_activity_data_protection_impact_assessments
|
||||||
|
WHERE
|
||||||
|
%s
|
||||||
|
AND organization_id = @organization_id
|
||||||
|
AND %s
|
||||||
|
`
|
||||||
|
|
||||||
|
q = fmt.Sprintf(q, scope.SQLFragment(), filter.SQLFragment())
|
||||||
|
|
||||||
|
args := pgx.StrictNamedArgs{"organization_id": organizationID}
|
||||||
|
maps.Copy(args, scope.SQLArguments())
|
||||||
|
maps.Copy(args, filter.SQLArguments())
|
||||||
|
|
||||||
|
rows, err := conn.Query(ctx, q, args)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("cannot query data protection impact assessments: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
results, err := pgx.CollectRows(rows, pgx.RowToAddrOfStructByName[DataProtectionImpactAssessment])
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("cannot collect data protection impact assessments: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
*dpias = results
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
func (dpia *DataProtectionImpactAssessment) LoadByID(
|
func (dpia *DataProtectionImpactAssessment) LoadByID(
|
||||||
ctx context.Context,
|
ctx context.Context,
|
||||||
conn pg.Conn,
|
conn pg.Conn,
|
||||||
@@ -429,4 +485,3 @@ WHERE dpia.tenant_id = @tenant_id AND dpia.organization_id = @organization_id AN
|
|||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -53,3 +53,9 @@ func (f *DataProtectionImpactAssessmentFilter) SQLFragment() string {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (f *DataProtectionImpactAssessmentFilter) SnapshotID() *gid.GID {
|
||||||
|
if f.snapshotID == nil || *f.snapshotID == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return *f.snapshotID
|
||||||
|
}
|
||||||
|
|||||||
@@ -26,6 +26,12 @@ import (
|
|||||||
"go.probo.inc/probo/pkg/page"
|
"go.probo.inc/probo/pkg/page"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
type ErrNoProcessingActivitiesFound struct{}
|
||||||
|
|
||||||
|
func (e ErrNoProcessingActivitiesFound) Error() string {
|
||||||
|
return "no processing activities found"
|
||||||
|
}
|
||||||
|
|
||||||
type (
|
type (
|
||||||
ProcessingActivity struct {
|
ProcessingActivity struct {
|
||||||
ID gid.GID `db:"id"`
|
ID gid.GID `db:"id"`
|
||||||
@@ -231,6 +237,70 @@ WHERE
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (p *ProcessingActivities) LoadAllByOrganizationID(
|
||||||
|
ctx context.Context,
|
||||||
|
conn pg.Conn,
|
||||||
|
scope Scoper,
|
||||||
|
organizationID gid.GID,
|
||||||
|
filter *ProcessingActivityFilter,
|
||||||
|
) error {
|
||||||
|
q := `
|
||||||
|
SELECT
|
||||||
|
id,
|
||||||
|
snapshot_id,
|
||||||
|
source_id,
|
||||||
|
organization_id,
|
||||||
|
name,
|
||||||
|
purpose,
|
||||||
|
data_subject_category,
|
||||||
|
personal_data_category,
|
||||||
|
special_or_criminal_data,
|
||||||
|
consent_evidence_link,
|
||||||
|
lawful_basis,
|
||||||
|
recipients,
|
||||||
|
location,
|
||||||
|
international_transfers,
|
||||||
|
transfer_safeguards,
|
||||||
|
retention_period,
|
||||||
|
security_measures,
|
||||||
|
data_protection_impact_assessment_needed,
|
||||||
|
transfer_impact_assessment_needed,
|
||||||
|
last_review_date,
|
||||||
|
next_review_date,
|
||||||
|
role,
|
||||||
|
data_protection_officer_id,
|
||||||
|
created_at,
|
||||||
|
updated_at
|
||||||
|
FROM
|
||||||
|
processing_activities
|
||||||
|
WHERE
|
||||||
|
%s
|
||||||
|
AND organization_id = @organization_id
|
||||||
|
AND %s
|
||||||
|
ORDER BY created_at DESC
|
||||||
|
`
|
||||||
|
|
||||||
|
q = fmt.Sprintf(q, scope.SQLFragment(), filter.SQLFragment())
|
||||||
|
|
||||||
|
args := pgx.StrictNamedArgs{"organization_id": organizationID}
|
||||||
|
maps.Copy(args, scope.SQLArguments())
|
||||||
|
maps.Copy(args, filter.SQLArguments())
|
||||||
|
|
||||||
|
rows, err := conn.Query(ctx, q, args)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("cannot query processing activities: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
processingActivities, err := pgx.CollectRows(rows, pgx.RowToAddrOfStructByName[ProcessingActivity])
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("cannot collect processing activities: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
*p = processingActivities
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
func (p *ProcessingActivity) Insert(
|
func (p *ProcessingActivity) Insert(
|
||||||
ctx context.Context,
|
ctx context.Context,
|
||||||
conn pg.Conn,
|
conn pg.Conn,
|
||||||
|
|||||||
@@ -52,3 +52,10 @@ func (f *ProcessingActivityFilter) SQLFragment() string {
|
|||||||
return "snapshot_id = @filter_snapshot_id"
|
return "snapshot_id = @filter_snapshot_id"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (f *ProcessingActivityFilter) SnapshotID() *gid.GID {
|
||||||
|
if f.snapshotID == nil || *f.snapshotID == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return *f.snapshotID
|
||||||
|
}
|
||||||
|
|||||||
@@ -20,10 +20,10 @@ import (
|
|||||||
"maps"
|
"maps"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"go.probo.inc/probo/pkg/gid"
|
|
||||||
"go.probo.inc/probo/pkg/page"
|
|
||||||
"github.com/jackc/pgx/v5"
|
"github.com/jackc/pgx/v5"
|
||||||
"go.gearno.de/kit/pg"
|
"go.gearno.de/kit/pg"
|
||||||
|
"go.probo.inc/probo/pkg/gid"
|
||||||
|
"go.probo.inc/probo/pkg/page"
|
||||||
)
|
)
|
||||||
|
|
||||||
type (
|
type (
|
||||||
@@ -99,6 +99,7 @@ func (s *Snapshots) CountByOrganizationID(
|
|||||||
conn pg.Conn,
|
conn pg.Conn,
|
||||||
scope Scoper,
|
scope Scoper,
|
||||||
organizationID gid.GID,
|
organizationID gid.GID,
|
||||||
|
filter *SnapshotFilter,
|
||||||
) (int, error) {
|
) (int, error) {
|
||||||
q := `
|
q := `
|
||||||
SELECT
|
SELECT
|
||||||
@@ -108,12 +109,14 @@ FROM
|
|||||||
WHERE
|
WHERE
|
||||||
%s
|
%s
|
||||||
AND organization_id = @organization_id
|
AND organization_id = @organization_id
|
||||||
|
AND %s
|
||||||
`
|
`
|
||||||
|
|
||||||
q = fmt.Sprintf(q, scope.SQLFragment())
|
q = fmt.Sprintf(q, scope.SQLFragment(), filter.SQLFragment())
|
||||||
|
|
||||||
args := pgx.StrictNamedArgs{"organization_id": organizationID}
|
args := pgx.StrictNamedArgs{"organization_id": organizationID}
|
||||||
maps.Copy(args, scope.SQLArguments())
|
maps.Copy(args, scope.SQLArguments())
|
||||||
|
maps.Copy(args, filter.SQLArguments())
|
||||||
|
|
||||||
row := conn.QueryRow(ctx, q, args)
|
row := conn.QueryRow(ctx, q, args)
|
||||||
|
|
||||||
|
|||||||
65
pkg/coredata/snapshot_filter.go
Normal file
65
pkg/coredata/snapshot_filter.go
Normal file
@@ -0,0 +1,65 @@
|
|||||||
|
// Copyright (c) 2025 Probo Inc <hello@getprobo.com>.
|
||||||
|
//
|
||||||
|
// Permission to use, copy, modify, and/or distribute this software for any
|
||||||
|
// purpose with or without fee is hereby granted, provided that the above
|
||||||
|
// copyright notice and this permission notice appear in all copies.
|
||||||
|
//
|
||||||
|
// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
|
||||||
|
// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
|
||||||
|
// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
|
||||||
|
// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
|
||||||
|
// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
|
||||||
|
// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
|
||||||
|
// PERFORMANCE OF THIS SOFTWARE.
|
||||||
|
|
||||||
|
package coredata
|
||||||
|
|
||||||
|
import (
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/jackc/pgx/v5"
|
||||||
|
)
|
||||||
|
|
||||||
|
type (
|
||||||
|
SnapshotFilter struct {
|
||||||
|
snapshotType *SnapshotsType
|
||||||
|
beforeDate *time.Time
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
func NewSnapshotFilter(snapshotType *SnapshotsType) *SnapshotFilter {
|
||||||
|
return &SnapshotFilter{
|
||||||
|
snapshotType: snapshotType,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *SnapshotFilter) WithBeforeDate(beforeDate *time.Time) *SnapshotFilter {
|
||||||
|
f.beforeDate = beforeDate
|
||||||
|
return f
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *SnapshotFilter) SQLArguments() pgx.NamedArgs {
|
||||||
|
args := pgx.NamedArgs{
|
||||||
|
"filter_snapshot_type": f.snapshotType,
|
||||||
|
"filter_before_date": f.beforeDate,
|
||||||
|
}
|
||||||
|
|
||||||
|
return args
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *SnapshotFilter) SQLFragment() string {
|
||||||
|
return `
|
||||||
|
(
|
||||||
|
CASE
|
||||||
|
WHEN @filter_snapshot_type::snapshots_type IS NOT NULL THEN
|
||||||
|
type = @filter_snapshot_type::snapshots_type
|
||||||
|
ELSE TRUE
|
||||||
|
END
|
||||||
|
AND
|
||||||
|
CASE
|
||||||
|
WHEN @filter_before_date::timestamptz IS NOT NULL THEN
|
||||||
|
created_at <= @filter_before_date::timestamptz
|
||||||
|
ELSE TRUE
|
||||||
|
END
|
||||||
|
)`
|
||||||
|
}
|
||||||
@@ -35,6 +35,12 @@ func (e ErrTransferImpactAssessmentNotFound) Error() string {
|
|||||||
return fmt.Sprintf("transfer impact assessment not found: %q", e.Identifier)
|
return fmt.Sprintf("transfer impact assessment not found: %q", e.Identifier)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type ErrNoTransferImpactAssessmentsFound struct{}
|
||||||
|
|
||||||
|
func (e ErrNoTransferImpactAssessmentsFound) Error() string {
|
||||||
|
return "no transfer impact assessments found"
|
||||||
|
}
|
||||||
|
|
||||||
type (
|
type (
|
||||||
TransferImpactAssessment struct {
|
TransferImpactAssessment struct {
|
||||||
ID gid.GID `db:"id"`
|
ID gid.GID `db:"id"`
|
||||||
@@ -151,6 +157,56 @@ WHERE
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (tias *TransferImpactAssessments) LoadAllByOrganizationID(
|
||||||
|
ctx context.Context,
|
||||||
|
conn pg.Conn,
|
||||||
|
scope Scoper,
|
||||||
|
organizationID gid.GID,
|
||||||
|
filter *TransferImpactAssessmentFilter,
|
||||||
|
) error {
|
||||||
|
q := `
|
||||||
|
SELECT
|
||||||
|
id,
|
||||||
|
snapshot_id,
|
||||||
|
source_id,
|
||||||
|
organization_id,
|
||||||
|
processing_activity_id,
|
||||||
|
data_subjects,
|
||||||
|
legal_mechanism,
|
||||||
|
transfer,
|
||||||
|
local_law_risk,
|
||||||
|
supplementary_measures,
|
||||||
|
created_at,
|
||||||
|
updated_at
|
||||||
|
FROM
|
||||||
|
processing_activity_transfer_impact_assessments
|
||||||
|
WHERE
|
||||||
|
%s
|
||||||
|
AND organization_id = @organization_id
|
||||||
|
AND %s
|
||||||
|
`
|
||||||
|
|
||||||
|
q = fmt.Sprintf(q, scope.SQLFragment(), filter.SQLFragment())
|
||||||
|
|
||||||
|
args := pgx.StrictNamedArgs{"organization_id": organizationID}
|
||||||
|
maps.Copy(args, scope.SQLArguments())
|
||||||
|
maps.Copy(args, filter.SQLArguments())
|
||||||
|
|
||||||
|
rows, err := conn.Query(ctx, q, args)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("cannot query transfer impact assessments: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
results, err := pgx.CollectRows(rows, pgx.RowToAddrOfStructByName[TransferImpactAssessment])
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("cannot collect transfer impact assessments: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
*tias = results
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
func (tia *TransferImpactAssessment) LoadByID(
|
func (tia *TransferImpactAssessment) LoadByID(
|
||||||
ctx context.Context,
|
ctx context.Context,
|
||||||
conn pg.Conn,
|
conn pg.Conn,
|
||||||
@@ -429,4 +485,3 @@ WHERE tia.tenant_id = @tenant_id AND tia.organization_id = @organization_id AND
|
|||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -53,3 +53,9 @@ func (f *TransferImpactAssessmentFilter) SQLFragment() string {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (f *TransferImpactAssessmentFilter) SnapshotID() *gid.GID {
|
||||||
|
if f.snapshotID == nil || *f.snapshotID == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return *f.snapshotID
|
||||||
|
}
|
||||||
|
|||||||
@@ -21,10 +21,10 @@ import (
|
|||||||
"maps"
|
"maps"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"go.probo.inc/probo/pkg/gid"
|
|
||||||
"go.probo.inc/probo/pkg/page"
|
|
||||||
"github.com/jackc/pgx/v5"
|
"github.com/jackc/pgx/v5"
|
||||||
"go.gearno.de/kit/pg"
|
"go.gearno.de/kit/pg"
|
||||||
|
"go.probo.inc/probo/pkg/gid"
|
||||||
|
"go.probo.inc/probo/pkg/page"
|
||||||
)
|
)
|
||||||
|
|
||||||
type (
|
type (
|
||||||
@@ -850,6 +850,74 @@ WHERE %s
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (v *Vendors) LoadAllByProcessingActivities(
|
||||||
|
ctx context.Context,
|
||||||
|
conn pg.Conn,
|
||||||
|
scope Scoper,
|
||||||
|
organizationID gid.GID,
|
||||||
|
filter *ProcessingActivityFilter,
|
||||||
|
) (map[gid.GID][]string, error) {
|
||||||
|
q := `
|
||||||
|
WITH filtered_processing_activities AS (
|
||||||
|
SELECT
|
||||||
|
pa.id
|
||||||
|
FROM
|
||||||
|
processing_activities pa
|
||||||
|
WHERE
|
||||||
|
pa.tenant_id = @tenant_id
|
||||||
|
AND pa.organization_id = @organization_id
|
||||||
|
AND %s
|
||||||
|
),
|
||||||
|
filtered_vendors AS (
|
||||||
|
SELECT
|
||||||
|
v.id,
|
||||||
|
v.name
|
||||||
|
FROM
|
||||||
|
vendors v
|
||||||
|
WHERE
|
||||||
|
v.tenant_id = @tenant_id
|
||||||
|
)
|
||||||
|
SELECT
|
||||||
|
pav.processing_activity_id,
|
||||||
|
fv.name
|
||||||
|
FROM
|
||||||
|
processing_activity_vendors pav
|
||||||
|
INNER JOIN
|
||||||
|
filtered_vendors fv ON fv.id = pav.vendor_id
|
||||||
|
INNER JOIN
|
||||||
|
filtered_processing_activities fpa ON fpa.id = pav.processing_activity_id
|
||||||
|
WHERE
|
||||||
|
pav.tenant_id = @tenant_id
|
||||||
|
ORDER BY
|
||||||
|
pav.processing_activity_id, fv.name
|
||||||
|
`
|
||||||
|
q = fmt.Sprintf(q, filter.SQLFragment())
|
||||||
|
|
||||||
|
args := pgx.StrictNamedArgs{
|
||||||
|
"organization_id": organizationID,
|
||||||
|
}
|
||||||
|
maps.Copy(args, scope.SQLArguments())
|
||||||
|
maps.Copy(args, filter.SQLArguments())
|
||||||
|
|
||||||
|
rows, err := conn.Query(ctx, q, args)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("cannot query vendors: %w", err)
|
||||||
|
}
|
||||||
|
defer rows.Close()
|
||||||
|
|
||||||
|
vendorMap := make(map[gid.GID][]string)
|
||||||
|
for rows.Next() {
|
||||||
|
var processingActivityID gid.GID
|
||||||
|
var vendorName string
|
||||||
|
if err := rows.Scan(&processingActivityID, &vendorName); err != nil {
|
||||||
|
return nil, fmt.Errorf("cannot scan vendor: %w", err)
|
||||||
|
}
|
||||||
|
vendorMap[processingActivityID] = append(vendorMap[processingActivityID], vendorName)
|
||||||
|
}
|
||||||
|
|
||||||
|
return vendorMap, nil
|
||||||
|
}
|
||||||
|
|
||||||
func (d Vendors) InsertDataSnapshots(
|
func (d Vendors) InsertDataSnapshots(
|
||||||
ctx context.Context,
|
ctx context.Context,
|
||||||
conn pg.Conn,
|
conn pg.Conn,
|
||||||
|
|||||||
315
pkg/docgen/data_protection_impact_assessments_template.html
Normal file
315
pkg/docgen/data_protection_impact_assessments_template.html
Normal file
@@ -0,0 +1,315 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>Data Protection Impact Assessments Export</title>
|
||||||
|
<style>
|
||||||
|
@page {
|
||||||
|
size: A4;
|
||||||
|
margin: 2.5cm;
|
||||||
|
@bottom-right {
|
||||||
|
content: "Page " counter(page) " of " counter(pages);
|
||||||
|
font-family: Arial, sans-serif;
|
||||||
|
font-size: 9pt;
|
||||||
|
color: #666;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
font-family: Arial, sans-serif;
|
||||||
|
font-size: 7.5pt;
|
||||||
|
line-height: 1.4;
|
||||||
|
color: #333;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
background: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Cover page */
|
||||||
|
.cover-page {
|
||||||
|
page-break-after: always;
|
||||||
|
}
|
||||||
|
|
||||||
|
.company-header {
|
||||||
|
margin-bottom: 30px;
|
||||||
|
page-break-after: avoid;
|
||||||
|
}
|
||||||
|
|
||||||
|
.company-logo {
|
||||||
|
max-height: 50px;
|
||||||
|
max-width: 250px;
|
||||||
|
object-fit: contain;
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.company-name {
|
||||||
|
font-size: 16pt;
|
||||||
|
color: #1a1a1a;
|
||||||
|
margin-top: 10px;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.export-title {
|
||||||
|
font-size: 22pt;
|
||||||
|
font-weight: normal;
|
||||||
|
color: #1a1a1a;
|
||||||
|
margin: 0 0 25px 0;
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
|
||||||
|
.document-meta {
|
||||||
|
margin: 0 0 30px 0;
|
||||||
|
font-size: 9pt;
|
||||||
|
}
|
||||||
|
|
||||||
|
.meta-table {
|
||||||
|
width: 100%;
|
||||||
|
border-collapse: collapse;
|
||||||
|
border: 1px solid #333;
|
||||||
|
}
|
||||||
|
|
||||||
|
.meta-table td {
|
||||||
|
padding: 6px 8px;
|
||||||
|
border: 1px solid #333;
|
||||||
|
vertical-align: middle;
|
||||||
|
}
|
||||||
|
|
||||||
|
.meta-table td:first-child {
|
||||||
|
font-weight: 600;
|
||||||
|
width: 25%;
|
||||||
|
background: #f8f8f8;
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
|
||||||
|
.classification {
|
||||||
|
font-weight: bold;
|
||||||
|
text-transform: uppercase;
|
||||||
|
}
|
||||||
|
|
||||||
|
.purpose-section {
|
||||||
|
margin: 30px 0;
|
||||||
|
page-break-after: avoid;
|
||||||
|
}
|
||||||
|
|
||||||
|
.purpose-title {
|
||||||
|
font-size: 15pt;
|
||||||
|
font-weight: bold;
|
||||||
|
color: #000;
|
||||||
|
margin: 0 0 15px 0;
|
||||||
|
page-break-after: avoid;
|
||||||
|
}
|
||||||
|
|
||||||
|
.purpose-text {
|
||||||
|
font-size: 10pt;
|
||||||
|
color: #333;
|
||||||
|
line-height: 1.5;
|
||||||
|
text-align: justify;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/* Assessment page */
|
||||||
|
.assessment-page {
|
||||||
|
page-break-before: always;
|
||||||
|
page-break-after: always;
|
||||||
|
}
|
||||||
|
|
||||||
|
.assessment-page:last-child {
|
||||||
|
page-break-after: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.assessment-title {
|
||||||
|
font-size: 15pt;
|
||||||
|
font-weight: bold;
|
||||||
|
color: #000;
|
||||||
|
margin: 0 0 15px 0;
|
||||||
|
page-break-after: avoid;
|
||||||
|
}
|
||||||
|
|
||||||
|
.assessment-field {
|
||||||
|
margin-bottom: 8px;
|
||||||
|
page-break-inside: avoid;
|
||||||
|
}
|
||||||
|
|
||||||
|
.assessment-field-label {
|
||||||
|
font-size: 10pt;
|
||||||
|
font-weight: bold;
|
||||||
|
color: #333;
|
||||||
|
margin: 12px 0 6px 0;
|
||||||
|
page-break-after: avoid;
|
||||||
|
page-break-inside: avoid;
|
||||||
|
}
|
||||||
|
|
||||||
|
.assessment-field-value {
|
||||||
|
color: #000;
|
||||||
|
font-size: 7.5pt;
|
||||||
|
line-height: 1.4;
|
||||||
|
}
|
||||||
|
|
||||||
|
.empty-value {
|
||||||
|
color: #999;
|
||||||
|
font-style: italic;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Annex page */
|
||||||
|
.annex-page {
|
||||||
|
page-break-before: always;
|
||||||
|
}
|
||||||
|
|
||||||
|
.annex-title {
|
||||||
|
font-size: 15pt;
|
||||||
|
font-weight: bold;
|
||||||
|
color: #000;
|
||||||
|
margin: 0 0 15px 0;
|
||||||
|
page-break-after: avoid;
|
||||||
|
}
|
||||||
|
|
||||||
|
.annex-section {
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.annex-section-title {
|
||||||
|
font-size: 13pt;
|
||||||
|
font-weight: bold;
|
||||||
|
color: #000;
|
||||||
|
margin: 15px 0 10px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.annex-subsection-title {
|
||||||
|
font-size: 10pt;
|
||||||
|
font-weight: bold;
|
||||||
|
color: #333;
|
||||||
|
margin: 12px 0 8px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.annex-enum-list {
|
||||||
|
margin: 10px 0;
|
||||||
|
padding-left: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.annex-enum-item {
|
||||||
|
margin-bottom: 8px;
|
||||||
|
font-size: 10pt;
|
||||||
|
line-height: 1.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.annex-enum-name {
|
||||||
|
font-weight: 600;
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
|
||||||
|
.annex-enum-description {
|
||||||
|
color: #000;
|
||||||
|
margin-left: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media print {
|
||||||
|
body {
|
||||||
|
background: white;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="cover-page">
|
||||||
|
<div class="company-header">
|
||||||
|
{{- if .CompanyHorizontalLogoBase64}}
|
||||||
|
{{imgTag .CompanyHorizontalLogoBase64 "Company Logo" "company-logo"}}
|
||||||
|
{{- else}}
|
||||||
|
<div class="company-name">{{.CompanyName}}</div>
|
||||||
|
{{- end}}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h1 class="export-title">Data Protection Impact Assessments</h1>
|
||||||
|
|
||||||
|
<div class="document-meta">
|
||||||
|
<table class="meta-table">
|
||||||
|
<tr>
|
||||||
|
<td>Classification</td>
|
||||||
|
<td>
|
||||||
|
<span class="classification">CONFIDENTIAL</span>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>Version</td>
|
||||||
|
<td>{{.Version}}</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>Published</td>
|
||||||
|
<td>{{.PublishedAt.Format "January 2, 2006"}}</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="purpose-section">
|
||||||
|
<div class="purpose-title">1. Purpose</div>
|
||||||
|
<div class="purpose-text">
|
||||||
|
This document contains Data Protection Impact Assessments (DPIAs) conducted for processing activities
|
||||||
|
that present a high risk to individuals' rights and freedoms. DPIAs are systematic assessments that
|
||||||
|
evaluate the necessity, proportionality, and risks associated with data processing operations, along
|
||||||
|
with the measures implemented to mitigate identified risks.
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{{- range $index, $assessment := .Assessments}}
|
||||||
|
<div class="assessment-page">
|
||||||
|
{{if eq $index 0}}
|
||||||
|
<h1 class="annex-title">2. Records</h1>
|
||||||
|
{{end}}
|
||||||
|
<h1 class="annex-section-title">2.{{add $index 1}} {{$assessment.ProcessingActivityName}}</h1>
|
||||||
|
|
||||||
|
<div class="assessment-field">
|
||||||
|
<div class="assessment-field-label">Description</div>
|
||||||
|
<div class="assessment-field-value">{{if .Description}}{{.Description}}{{else}}<span class="empty-value">Not specified</span>{{end}}</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="assessment-field">
|
||||||
|
<div class="assessment-field-label">Necessity and Proportionality</div>
|
||||||
|
<div class="assessment-field-value">{{if .NecessityAndProportionality}}{{.NecessityAndProportionality}}{{else}}<span class="empty-value">Not specified</span>{{end}}</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="assessment-field">
|
||||||
|
<div class="assessment-field-label">Potential Risk</div>
|
||||||
|
<div class="assessment-field-value">{{if .PotentialRisk}}{{.PotentialRisk}}{{else}}<span class="empty-value">Not specified</span>{{end}}</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="assessment-field">
|
||||||
|
<div class="assessment-field-label">Mitigations</div>
|
||||||
|
<div class="assessment-field-value">{{if .Mitigations}}{{.Mitigations}}{{else}}<span class="empty-value">Not specified</span>{{end}}</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="assessment-field">
|
||||||
|
<div class="assessment-field-label">Residual Risk</div>
|
||||||
|
<div class="assessment-field-value">{{if .ResidualRisk}}{{.ResidualRisk | formatResidualRisk}}{{else}}<span class="empty-value">Not specified</span>{{end}}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{{- end}}
|
||||||
|
|
||||||
|
<div class="annex-page">
|
||||||
|
<h1 class="annex-title">3. Annexes</h1>
|
||||||
|
|
||||||
|
<div class="annex-section">
|
||||||
|
<div class="annex-section-title">3.1 Lexicon</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="annex-section">
|
||||||
|
<div class="annex-subsection-title">Residual Risk</div>
|
||||||
|
<ul class="annex-enum-list">
|
||||||
|
<li class="annex-enum-item">
|
||||||
|
<span class="annex-enum-name">Low:</span>
|
||||||
|
<span class="annex-enum-description">The residual risk after implementing mitigation measures is considered low. The processing activity poses minimal risk to individuals' rights and freedoms.</span>
|
||||||
|
</li>
|
||||||
|
<li class="annex-enum-item">
|
||||||
|
<span class="annex-enum-name">Medium:</span>
|
||||||
|
<span class="annex-enum-description">The residual risk after implementing mitigation measures is considered medium. The processing activity poses a moderate risk to individuals' rights and freedoms, requiring ongoing monitoring.</span>
|
||||||
|
</li>
|
||||||
|
<li class="annex-enum-item">
|
||||||
|
<span class="annex-enum-name">High:</span>
|
||||||
|
<span class="annex-enum-description">The residual risk after implementing mitigation measures is considered high. The processing activity poses significant risk to individuals' rights and freedoms, requiring enhanced safeguards and regular review.</span>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -23,21 +23,36 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"go.probo.inc/probo/pkg/coredata"
|
|
||||||
"github.com/yuin/goldmark"
|
"github.com/yuin/goldmark"
|
||||||
"github.com/yuin/goldmark/extension"
|
"github.com/yuin/goldmark/extension"
|
||||||
gmhtml "github.com/yuin/goldmark/renderer/html"
|
gmhtml "github.com/yuin/goldmark/renderer/html"
|
||||||
|
"go.probo.inc/probo/pkg/coredata"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
firstPageTOCItems = 21
|
||||||
|
otherPageTOCItems = 28
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
//go:embed template.html
|
//go:embed template.html
|
||||||
htmlTemplateContent string
|
htmlTemplateContent string
|
||||||
|
|
||||||
|
//go:embed processing_activities_template.html
|
||||||
|
processingActivitiesTemplateContent string
|
||||||
|
|
||||||
|
//go:embed data_protection_impact_assessments_template.html
|
||||||
|
dataProtectionImpactAssessmentsTemplateContent string
|
||||||
|
|
||||||
|
//go:embed transfer_impact_assessments_template.html
|
||||||
|
transferImpactAssessmentsTemplateContent string
|
||||||
|
|
||||||
templateFuncs = template.FuncMap{
|
templateFuncs = template.FuncMap{
|
||||||
"now": func() time.Time { return time.Now() },
|
"now": func() time.Time { return time.Now() },
|
||||||
"eq": func(a, b string) bool { return a == b },
|
"eq": func(a, b any) bool { return a == b },
|
||||||
"string": func(v fmt.Stringer) string { return v.String() },
|
"string": func(v fmt.Stringer) string { return v.String() },
|
||||||
"lower": func(s string) string { return strings.ToLower(s) },
|
"lower": func(s string) string { return strings.ToLower(s) },
|
||||||
|
"add": func(a, b int) int { return a + b },
|
||||||
"classificationString": func(c Classification) string { return string(c) },
|
"classificationString": func(c Classification) string { return string(c) },
|
||||||
"formatContent": func(content string) template.HTML {
|
"formatContent": func(content string) template.HTML {
|
||||||
md := goldmark.New(
|
md := goldmark.New(
|
||||||
@@ -56,9 +71,111 @@ var (
|
|||||||
"imgTag": func(src, alt, class string) template.HTML {
|
"imgTag": func(src, alt, class string) template.HTML {
|
||||||
return template.HTML(fmt.Sprintf(`<img src="%s" alt="%s" class="%s">`, html.EscapeString(src), html.EscapeString(alt), html.EscapeString(class)))
|
return template.HTML(fmt.Sprintf(`<img src="%s" alt="%s" class="%s">`, html.EscapeString(src), html.EscapeString(alt), html.EscapeString(class)))
|
||||||
},
|
},
|
||||||
|
"formatLawfulBasis": func(basis coredata.ProcessingActivityLawfulBasis) string {
|
||||||
|
switch basis {
|
||||||
|
case coredata.ProcessingActivityLawfulBasisConsent:
|
||||||
|
return "Consent"
|
||||||
|
case coredata.ProcessingActivityLawfulBasisContractualNecessity:
|
||||||
|
return "Contractual Necessity"
|
||||||
|
case coredata.ProcessingActivityLawfulBasisLegalObligation:
|
||||||
|
return "Legal Obligation"
|
||||||
|
case coredata.ProcessingActivityLawfulBasisLegitimateInterest:
|
||||||
|
return "Legitimate Interest"
|
||||||
|
case coredata.ProcessingActivityLawfulBasisPublicTask:
|
||||||
|
return "Public Task"
|
||||||
|
case coredata.ProcessingActivityLawfulBasisVitalInterests:
|
||||||
|
return "Vital Interests"
|
||||||
|
default:
|
||||||
|
return basis.String()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"formatSpecialOrCriminalData": func(data coredata.ProcessingActivitySpecialOrCriminalDatum) string {
|
||||||
|
switch data {
|
||||||
|
case coredata.ProcessingActivitySpecialOrCriminalDatumYes:
|
||||||
|
return "Yes"
|
||||||
|
case coredata.ProcessingActivitySpecialOrCriminalDatumNo:
|
||||||
|
return "No"
|
||||||
|
case coredata.ProcessingActivitySpecialOrCriminalDatumPossible:
|
||||||
|
return "Possible"
|
||||||
|
default:
|
||||||
|
return data.String()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"formatTransferSafeguard": func(safeguard *coredata.ProcessingActivityTransferSafeguard) string {
|
||||||
|
if safeguard == nil {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
switch *safeguard {
|
||||||
|
case coredata.ProcessingActivityTransferSafeguardStandardContractualClauses:
|
||||||
|
return "Standard Contractual Clauses"
|
||||||
|
case coredata.ProcessingActivityTransferSafeguardBindingCorporateRules:
|
||||||
|
return "Binding Corporate Rules"
|
||||||
|
case coredata.ProcessingActivityTransferSafeguardAdequacyDecision:
|
||||||
|
return "Adequacy Decision"
|
||||||
|
case coredata.ProcessingActivityTransferSafeguardDerogations:
|
||||||
|
return "Derogations"
|
||||||
|
case coredata.ProcessingActivityTransferSafeguardCodesOfConduct:
|
||||||
|
return "Codes of Conduct"
|
||||||
|
case coredata.ProcessingActivityTransferSafeguardCertificationMechanisms:
|
||||||
|
return "Certification Mechanisms"
|
||||||
|
default:
|
||||||
|
return safeguard.String()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"formatDPIANeeded": func(needed coredata.ProcessingActivityDataProtectionImpactAssessment) string {
|
||||||
|
switch needed {
|
||||||
|
case coredata.ProcessingActivityDataProtectionImpactAssessmentNeeded:
|
||||||
|
return "Yes"
|
||||||
|
case coredata.ProcessingActivityDataProtectionImpactAssessmentNotNeeded:
|
||||||
|
return "No"
|
||||||
|
default:
|
||||||
|
return needed.String()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"formatTIANeeded": func(needed coredata.ProcessingActivityTransferImpactAssessment) string {
|
||||||
|
switch needed {
|
||||||
|
case coredata.ProcessingActivityTransferImpactAssessmentNeeded:
|
||||||
|
return "Yes"
|
||||||
|
case coredata.ProcessingActivityTransferImpactAssessmentNotNeeded:
|
||||||
|
return "No"
|
||||||
|
default:
|
||||||
|
return needed.String()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"formatRole": func(role coredata.ProcessingActivityRole) string {
|
||||||
|
switch role {
|
||||||
|
case coredata.ProcessingActivityRoleController:
|
||||||
|
return "Controller"
|
||||||
|
case coredata.ProcessingActivityRoleProcessor:
|
||||||
|
return "Processor"
|
||||||
|
default:
|
||||||
|
return role.String()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"formatResidualRisk": func(risk *coredata.DataProtectionImpactAssessmentResidualRisk) string {
|
||||||
|
if risk == nil {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
switch *risk {
|
||||||
|
case coredata.DataProtectionImpactAssessmentResidualRiskLow:
|
||||||
|
return "Low"
|
||||||
|
case coredata.DataProtectionImpactAssessmentResidualRiskMedium:
|
||||||
|
return "Medium"
|
||||||
|
case coredata.DataProtectionImpactAssessmentResidualRiskHigh:
|
||||||
|
return "High"
|
||||||
|
default:
|
||||||
|
return risk.String()
|
||||||
|
}
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
documentTemplate = template.Must(template.New("document").Funcs(templateFuncs).Parse(htmlTemplateContent))
|
documentTemplate = template.Must(template.New("document").Funcs(templateFuncs).Parse(htmlTemplateContent))
|
||||||
|
|
||||||
|
processingActivitiesTemplate = template.Must(template.New("processingActivities").Funcs(templateFuncs).Parse(processingActivitiesTemplateContent))
|
||||||
|
|
||||||
|
dataProtectionImpactAssessmentsTemplate = template.Must(template.New("dataProtectionImpactAssessments").Funcs(templateFuncs).Parse(dataProtectionImpactAssessmentsTemplateContent))
|
||||||
|
|
||||||
|
transferImpactAssessmentsTemplate = template.Must(template.New("transferImpactAssessments").Funcs(templateFuncs).Parse(transferImpactAssessmentsTemplateContent))
|
||||||
)
|
)
|
||||||
|
|
||||||
type (
|
type (
|
||||||
@@ -82,6 +199,71 @@ type (
|
|||||||
State coredata.DocumentVersionSignatureState
|
State coredata.DocumentVersionSignatureState
|
||||||
RequestedAt time.Time
|
RequestedAt time.Time
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ProcessingActivityTableData struct {
|
||||||
|
CompanyName string
|
||||||
|
CompanyHorizontalLogoBase64 string
|
||||||
|
Version int
|
||||||
|
PublishedAt time.Time
|
||||||
|
Activities []ProcessingActivityRowData
|
||||||
|
}
|
||||||
|
|
||||||
|
ProcessingActivityRowData struct {
|
||||||
|
Name string
|
||||||
|
Purpose *string
|
||||||
|
DataSubjectCategory *string
|
||||||
|
PersonalDataCategory *string
|
||||||
|
SpecialOrCriminalData coredata.ProcessingActivitySpecialOrCriminalDatum
|
||||||
|
ConsentEvidenceLink *string
|
||||||
|
LawfulBasis coredata.ProcessingActivityLawfulBasis
|
||||||
|
Recipients *string
|
||||||
|
Location *string
|
||||||
|
InternationalTransfers bool
|
||||||
|
TransferSafeguards *coredata.ProcessingActivityTransferSafeguard
|
||||||
|
RetentionPeriod *string
|
||||||
|
SecurityMeasures *string
|
||||||
|
DataProtectionImpactAssessmentNeeded coredata.ProcessingActivityDataProtectionImpactAssessment
|
||||||
|
TransferImpactAssessmentNeeded coredata.ProcessingActivityTransferImpactAssessment
|
||||||
|
LastReviewDate *time.Time
|
||||||
|
NextReviewDate *time.Time
|
||||||
|
Role coredata.ProcessingActivityRole
|
||||||
|
DataProtectionOfficerFullName *string
|
||||||
|
Vendors string
|
||||||
|
}
|
||||||
|
|
||||||
|
DataProtectionImpactAssessmentTableData struct {
|
||||||
|
CompanyName string
|
||||||
|
CompanyHorizontalLogoBase64 string
|
||||||
|
Version int
|
||||||
|
PublishedAt time.Time
|
||||||
|
Assessments []DataProtectionImpactAssessmentRowData
|
||||||
|
}
|
||||||
|
|
||||||
|
DataProtectionImpactAssessmentRowData struct {
|
||||||
|
ProcessingActivityName string
|
||||||
|
Description *string
|
||||||
|
NecessityAndProportionality *string
|
||||||
|
PotentialRisk *string
|
||||||
|
Mitigations *string
|
||||||
|
ResidualRisk *coredata.DataProtectionImpactAssessmentResidualRisk
|
||||||
|
}
|
||||||
|
|
||||||
|
TransferImpactAssessmentTableData struct {
|
||||||
|
CompanyName string
|
||||||
|
CompanyHorizontalLogoBase64 string
|
||||||
|
Version int
|
||||||
|
PublishedAt time.Time
|
||||||
|
Assessments []TransferImpactAssessmentRowData
|
||||||
|
}
|
||||||
|
|
||||||
|
TransferImpactAssessmentRowData struct {
|
||||||
|
ProcessingActivityName string
|
||||||
|
DataSubjects *string
|
||||||
|
LegalMechanism *string
|
||||||
|
Transfer *string
|
||||||
|
LocalLawRisk *string
|
||||||
|
SupplementaryMeasures *string
|
||||||
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
@@ -99,3 +281,30 @@ func RenderHTML(data DocumentData) ([]byte, error) {
|
|||||||
|
|
||||||
return buf.Bytes(), nil
|
return buf.Bytes(), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func RenderProcessingActivitiesTableHTML(data ProcessingActivityTableData) ([]byte, error) {
|
||||||
|
var buf bytes.Buffer
|
||||||
|
if err := processingActivitiesTemplate.Execute(&buf, data); err != nil {
|
||||||
|
return nil, fmt.Errorf("cannot execute processing activities template: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return buf.Bytes(), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func RenderDataProtectionImpactAssessmentsTableHTML(data DataProtectionImpactAssessmentTableData) ([]byte, error) {
|
||||||
|
var buf bytes.Buffer
|
||||||
|
if err := dataProtectionImpactAssessmentsTemplate.Execute(&buf, data); err != nil {
|
||||||
|
return nil, fmt.Errorf("cannot execute data protection impact assessments template: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return buf.Bytes(), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func RenderTransferImpactAssessmentsTableHTML(data TransferImpactAssessmentTableData) ([]byte, error) {
|
||||||
|
var buf bytes.Buffer
|
||||||
|
if err := transferImpactAssessmentsTemplate.Execute(&buf, data); err != nil {
|
||||||
|
return nil, fmt.Errorf("cannot execute transfer impact assessments template: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return buf.Bytes(), nil
|
||||||
|
}
|
||||||
|
|||||||
@@ -195,7 +195,7 @@ func TestTemplateFunctions(t *testing.T) {
|
|||||||
})
|
})
|
||||||
|
|
||||||
t.Run("eq function", func(t *testing.T) {
|
t.Run("eq function", func(t *testing.T) {
|
||||||
eqFunc := templateFuncs["eq"].(func(string, string) bool)
|
eqFunc := templateFuncs["eq"].(func(any, any) bool)
|
||||||
assert.True(t, eqFunc("test", "test"))
|
assert.True(t, eqFunc("test", "test"))
|
||||||
assert.False(t, eqFunc("test", "other"))
|
assert.False(t, eqFunc("test", "other"))
|
||||||
})
|
})
|
||||||
@@ -266,7 +266,7 @@ func TestHTMLEscaping(t *testing.T) {
|
|||||||
|
|
||||||
resultStr := string(result)
|
resultStr := string(result)
|
||||||
|
|
||||||
// Verify dangerous content is escaped
|
// Verify dangerous content is escaped
|
||||||
assert.NotContains(t, resultStr, "<script>alert('xss')</script>")
|
assert.NotContains(t, resultStr, "<script>alert('xss')</script>")
|
||||||
assert.NotContains(t, resultStr, "<malicious>tag")
|
assert.NotContains(t, resultStr, "<malicious>tag")
|
||||||
assert.Contains(t, resultStr, "<script>")
|
assert.Contains(t, resultStr, "<script>")
|
||||||
|
|||||||
535
pkg/docgen/processing_activities_template.html
Normal file
535
pkg/docgen/processing_activities_template.html
Normal file
@@ -0,0 +1,535 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>Processing Activities Export</title>
|
||||||
|
<style>
|
||||||
|
@page {
|
||||||
|
size: A4;
|
||||||
|
margin: 2.5cm;
|
||||||
|
@bottom-right {
|
||||||
|
content: "Page " counter(page) " of " counter(pages);
|
||||||
|
font-family: Arial, sans-serif;
|
||||||
|
font-size: 9pt;
|
||||||
|
color: #666;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
font-family: Arial, sans-serif;
|
||||||
|
font-size: 7.5pt;
|
||||||
|
line-height: 1.4;
|
||||||
|
color: #333;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
background: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Cover page */
|
||||||
|
.cover-page {
|
||||||
|
page-break-after: always;
|
||||||
|
}
|
||||||
|
|
||||||
|
.company-header {
|
||||||
|
margin-bottom: 30px;
|
||||||
|
page-break-after: avoid;
|
||||||
|
}
|
||||||
|
|
||||||
|
.company-logo {
|
||||||
|
max-height: 50px;
|
||||||
|
max-width: 250px;
|
||||||
|
object-fit: contain;
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.company-name {
|
||||||
|
font-size: 16pt;
|
||||||
|
color: #1a1a1a;
|
||||||
|
margin-top: 10px;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.export-title {
|
||||||
|
font-size: 22pt;
|
||||||
|
font-weight: normal;
|
||||||
|
color: #1a1a1a;
|
||||||
|
margin: 0 0 25px 0;
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
|
||||||
|
.document-meta {
|
||||||
|
margin: 0 0 30px 0;
|
||||||
|
font-size: 9pt;
|
||||||
|
}
|
||||||
|
|
||||||
|
.meta-table {
|
||||||
|
width: 100%;
|
||||||
|
border-collapse: collapse;
|
||||||
|
border: 1px solid #333;
|
||||||
|
}
|
||||||
|
|
||||||
|
.meta-table td {
|
||||||
|
padding: 6px 8px;
|
||||||
|
border: 1px solid #333;
|
||||||
|
vertical-align: middle;
|
||||||
|
}
|
||||||
|
|
||||||
|
.meta-table td:first-child {
|
||||||
|
font-weight: 600;
|
||||||
|
width: 25%;
|
||||||
|
background: #f8f8f8;
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
|
||||||
|
.classification {
|
||||||
|
font-weight: bold;
|
||||||
|
text-transform: uppercase;
|
||||||
|
}
|
||||||
|
|
||||||
|
.purpose-section {
|
||||||
|
margin: 30px 0;
|
||||||
|
page-break-after: avoid;
|
||||||
|
}
|
||||||
|
|
||||||
|
.purpose-title {
|
||||||
|
font-size: 15pt;
|
||||||
|
font-weight: bold;
|
||||||
|
color: #000;
|
||||||
|
margin: 0 0 15px 0;
|
||||||
|
page-break-after: avoid;
|
||||||
|
}
|
||||||
|
|
||||||
|
.purpose-text {
|
||||||
|
font-size: 10pt;
|
||||||
|
color: #333;
|
||||||
|
line-height: 1.5;
|
||||||
|
text-align: justify;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/* Activity page */
|
||||||
|
.activity-page {
|
||||||
|
page-break-before: always;
|
||||||
|
page-break-after: always;
|
||||||
|
}
|
||||||
|
|
||||||
|
.activity-page:last-child {
|
||||||
|
page-break-after: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.activity-title {
|
||||||
|
font-size: 15pt;
|
||||||
|
font-weight: bold;
|
||||||
|
color: #000;
|
||||||
|
margin: 0 0 15px 0;
|
||||||
|
page-break-after: avoid;
|
||||||
|
}
|
||||||
|
|
||||||
|
.activity-section {
|
||||||
|
margin-bottom: 8px;
|
||||||
|
page-break-inside: avoid;
|
||||||
|
}
|
||||||
|
|
||||||
|
.activity-section-title {
|
||||||
|
font-size: 10pt;
|
||||||
|
font-weight: bold;
|
||||||
|
color: #333;
|
||||||
|
margin: 12px 0 6px 0;
|
||||||
|
page-break-after: avoid;
|
||||||
|
page-break-inside: avoid;
|
||||||
|
}
|
||||||
|
|
||||||
|
.activity-field {
|
||||||
|
margin-bottom: 8px;
|
||||||
|
page-break-inside: avoid;
|
||||||
|
}
|
||||||
|
|
||||||
|
.activity-field-label {
|
||||||
|
font-weight: 600;
|
||||||
|
color: #333;
|
||||||
|
margin-bottom: 3px;
|
||||||
|
font-size: 7.5pt;
|
||||||
|
}
|
||||||
|
|
||||||
|
.activity-field-value {
|
||||||
|
color: #000;
|
||||||
|
font-size: 7.5pt;
|
||||||
|
line-height: 1.4;
|
||||||
|
}
|
||||||
|
|
||||||
|
.empty-value {
|
||||||
|
color: #999;
|
||||||
|
font-style: italic;
|
||||||
|
}
|
||||||
|
|
||||||
|
.activity-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr 1fr;
|
||||||
|
gap: 10px;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.activity-grid-3 {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr 1fr 1fr;
|
||||||
|
gap: 10px;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Annex page */
|
||||||
|
.annex-page {
|
||||||
|
page-break-before: always;
|
||||||
|
}
|
||||||
|
|
||||||
|
.annex-title {
|
||||||
|
font-size: 15pt;
|
||||||
|
font-weight: bold;
|
||||||
|
color: #000;
|
||||||
|
margin: 0 0 15px 0;
|
||||||
|
page-break-after: avoid;
|
||||||
|
}
|
||||||
|
|
||||||
|
.annex-section {
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.annex-section-title {
|
||||||
|
font-size: 13pt;
|
||||||
|
font-weight: bold;
|
||||||
|
color: #000;
|
||||||
|
margin: 15px 0 10px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.annex-subsection-title {
|
||||||
|
font-size: 10pt;
|
||||||
|
font-weight: bold;
|
||||||
|
color: #333;
|
||||||
|
margin: 12px 0 8px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.annex-enum-list {
|
||||||
|
margin: 10px 0;
|
||||||
|
padding-left: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.annex-enum-item {
|
||||||
|
margin-bottom: 8px;
|
||||||
|
font-size: 10pt;
|
||||||
|
line-height: 1.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.annex-enum-name {
|
||||||
|
font-weight: 600;
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
|
||||||
|
.annex-enum-description {
|
||||||
|
color: #000;
|
||||||
|
margin-left: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media print {
|
||||||
|
body {
|
||||||
|
background: white;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="cover-page">
|
||||||
|
<div class="company-header">
|
||||||
|
{{- if .CompanyHorizontalLogoBase64}}
|
||||||
|
{{imgTag .CompanyHorizontalLogoBase64 "Company Logo" "company-logo"}}
|
||||||
|
{{- else}}
|
||||||
|
<div class="company-name">{{.CompanyName}}</div>
|
||||||
|
{{- end}}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h1 class="export-title">Processing Activities</h1>
|
||||||
|
|
||||||
|
<div class="document-meta">
|
||||||
|
<table class="meta-table">
|
||||||
|
<tr>
|
||||||
|
<td>Classification</td>
|
||||||
|
<td>
|
||||||
|
<span class="classification">CONFIDENTIAL</span>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>Version</td>
|
||||||
|
<td>{{.Version}}</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>Published</td>
|
||||||
|
<td>{{.PublishedAt.Format "January 2, 2006"}}</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="purpose-section">
|
||||||
|
<div class="purpose-title">1. Purpose</div>
|
||||||
|
<div class="purpose-text">
|
||||||
|
This document provides a comprehensive overview of all processing activities within the organization.
|
||||||
|
It serves as a record of personal data processing operations, documenting the purposes, legal bases,
|
||||||
|
data categories, and associated safeguards for each activity.
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{{- range $index, $activity := .Activities}}
|
||||||
|
<div class="activity-page">
|
||||||
|
{{if eq $index 0}}
|
||||||
|
<h1 class="annex-title">2. Records</h1>
|
||||||
|
{{end}}
|
||||||
|
<h1 class="annex-section-title">2.{{add $index 1}} {{$activity.Name}}</h1>
|
||||||
|
|
||||||
|
<div class="activity-section">
|
||||||
|
<div class="activity-section-title">2.{{add $index 1}}.1 General Information</div>
|
||||||
|
<div class="activity-field">
|
||||||
|
<div class="activity-field-label">Purpose</div>
|
||||||
|
<div class="activity-field-value">{{if .Purpose}}{{.Purpose}}{{else}}<span class="empty-value">Not specified</span>{{end}}</div>
|
||||||
|
</div>
|
||||||
|
<div class="activity-grid">
|
||||||
|
<div class="activity-field">
|
||||||
|
<div class="activity-field-label">Role</div>
|
||||||
|
<div class="activity-field-value">{{.Role | formatRole}}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="activity-section">
|
||||||
|
<div class="activity-section-title">2.{{add $index 1}}.2 Data Categories</div>
|
||||||
|
<div class="activity-grid-3">
|
||||||
|
<div class="activity-field">
|
||||||
|
<div class="activity-field-label">Data Subject Category</div>
|
||||||
|
<div class="activity-field-value">{{if .DataSubjectCategory}}{{.DataSubjectCategory}}{{else}}<span class="empty-value">Not specified</span>{{end}}</div>
|
||||||
|
</div>
|
||||||
|
<div class="activity-field">
|
||||||
|
<div class="activity-field-label">Personal Data Category</div>
|
||||||
|
<div class="activity-field-value">{{if .PersonalDataCategory}}{{.PersonalDataCategory}}{{else}}<span class="empty-value">Not specified</span>{{end}}</div>
|
||||||
|
</div>
|
||||||
|
<div class="activity-field">
|
||||||
|
<div class="activity-field-label">Special/Criminal Data</div>
|
||||||
|
<div class="activity-field-value">{{.SpecialOrCriminalData | formatSpecialOrCriminalData}}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="activity-section">
|
||||||
|
<div class="activity-section-title">2.{{add $index 1}}.3 Legal Basis</div>
|
||||||
|
<div class="activity-grid">
|
||||||
|
<div class="activity-field">
|
||||||
|
<div class="activity-field-label">Lawful Basis</div>
|
||||||
|
<div class="activity-field-value">{{.LawfulBasis | formatLawfulBasis}}</div>
|
||||||
|
</div>
|
||||||
|
<div class="activity-field">
|
||||||
|
<div class="activity-field-label">Consent Evidence Link</div>
|
||||||
|
<div class="activity-field-value">{{if .ConsentEvidenceLink}}{{.ConsentEvidenceLink}}{{else}}<span class="empty-value">Not specified</span>{{end}}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="activity-section">
|
||||||
|
<div class="activity-section-title">2.{{add $index 1}}.4 Data Sharing & Transfers</div>
|
||||||
|
<div class="activity-grid">
|
||||||
|
<div class="activity-field">
|
||||||
|
<div class="activity-field-label">Recipients</div>
|
||||||
|
<div class="activity-field-value">{{if .Recipients}}{{.Recipients}}{{else}}<span class="empty-value">Not specified</span>{{end}}</div>
|
||||||
|
</div>
|
||||||
|
<div class="activity-field">
|
||||||
|
<div class="activity-field-label">Location</div>
|
||||||
|
<div class="activity-field-value">{{if .Location}}{{.Location}}{{else}}<span class="empty-value">Not specified</span>{{end}}</div>
|
||||||
|
</div>
|
||||||
|
<div class="activity-field">
|
||||||
|
<div class="activity-field-label">International Transfers</div>
|
||||||
|
<div class="activity-field-value">{{if .InternationalTransfers}}Yes{{else}}No{{end}}</div>
|
||||||
|
</div>
|
||||||
|
<div class="activity-field">
|
||||||
|
<div class="activity-field-label">Transfer Safeguards</div>
|
||||||
|
<div class="activity-field-value">{{if .TransferSafeguards}}{{.TransferSafeguards | formatTransferSafeguard}}{{else}}<span class="empty-value">Not specified</span>{{end}}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="activity-section">
|
||||||
|
<div class="activity-section-title">2.{{add $index 1}}.5 Retention & Security</div>
|
||||||
|
<div class="activity-grid">
|
||||||
|
<div class="activity-field">
|
||||||
|
<div class="activity-field-label">Retention Period</div>
|
||||||
|
<div class="activity-field-value">{{if .RetentionPeriod}}{{.RetentionPeriod}}{{else}}<span class="empty-value">Not specified</span>{{end}}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="activity-field">
|
||||||
|
<div class="activity-field-label">Security Measures</div>
|
||||||
|
<div class="activity-field-value">{{if .SecurityMeasures}}{{.SecurityMeasures}}{{else}}<span class="empty-value">Not specified</span>{{end}}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="activity-section">
|
||||||
|
<div class="activity-section-title">2.{{add $index 1}}.6 Assessments & Reviews</div>
|
||||||
|
<div class="activity-grid">
|
||||||
|
<div class="activity-field">
|
||||||
|
<div class="activity-field-label">DPIA Needed</div>
|
||||||
|
<div class="activity-field-value">{{.DataProtectionImpactAssessmentNeeded | formatDPIANeeded}}</div>
|
||||||
|
</div>
|
||||||
|
<div class="activity-field">
|
||||||
|
<div class="activity-field-label">TIA Needed</div>
|
||||||
|
<div class="activity-field-value">{{.TransferImpactAssessmentNeeded | formatTIANeeded}}</div>
|
||||||
|
</div>
|
||||||
|
<div class="activity-field">
|
||||||
|
<div class="activity-field-label">Last Review Date</div>
|
||||||
|
<div class="activity-field-value">{{if .LastReviewDate}}{{.LastReviewDate.Format "January 2, 2006"}}{{else}}<span class="empty-value">Not specified</span>{{end}}</div>
|
||||||
|
</div>
|
||||||
|
<div class="activity-field">
|
||||||
|
<div class="activity-field-label">Next Review Date</div>
|
||||||
|
<div class="activity-field-value">{{if .NextReviewDate}}{{.NextReviewDate.Format "January 2, 2006"}}{{else}}<span class="empty-value">Not specified</span>{{end}}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="activity-section">
|
||||||
|
<div class="activity-section-title">2.{{add $index 1}}.7 Responsible Parties</div>
|
||||||
|
<div class="activity-grid">
|
||||||
|
<div class="activity-field">
|
||||||
|
<div class="activity-field-label">Data Protection Officer</div>
|
||||||
|
<div class="activity-field-value">{{if .DataProtectionOfficerFullName}}{{.DataProtectionOfficerFullName}}{{else}}<span class="empty-value">Not assigned</span>{{end}}</div>
|
||||||
|
</div>
|
||||||
|
<div class="activity-field">
|
||||||
|
<div class="activity-field-label">Vendors</div>
|
||||||
|
<div class="activity-field-value">{{if .Vendors}}{{.Vendors}}{{else}}<span class="empty-value">None</span>{{end}}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{{- end}}
|
||||||
|
|
||||||
|
<div class="annex-page">
|
||||||
|
<h1 class="annex-title">3. Annexes</h1>
|
||||||
|
|
||||||
|
<div class="annex-section">
|
||||||
|
<div class="annex-section-title">3.1 Lexicon</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="annex-section">
|
||||||
|
<div class="annex-subsection-title">Role</div>
|
||||||
|
<ul class="annex-enum-list">
|
||||||
|
<li class="annex-enum-item">
|
||||||
|
<span class="annex-enum-name">Controller:</span>
|
||||||
|
<span class="annex-enum-description">The entity that determines the purposes and means of processing personal data.</span>
|
||||||
|
</li>
|
||||||
|
<li class="annex-enum-item">
|
||||||
|
<span class="annex-enum-name">Processor:</span>
|
||||||
|
<span class="annex-enum-description">The entity that processes personal data on behalf of the controller.</span>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="annex-section">
|
||||||
|
<div class="annex-subsection-title">Lawful Basis</div>
|
||||||
|
<ul class="annex-enum-list">
|
||||||
|
<li class="annex-enum-item">
|
||||||
|
<span class="annex-enum-name">Consent:</span>
|
||||||
|
<span class="annex-enum-description">The data subject has given consent to the processing of their personal data.</span>
|
||||||
|
</li>
|
||||||
|
<li class="annex-enum-item">
|
||||||
|
<span class="annex-enum-name">Contractual Necessity:</span>
|
||||||
|
<span class="annex-enum-description">Processing is necessary for the performance of a contract to which the data subject is party.</span>
|
||||||
|
</li>
|
||||||
|
<li class="annex-enum-item">
|
||||||
|
<span class="annex-enum-name">Legal Obligation:</span>
|
||||||
|
<span class="annex-enum-description">Processing is necessary for compliance with a legal obligation to which the controller is subject.</span>
|
||||||
|
</li>
|
||||||
|
<li class="annex-enum-item">
|
||||||
|
<span class="annex-enum-name">Legitimate Interest:</span>
|
||||||
|
<span class="annex-enum-description">Processing is necessary for the purposes of the legitimate interests pursued by the controller or a third party.</span>
|
||||||
|
</li>
|
||||||
|
<li class="annex-enum-item">
|
||||||
|
<span class="annex-enum-name">Public Task:</span>
|
||||||
|
<span class="annex-enum-description">Processing is necessary for the performance of a task carried out in the public interest or in the exercise of official authority.</span>
|
||||||
|
</li>
|
||||||
|
<li class="annex-enum-item">
|
||||||
|
<span class="annex-enum-name">Vital Interests:</span>
|
||||||
|
<span class="annex-enum-description">Processing is necessary to protect the vital interests of the data subject or of another natural person.</span>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="annex-section">
|
||||||
|
<div class="annex-subsection-title">Special/Criminal Data</div>
|
||||||
|
<ul class="annex-enum-list">
|
||||||
|
<li class="annex-enum-item">
|
||||||
|
<span class="annex-enum-name">Yes:</span>
|
||||||
|
<span class="annex-enum-description">The processing activity involves special categories of personal data or data relating to criminal convictions and offences.</span>
|
||||||
|
</li>
|
||||||
|
<li class="annex-enum-item">
|
||||||
|
<span class="annex-enum-name">No:</span>
|
||||||
|
<span class="annex-enum-description">The processing activity does not involve special categories of personal data or data relating to criminal convictions and offences.</span>
|
||||||
|
</li>
|
||||||
|
<li class="annex-enum-item">
|
||||||
|
<span class="annex-enum-name">Possible:</span>
|
||||||
|
<span class="annex-enum-description">The processing activity may involve special categories of personal data or data relating to criminal convictions and offences.</span>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="annex-section">
|
||||||
|
<div class="annex-subsection-title">Transfer Safeguards</div>
|
||||||
|
<ul class="annex-enum-list">
|
||||||
|
<li class="annex-enum-item">
|
||||||
|
<span class="annex-enum-name">Standard Contractual Clauses:</span>
|
||||||
|
<span class="annex-enum-description">Standard contractual clauses approved by the European Commission are used to ensure adequate protection for international transfers.</span>
|
||||||
|
</li>
|
||||||
|
<li class="annex-enum-item">
|
||||||
|
<span class="annex-enum-name">Binding Corporate Rules:</span>
|
||||||
|
<span class="annex-enum-description">Binding corporate rules approved by a supervisory authority are used to ensure adequate protection for international transfers.</span>
|
||||||
|
</li>
|
||||||
|
<li class="annex-enum-item">
|
||||||
|
<span class="annex-enum-name">Adequacy Decision:</span>
|
||||||
|
<span class="annex-enum-description">The European Commission has determined that the third country ensures an adequate level of protection.</span>
|
||||||
|
</li>
|
||||||
|
<li class="annex-enum-item">
|
||||||
|
<span class="annex-enum-name">Derogations:</span>
|
||||||
|
<span class="annex-enum-description">A derogation under Article 49 of the GDPR is used for the international transfer.</span>
|
||||||
|
</li>
|
||||||
|
<li class="annex-enum-item">
|
||||||
|
<span class="annex-enum-name">Codes of Conduct:</span>
|
||||||
|
<span class="annex-enum-description">An approved code of conduct together with binding and enforceable commitments is used to ensure adequate protection.</span>
|
||||||
|
</li>
|
||||||
|
<li class="annex-enum-item">
|
||||||
|
<span class="annex-enum-name">Certification Mechanisms:</span>
|
||||||
|
<span class="annex-enum-description">An approved certification mechanism together with binding and enforceable commitments is used to ensure adequate protection.</span>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="annex-section">
|
||||||
|
<div class="annex-subsection-title">DPIA Needed</div>
|
||||||
|
<ul class="annex-enum-list">
|
||||||
|
<li class="annex-enum-item">
|
||||||
|
<span class="annex-enum-name">Yes:</span>
|
||||||
|
<span class="annex-enum-description">A Data Protection Impact Assessment is required for this processing activity.</span>
|
||||||
|
</li>
|
||||||
|
<li class="annex-enum-item">
|
||||||
|
<span class="annex-enum-name">No:</span>
|
||||||
|
<span class="annex-enum-description">A Data Protection Impact Assessment is not required for this processing activity.</span>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="annex-section">
|
||||||
|
<div class="annex-subsection-title">TIA Needed</div>
|
||||||
|
<ul class="annex-enum-list">
|
||||||
|
<li class="annex-enum-item">
|
||||||
|
<span class="annex-enum-name">Yes:</span>
|
||||||
|
<span class="annex-enum-description">A Transfer Impact Assessment is required for this processing activity.</span>
|
||||||
|
</li>
|
||||||
|
<li class="annex-enum-item">
|
||||||
|
<span class="annex-enum-name">No:</span>
|
||||||
|
<span class="annex-enum-description">A Transfer Impact Assessment is not required for this processing activity.</span>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
288
pkg/docgen/transfer_impact_assessments_template.html
Normal file
288
pkg/docgen/transfer_impact_assessments_template.html
Normal file
@@ -0,0 +1,288 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>Transfer Impact Assessments Export</title>
|
||||||
|
<style>
|
||||||
|
@page {
|
||||||
|
size: A4;
|
||||||
|
margin: 2.5cm;
|
||||||
|
@bottom-right {
|
||||||
|
content: "Page " counter(page) " of " counter(pages);
|
||||||
|
font-family: Arial, sans-serif;
|
||||||
|
font-size: 9pt;
|
||||||
|
color: #666;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
font-family: Arial, sans-serif;
|
||||||
|
font-size: 7.5pt;
|
||||||
|
line-height: 1.4;
|
||||||
|
color: #333;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
background: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Cover page */
|
||||||
|
.cover-page {
|
||||||
|
page-break-after: always;
|
||||||
|
}
|
||||||
|
|
||||||
|
.company-header {
|
||||||
|
margin-bottom: 30px;
|
||||||
|
page-break-after: avoid;
|
||||||
|
}
|
||||||
|
|
||||||
|
.company-logo {
|
||||||
|
max-height: 50px;
|
||||||
|
max-width: 250px;
|
||||||
|
object-fit: contain;
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.company-name {
|
||||||
|
font-size: 16pt;
|
||||||
|
color: #1a1a1a;
|
||||||
|
margin-top: 10px;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.export-title {
|
||||||
|
font-size: 22pt;
|
||||||
|
font-weight: normal;
|
||||||
|
color: #1a1a1a;
|
||||||
|
margin: 0 0 25px 0;
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
|
||||||
|
.document-meta {
|
||||||
|
margin: 0 0 30px 0;
|
||||||
|
font-size: 9pt;
|
||||||
|
}
|
||||||
|
|
||||||
|
.meta-table {
|
||||||
|
width: 100%;
|
||||||
|
border-collapse: collapse;
|
||||||
|
border: 1px solid #333;
|
||||||
|
}
|
||||||
|
|
||||||
|
.meta-table td {
|
||||||
|
padding: 6px 8px;
|
||||||
|
border: 1px solid #333;
|
||||||
|
vertical-align: middle;
|
||||||
|
}
|
||||||
|
|
||||||
|
.meta-table td:first-child {
|
||||||
|
font-weight: 600;
|
||||||
|
width: 25%;
|
||||||
|
background: #f8f8f8;
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
|
||||||
|
.classification {
|
||||||
|
font-weight: bold;
|
||||||
|
text-transform: uppercase;
|
||||||
|
}
|
||||||
|
|
||||||
|
.purpose-section {
|
||||||
|
margin: 30px 0;
|
||||||
|
page-break-after: avoid;
|
||||||
|
}
|
||||||
|
|
||||||
|
.purpose-title {
|
||||||
|
font-size: 15pt;
|
||||||
|
font-weight: bold;
|
||||||
|
color: #000;
|
||||||
|
margin: 0 0 15px 0;
|
||||||
|
page-break-after: avoid;
|
||||||
|
}
|
||||||
|
|
||||||
|
.purpose-text {
|
||||||
|
font-size: 10pt;
|
||||||
|
color: #333;
|
||||||
|
line-height: 1.5;
|
||||||
|
text-align: justify;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/* Assessment page */
|
||||||
|
.assessment-page {
|
||||||
|
page-break-before: always;
|
||||||
|
page-break-after: always;
|
||||||
|
}
|
||||||
|
|
||||||
|
.assessment-page:last-child {
|
||||||
|
page-break-after: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.assessment-title {
|
||||||
|
font-size: 15pt;
|
||||||
|
font-weight: bold;
|
||||||
|
color: #000;
|
||||||
|
margin: 0 0 15px 0;
|
||||||
|
page-break-after: avoid;
|
||||||
|
}
|
||||||
|
|
||||||
|
.assessment-field {
|
||||||
|
margin-bottom: 8px;
|
||||||
|
page-break-inside: avoid;
|
||||||
|
}
|
||||||
|
|
||||||
|
.assessment-field-label {
|
||||||
|
font-size: 10pt;
|
||||||
|
font-weight: bold;
|
||||||
|
color: #333;
|
||||||
|
margin: 12px 0 6px 0;
|
||||||
|
page-break-after: avoid;
|
||||||
|
page-break-inside: avoid;
|
||||||
|
}
|
||||||
|
|
||||||
|
.assessment-field-value {
|
||||||
|
color: #000;
|
||||||
|
font-size: 7.5pt;
|
||||||
|
line-height: 1.4;
|
||||||
|
}
|
||||||
|
|
||||||
|
.empty-value {
|
||||||
|
color: #999;
|
||||||
|
font-style: italic;
|
||||||
|
}
|
||||||
|
|
||||||
|
.annex-page {
|
||||||
|
page-break-before: always;
|
||||||
|
}
|
||||||
|
|
||||||
|
.annex-title {
|
||||||
|
font-size: 15pt;
|
||||||
|
font-weight: bold;
|
||||||
|
color: #000;
|
||||||
|
margin: 0 0 15px 0;
|
||||||
|
page-break-after: avoid;
|
||||||
|
}
|
||||||
|
|
||||||
|
.annex-section {
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.annex-section-title {
|
||||||
|
font-size: 13pt;
|
||||||
|
font-weight: bold;
|
||||||
|
color: #000;
|
||||||
|
margin: 15px 0 10px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.annex-subsection-title {
|
||||||
|
font-size: 10pt;
|
||||||
|
font-weight: bold;
|
||||||
|
color: #333;
|
||||||
|
margin: 12px 0 8px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.annex-enum-list {
|
||||||
|
margin: 10px 0;
|
||||||
|
padding-left: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.annex-enum-item {
|
||||||
|
margin-bottom: 8px;
|
||||||
|
font-size: 10pt;
|
||||||
|
line-height: 1.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.annex-enum-name {
|
||||||
|
font-weight: 600;
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
|
||||||
|
.annex-enum-description {
|
||||||
|
color: #000;
|
||||||
|
margin-left: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media print {
|
||||||
|
body {
|
||||||
|
background: white;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="cover-page">
|
||||||
|
<div class="company-header">
|
||||||
|
{{- if .CompanyHorizontalLogoBase64}}
|
||||||
|
{{imgTag .CompanyHorizontalLogoBase64 "Company Logo" "company-logo"}}
|
||||||
|
{{- else}}
|
||||||
|
<div class="company-name">{{.CompanyName}}</div>
|
||||||
|
{{- end}}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h1 class="export-title">Transfer Impact Assessments</h1>
|
||||||
|
|
||||||
|
<div class="document-meta">
|
||||||
|
<table class="meta-table">
|
||||||
|
<tr>
|
||||||
|
<td>Classification</td>
|
||||||
|
<td>
|
||||||
|
<span class="classification">CONFIDENTIAL</span>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>Version</td>
|
||||||
|
<td>{{.Version}}</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>Published</td>
|
||||||
|
<td>{{.PublishedAt.Format "January 2, 2006"}}</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="purpose-section">
|
||||||
|
<div class="purpose-title">1. Purpose</div>
|
||||||
|
<div class="purpose-text">
|
||||||
|
This document contains Transfer Impact Assessments (TIAs) conducted for processing activities involving
|
||||||
|
international transfers of personal data to countries outside the European Economic Area (EEA). TIAs
|
||||||
|
evaluate the legal mechanisms used for transfers, assess risks related to local laws in destination
|
||||||
|
countries, and document supplementary measures implemented to ensure an adequate level of data protection.
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{{- range $index, $assessment := .Assessments}}
|
||||||
|
<div class="assessment-page">
|
||||||
|
{{if eq $index 0}}
|
||||||
|
<h1 class="annex-title">2. Records</h1>
|
||||||
|
{{end}}
|
||||||
|
<h1 class="annex-section-title">2.{{add $index 1}} {{$assessment.ProcessingActivityName}}</h1>
|
||||||
|
|
||||||
|
<div class="assessment-field">
|
||||||
|
<div class="assessment-field-label">Data Subjects</div>
|
||||||
|
<div class="assessment-field-value">{{if .DataSubjects}}{{.DataSubjects}}{{else}}<span class="empty-value">Not specified</span>{{end}}</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="assessment-field">
|
||||||
|
<div class="assessment-field-label">Transfer</div>
|
||||||
|
<div class="assessment-field-value">{{if .Transfer}}{{.Transfer}}{{else}}<span class="empty-value">Not specified</span>{{end}}</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="assessment-field">
|
||||||
|
<div class="assessment-field-label">Legal Mechanism</div>
|
||||||
|
<div class="assessment-field-value">{{if .LegalMechanism}}{{.LegalMechanism}}{{else}}<span class="empty-value">Not specified</span>{{end}}</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="assessment-field">
|
||||||
|
<div class="assessment-field-label">Local Law Risk</div>
|
||||||
|
<div class="assessment-field-value">{{if .LocalLawRisk}}{{.LocalLawRisk}}{{else}}<span class="empty-value">Not specified</span>{{end}}</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="assessment-field">
|
||||||
|
<div class="assessment-field-label">Supplementary Measures</div>
|
||||||
|
<div class="assessment-field-value">{{if .SupplementaryMeasures}}{{.SupplementaryMeasures}}{{else}}<span class="empty-value">Not specified</span>{{end}}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{{- end}}
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -17,17 +17,21 @@ package probo
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"io"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"go.gearno.de/kit/pg"
|
"go.gearno.de/kit/pg"
|
||||||
"go.probo.inc/probo/pkg/coredata"
|
"go.probo.inc/probo/pkg/coredata"
|
||||||
|
"go.probo.inc/probo/pkg/docgen"
|
||||||
"go.probo.inc/probo/pkg/gid"
|
"go.probo.inc/probo/pkg/gid"
|
||||||
|
"go.probo.inc/probo/pkg/html2pdf"
|
||||||
"go.probo.inc/probo/pkg/page"
|
"go.probo.inc/probo/pkg/page"
|
||||||
"go.probo.inc/probo/pkg/validator"
|
"go.probo.inc/probo/pkg/validator"
|
||||||
)
|
)
|
||||||
|
|
||||||
type DataProtectionImpactAssessmentService struct {
|
type DataProtectionImpactAssessmentService struct {
|
||||||
svc *TenantService
|
svc *TenantService
|
||||||
|
html2pdfConverter *html2pdf.Converter
|
||||||
}
|
}
|
||||||
|
|
||||||
type (
|
type (
|
||||||
@@ -298,3 +302,130 @@ func (s *DataProtectionImpactAssessmentService) Delete(
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *DataProtectionImpactAssessmentService) ExportPDF(
|
||||||
|
ctx context.Context,
|
||||||
|
organizationID gid.GID,
|
||||||
|
filter *coredata.DataProtectionImpactAssessmentFilter,
|
||||||
|
) ([]byte, error) {
|
||||||
|
var tableData docgen.DataProtectionImpactAssessmentTableData
|
||||||
|
|
||||||
|
err := s.svc.pg.WithTx(
|
||||||
|
ctx,
|
||||||
|
func(conn pg.Conn) error {
|
||||||
|
var assessments coredata.DataProtectionImpactAssessments
|
||||||
|
if err := assessments.LoadAllByOrganizationID(ctx, conn, s.svc.scope, organizationID, filter); err != nil {
|
||||||
|
return fmt.Errorf("cannot load data protection impact assessments: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(assessments) == 0 {
|
||||||
|
return &coredata.ErrNoDataProtectionImpactAssessmentsFound{}
|
||||||
|
}
|
||||||
|
|
||||||
|
organization := &coredata.Organization{}
|
||||||
|
if err := organization.LoadByID(ctx, conn, s.svc.scope, organizationID); err != nil {
|
||||||
|
return fmt.Errorf("cannot load organization: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
horizontalLogoBase64 := ""
|
||||||
|
if organization.HorizontalLogoFileID != nil {
|
||||||
|
fileRecord := &coredata.File{}
|
||||||
|
fileErr := s.svc.pg.WithConn(ctx, func(conn pg.Conn) error {
|
||||||
|
return fileRecord.LoadByID(ctx, conn, s.svc.scope, *organization.HorizontalLogoFileID)
|
||||||
|
})
|
||||||
|
if fileErr == nil {
|
||||||
|
base64Data, mimeType, logoErr := s.svc.fileManager.GetFileBase64(ctx, fileRecord)
|
||||||
|
if logoErr == nil {
|
||||||
|
horizontalLogoBase64 = fmt.Sprintf("data:%s;base64,%s", mimeType, base64Data)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var snapshots coredata.Snapshots
|
||||||
|
snapshotType := coredata.SnapshotsTypeProcessingActivities
|
||||||
|
|
||||||
|
var version int
|
||||||
|
var publishedAt time.Time
|
||||||
|
|
||||||
|
if snapshotID := filter.SnapshotID(); snapshotID != nil {
|
||||||
|
snapshot := &coredata.Snapshot{}
|
||||||
|
if err := snapshot.LoadByID(ctx, conn, s.svc.scope, *snapshotID); err != nil {
|
||||||
|
return fmt.Errorf("cannot load snapshot: %w", err)
|
||||||
|
}
|
||||||
|
publishedAt = snapshot.CreatedAt
|
||||||
|
snapshotFilter := coredata.NewSnapshotFilter(&snapshotType).WithBeforeDate(&snapshot.CreatedAt)
|
||||||
|
snapshotCount, err := snapshots.CountByOrganizationID(ctx, conn, s.svc.scope, organizationID, snapshotFilter)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("cannot count processing activities snapshots: %w", err)
|
||||||
|
}
|
||||||
|
version = snapshotCount
|
||||||
|
} else {
|
||||||
|
publishedAt = time.Now()
|
||||||
|
snapshotFilter := coredata.NewSnapshotFilter(&snapshotType)
|
||||||
|
snapshotCount, err := snapshots.CountByOrganizationID(ctx, conn, s.svc.scope, organizationID, snapshotFilter)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("cannot count processing activities snapshots: %w", err)
|
||||||
|
}
|
||||||
|
version = snapshotCount + 1
|
||||||
|
}
|
||||||
|
|
||||||
|
assessmentRows := make([]docgen.DataProtectionImpactAssessmentRowData, len(assessments))
|
||||||
|
for i, assessment := range assessments {
|
||||||
|
processingActivity := &coredata.ProcessingActivity{}
|
||||||
|
if err := processingActivity.LoadByID(ctx, conn, s.svc.scope, assessment.ProcessingActivityID); err != nil {
|
||||||
|
return fmt.Errorf("cannot load processing activity: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
assessmentRows[i] = docgen.DataProtectionImpactAssessmentRowData{
|
||||||
|
ProcessingActivityName: processingActivity.Name,
|
||||||
|
Description: assessment.Description,
|
||||||
|
NecessityAndProportionality: assessment.NecessityAndProportionality,
|
||||||
|
PotentialRisk: assessment.PotentialRisk,
|
||||||
|
Mitigations: assessment.Mitigations,
|
||||||
|
ResidualRisk: assessment.ResidualRisk,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
tableData = docgen.DataProtectionImpactAssessmentTableData{
|
||||||
|
CompanyName: organization.Name,
|
||||||
|
CompanyHorizontalLogoBase64: horizontalLogoBase64,
|
||||||
|
Version: version,
|
||||||
|
PublishedAt: publishedAt,
|
||||||
|
Assessments: assessmentRows,
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
htmlContent, err := docgen.RenderDataProtectionImpactAssessmentsTableHTML(tableData)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("cannot render HTML: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
cfg := html2pdf.RenderConfig{
|
||||||
|
PageFormat: html2pdf.PageFormatA4,
|
||||||
|
Orientation: html2pdf.OrientationPortrait,
|
||||||
|
MarginTop: html2pdf.NewMarginInches(0.98),
|
||||||
|
MarginBottom: html2pdf.NewMarginInches(0.98),
|
||||||
|
MarginLeft: html2pdf.NewMarginInches(0.98),
|
||||||
|
MarginRight: html2pdf.NewMarginInches(0.98),
|
||||||
|
PrintBackground: true,
|
||||||
|
Scale: 1.0,
|
||||||
|
}
|
||||||
|
|
||||||
|
pdfReader, err := s.html2pdfConverter.GeneratePDF(ctx, htmlContent, cfg)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("cannot generate PDF: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
pdfData, err := io.ReadAll(pdfReader)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("cannot read PDF data: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return pdfData, nil
|
||||||
|
}
|
||||||
|
|||||||
@@ -17,17 +17,22 @@ package probo
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"go.gearno.de/kit/pg"
|
"go.gearno.de/kit/pg"
|
||||||
"go.probo.inc/probo/pkg/coredata"
|
"go.probo.inc/probo/pkg/coredata"
|
||||||
|
"go.probo.inc/probo/pkg/docgen"
|
||||||
"go.probo.inc/probo/pkg/gid"
|
"go.probo.inc/probo/pkg/gid"
|
||||||
|
"go.probo.inc/probo/pkg/html2pdf"
|
||||||
"go.probo.inc/probo/pkg/page"
|
"go.probo.inc/probo/pkg/page"
|
||||||
"go.probo.inc/probo/pkg/validator"
|
"go.probo.inc/probo/pkg/validator"
|
||||||
)
|
)
|
||||||
|
|
||||||
type ProcessingActivityService struct {
|
type ProcessingActivityService struct {
|
||||||
svc *TenantService
|
svc *TenantService
|
||||||
|
html2pdfConverter *html2pdf.Converter
|
||||||
}
|
}
|
||||||
|
|
||||||
type (
|
type (
|
||||||
@@ -382,3 +387,159 @@ func (s ProcessingActivityService) CountForOrganizationID(
|
|||||||
|
|
||||||
return count, nil
|
return count, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *ProcessingActivityService) ExportPDF(
|
||||||
|
ctx context.Context,
|
||||||
|
organizationID gid.GID,
|
||||||
|
filter *coredata.ProcessingActivityFilter,
|
||||||
|
) ([]byte, error) {
|
||||||
|
var tableData docgen.ProcessingActivityTableData
|
||||||
|
|
||||||
|
err := s.svc.pg.WithTx(
|
||||||
|
ctx,
|
||||||
|
func(conn pg.Conn) error {
|
||||||
|
var processingActivities coredata.ProcessingActivities
|
||||||
|
if err := processingActivities.LoadAllByOrganizationID(ctx, conn, s.svc.scope, organizationID, filter); err != nil {
|
||||||
|
return fmt.Errorf("cannot load processing activities: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(processingActivities) == 0 {
|
||||||
|
return &coredata.ErrNoProcessingActivitiesFound{}
|
||||||
|
}
|
||||||
|
|
||||||
|
organization := &coredata.Organization{}
|
||||||
|
if err := organization.LoadByID(ctx, conn, s.svc.scope, organizationID); err != nil {
|
||||||
|
return fmt.Errorf("cannot load organization: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
horizontalLogoBase64 := ""
|
||||||
|
if organization.HorizontalLogoFileID != nil {
|
||||||
|
fileRecord := &coredata.File{}
|
||||||
|
fileErr := s.svc.pg.WithConn(ctx, func(conn pg.Conn) error {
|
||||||
|
return fileRecord.LoadByID(ctx, conn, s.svc.scope, *organization.HorizontalLogoFileID)
|
||||||
|
})
|
||||||
|
if fileErr == nil {
|
||||||
|
base64Data, mimeType, logoErr := s.svc.fileManager.GetFileBase64(ctx, fileRecord)
|
||||||
|
if logoErr == nil {
|
||||||
|
horizontalLogoBase64 = fmt.Sprintf("data:%s;base64,%s", mimeType, base64Data)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var vendors coredata.Vendors
|
||||||
|
vendorMap, err := vendors.LoadAllByProcessingActivities(ctx, conn, s.svc.scope, organizationID, filter)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("cannot load vendors: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
var snapshots coredata.Snapshots
|
||||||
|
snapshotType := coredata.SnapshotsTypeProcessingActivities
|
||||||
|
|
||||||
|
var version int
|
||||||
|
var publishedAt time.Time
|
||||||
|
|
||||||
|
if snapshotID := filter.SnapshotID(); snapshotID != nil {
|
||||||
|
snapshot := &coredata.Snapshot{}
|
||||||
|
if err := snapshot.LoadByID(ctx, conn, s.svc.scope, *snapshotID); err != nil {
|
||||||
|
return fmt.Errorf("cannot load snapshot: %w", err)
|
||||||
|
}
|
||||||
|
publishedAt = snapshot.CreatedAt
|
||||||
|
snapshotFilter := coredata.NewSnapshotFilter(&snapshotType).WithBeforeDate(&snapshot.CreatedAt)
|
||||||
|
snapshotCount, err := snapshots.CountByOrganizationID(ctx, conn, s.svc.scope, organizationID, snapshotFilter)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("cannot count processing activities snapshots: %w", err)
|
||||||
|
}
|
||||||
|
version = snapshotCount
|
||||||
|
} else {
|
||||||
|
publishedAt = time.Now()
|
||||||
|
snapshotFilter := coredata.NewSnapshotFilter(&snapshotType)
|
||||||
|
snapshotCount, err := snapshots.CountByOrganizationID(ctx, conn, s.svc.scope, organizationID, snapshotFilter)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("cannot count processing activities snapshots: %w", err)
|
||||||
|
}
|
||||||
|
version = snapshotCount + 1
|
||||||
|
}
|
||||||
|
|
||||||
|
activities := make([]docgen.ProcessingActivityRowData, len(processingActivities))
|
||||||
|
for i, pa := range processingActivities {
|
||||||
|
dpoFullName := (*string)(nil)
|
||||||
|
if pa.DataProtectionOfficerID != nil {
|
||||||
|
dpo := &coredata.People{}
|
||||||
|
if err := dpo.LoadByID(ctx, conn, s.svc.scope, *pa.DataProtectionOfficerID); err == nil {
|
||||||
|
dpoFullName = &dpo.FullName
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
vendorsList := ""
|
||||||
|
if vendorNames, ok := vendorMap[pa.ID]; ok && len(vendorNames) > 0 {
|
||||||
|
vendorsList = strings.Join(vendorNames, ", ")
|
||||||
|
}
|
||||||
|
|
||||||
|
activities[i] = docgen.ProcessingActivityRowData{
|
||||||
|
Name: pa.Name,
|
||||||
|
Purpose: pa.Purpose,
|
||||||
|
DataSubjectCategory: pa.DataSubjectCategory,
|
||||||
|
PersonalDataCategory: pa.PersonalDataCategory,
|
||||||
|
SpecialOrCriminalData: pa.SpecialOrCriminalData,
|
||||||
|
ConsentEvidenceLink: pa.ConsentEvidenceLink,
|
||||||
|
LawfulBasis: pa.LawfulBasis,
|
||||||
|
Recipients: pa.Recipients,
|
||||||
|
Location: pa.Location,
|
||||||
|
InternationalTransfers: pa.InternationalTransfers,
|
||||||
|
TransferSafeguards: pa.TransferSafeguard,
|
||||||
|
RetentionPeriod: pa.RetentionPeriod,
|
||||||
|
SecurityMeasures: pa.SecurityMeasures,
|
||||||
|
DataProtectionImpactAssessmentNeeded: pa.DataProtectionImpactAssessmentNeeded,
|
||||||
|
TransferImpactAssessmentNeeded: pa.TransferImpactAssessmentNeeded,
|
||||||
|
LastReviewDate: pa.LastReviewDate,
|
||||||
|
NextReviewDate: pa.NextReviewDate,
|
||||||
|
Role: pa.Role,
|
||||||
|
DataProtectionOfficerFullName: dpoFullName,
|
||||||
|
Vendors: vendorsList,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
tableData = docgen.ProcessingActivityTableData{
|
||||||
|
CompanyName: organization.Name,
|
||||||
|
CompanyHorizontalLogoBase64: horizontalLogoBase64,
|
||||||
|
Version: version,
|
||||||
|
PublishedAt: publishedAt,
|
||||||
|
Activities: activities,
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
htmlContent, err := docgen.RenderProcessingActivitiesTableHTML(tableData)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("cannot generate HTML: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
cfg := html2pdf.RenderConfig{
|
||||||
|
PageFormat: html2pdf.PageFormatA4,
|
||||||
|
Orientation: html2pdf.OrientationPortrait,
|
||||||
|
MarginTop: html2pdf.NewMarginInches(0.98),
|
||||||
|
MarginBottom: html2pdf.NewMarginInches(0.98),
|
||||||
|
MarginLeft: html2pdf.NewMarginInches(0.98),
|
||||||
|
MarginRight: html2pdf.NewMarginInches(0.98),
|
||||||
|
PrintBackground: true,
|
||||||
|
Scale: 1.0,
|
||||||
|
}
|
||||||
|
|
||||||
|
pdfReader, err := s.html2pdfConverter.GeneratePDF(ctx, htmlContent, cfg)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("cannot generate PDF: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
pdfData, err := io.ReadAll(pdfReader)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("cannot read PDF data: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return pdfData, nil
|
||||||
|
}
|
||||||
|
|||||||
@@ -252,9 +252,18 @@ func (s *Service) WithTenant(tenantID gid.TenantID) *TenantService {
|
|||||||
tenantService.Snapshots = &SnapshotService{svc: tenantService}
|
tenantService.Snapshots = &SnapshotService{svc: tenantService}
|
||||||
tenantService.ContinualImprovements = &ContinualImprovementService{svc: tenantService}
|
tenantService.ContinualImprovements = &ContinualImprovementService{svc: tenantService}
|
||||||
tenantService.RightsRequests = &RightsRequestService{svc: tenantService}
|
tenantService.RightsRequests = &RightsRequestService{svc: tenantService}
|
||||||
tenantService.ProcessingActivities = &ProcessingActivityService{svc: tenantService}
|
tenantService.ProcessingActivities = &ProcessingActivityService{
|
||||||
tenantService.DataProtectionImpactAssessments = &DataProtectionImpactAssessmentService{svc: tenantService}
|
svc: tenantService,
|
||||||
tenantService.TransferImpactAssessments = &TransferImpactAssessmentService{svc: tenantService}
|
html2pdfConverter: s.html2pdfConverter,
|
||||||
|
}
|
||||||
|
tenantService.DataProtectionImpactAssessments = &DataProtectionImpactAssessmentService{
|
||||||
|
svc: tenantService,
|
||||||
|
html2pdfConverter: s.html2pdfConverter,
|
||||||
|
}
|
||||||
|
tenantService.TransferImpactAssessments = &TransferImpactAssessmentService{
|
||||||
|
svc: tenantService,
|
||||||
|
html2pdfConverter: s.html2pdfConverter,
|
||||||
|
}
|
||||||
tenantService.Files = &FileService{svc: tenantService}
|
tenantService.Files = &FileService{svc: tenantService}
|
||||||
tenantService.CustomDomains = &CustomDomainService{
|
tenantService.CustomDomains = &CustomDomainService{
|
||||||
svc: tenantService,
|
svc: tenantService,
|
||||||
|
|||||||
@@ -192,7 +192,8 @@ func (s *SnapshotService) CountForOrganizationID(
|
|||||||
ctx,
|
ctx,
|
||||||
func(conn pg.Conn) (err error) {
|
func(conn pg.Conn) (err error) {
|
||||||
snapshots := coredata.Snapshots{}
|
snapshots := coredata.Snapshots{}
|
||||||
count, err = snapshots.CountByOrganizationID(ctx, conn, s.svc.scope, organizationID)
|
filter := coredata.NewSnapshotFilter(nil)
|
||||||
|
count, err = snapshots.CountByOrganizationID(ctx, conn, s.svc.scope, organizationID, filter)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("cannot count snapshots: %w", err)
|
return fmt.Errorf("cannot count snapshots: %w", err)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,17 +17,21 @@ package probo
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"io"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"go.gearno.de/kit/pg"
|
"go.gearno.de/kit/pg"
|
||||||
"go.probo.inc/probo/pkg/coredata"
|
"go.probo.inc/probo/pkg/coredata"
|
||||||
|
"go.probo.inc/probo/pkg/docgen"
|
||||||
"go.probo.inc/probo/pkg/gid"
|
"go.probo.inc/probo/pkg/gid"
|
||||||
|
"go.probo.inc/probo/pkg/html2pdf"
|
||||||
"go.probo.inc/probo/pkg/page"
|
"go.probo.inc/probo/pkg/page"
|
||||||
"go.probo.inc/probo/pkg/validator"
|
"go.probo.inc/probo/pkg/validator"
|
||||||
)
|
)
|
||||||
|
|
||||||
type TransferImpactAssessmentService struct {
|
type TransferImpactAssessmentService struct {
|
||||||
svc *TenantService
|
svc *TenantService
|
||||||
|
html2pdfConverter *html2pdf.Converter
|
||||||
}
|
}
|
||||||
|
|
||||||
type (
|
type (
|
||||||
@@ -298,3 +302,130 @@ func (s *TransferImpactAssessmentService) Delete(
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *TransferImpactAssessmentService) ExportPDF(
|
||||||
|
ctx context.Context,
|
||||||
|
organizationID gid.GID,
|
||||||
|
filter *coredata.TransferImpactAssessmentFilter,
|
||||||
|
) ([]byte, error) {
|
||||||
|
var tableData docgen.TransferImpactAssessmentTableData
|
||||||
|
|
||||||
|
err := s.svc.pg.WithTx(
|
||||||
|
ctx,
|
||||||
|
func(conn pg.Conn) error {
|
||||||
|
var assessments coredata.TransferImpactAssessments
|
||||||
|
if err := assessments.LoadAllByOrganizationID(ctx, conn, s.svc.scope, organizationID, filter); err != nil {
|
||||||
|
return fmt.Errorf("cannot load transfer impact assessments: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(assessments) == 0 {
|
||||||
|
return &coredata.ErrNoTransferImpactAssessmentsFound{}
|
||||||
|
}
|
||||||
|
|
||||||
|
organization := &coredata.Organization{}
|
||||||
|
if err := organization.LoadByID(ctx, conn, s.svc.scope, organizationID); err != nil {
|
||||||
|
return fmt.Errorf("cannot load organization: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
horizontalLogoBase64 := ""
|
||||||
|
if organization.HorizontalLogoFileID != nil {
|
||||||
|
fileRecord := &coredata.File{}
|
||||||
|
fileErr := s.svc.pg.WithConn(ctx, func(conn pg.Conn) error {
|
||||||
|
return fileRecord.LoadByID(ctx, conn, s.svc.scope, *organization.HorizontalLogoFileID)
|
||||||
|
})
|
||||||
|
if fileErr == nil {
|
||||||
|
base64Data, mimeType, logoErr := s.svc.fileManager.GetFileBase64(ctx, fileRecord)
|
||||||
|
if logoErr == nil {
|
||||||
|
horizontalLogoBase64 = fmt.Sprintf("data:%s;base64,%s", mimeType, base64Data)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var snapshots coredata.Snapshots
|
||||||
|
snapshotType := coredata.SnapshotsTypeProcessingActivities
|
||||||
|
|
||||||
|
var version int
|
||||||
|
var publishedAt time.Time
|
||||||
|
|
||||||
|
if snapshotID := filter.SnapshotID(); snapshotID != nil {
|
||||||
|
snapshot := &coredata.Snapshot{}
|
||||||
|
if err := snapshot.LoadByID(ctx, conn, s.svc.scope, *snapshotID); err != nil {
|
||||||
|
return fmt.Errorf("cannot load snapshot: %w", err)
|
||||||
|
}
|
||||||
|
publishedAt = snapshot.CreatedAt
|
||||||
|
snapshotFilter := coredata.NewSnapshotFilter(&snapshotType).WithBeforeDate(&snapshot.CreatedAt)
|
||||||
|
snapshotCount, err := snapshots.CountByOrganizationID(ctx, conn, s.svc.scope, organizationID, snapshotFilter)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("cannot count processing activities snapshots: %w", err)
|
||||||
|
}
|
||||||
|
version = snapshotCount
|
||||||
|
} else {
|
||||||
|
publishedAt = time.Now()
|
||||||
|
snapshotFilter := coredata.NewSnapshotFilter(&snapshotType)
|
||||||
|
snapshotCount, err := snapshots.CountByOrganizationID(ctx, conn, s.svc.scope, organizationID, snapshotFilter)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("cannot count processing activities snapshots: %w", err)
|
||||||
|
}
|
||||||
|
version = snapshotCount + 1
|
||||||
|
}
|
||||||
|
|
||||||
|
assessmentRows := make([]docgen.TransferImpactAssessmentRowData, len(assessments))
|
||||||
|
for i, assessment := range assessments {
|
||||||
|
processingActivity := &coredata.ProcessingActivity{}
|
||||||
|
if err := processingActivity.LoadByID(ctx, conn, s.svc.scope, assessment.ProcessingActivityID); err != nil {
|
||||||
|
return fmt.Errorf("cannot load processing activity: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
assessmentRows[i] = docgen.TransferImpactAssessmentRowData{
|
||||||
|
ProcessingActivityName: processingActivity.Name,
|
||||||
|
DataSubjects: assessment.DataSubjects,
|
||||||
|
LegalMechanism: assessment.LegalMechanism,
|
||||||
|
Transfer: assessment.Transfer,
|
||||||
|
LocalLawRisk: assessment.LocalLawRisk,
|
||||||
|
SupplementaryMeasures: assessment.SupplementaryMeasures,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
tableData = docgen.TransferImpactAssessmentTableData{
|
||||||
|
CompanyName: organization.Name,
|
||||||
|
CompanyHorizontalLogoBase64: horizontalLogoBase64,
|
||||||
|
Version: version,
|
||||||
|
PublishedAt: publishedAt,
|
||||||
|
Assessments: assessmentRows,
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
htmlContent, err := docgen.RenderTransferImpactAssessmentsTableHTML(tableData)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("cannot render HTML: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
cfg := html2pdf.RenderConfig{
|
||||||
|
PageFormat: html2pdf.PageFormatA4,
|
||||||
|
Orientation: html2pdf.OrientationPortrait,
|
||||||
|
MarginTop: html2pdf.NewMarginInches(0.98),
|
||||||
|
MarginBottom: html2pdf.NewMarginInches(0.98),
|
||||||
|
MarginLeft: html2pdf.NewMarginInches(0.98),
|
||||||
|
MarginRight: html2pdf.NewMarginInches(0.98),
|
||||||
|
PrintBackground: true,
|
||||||
|
Scale: 1.0,
|
||||||
|
}
|
||||||
|
|
||||||
|
pdfReader, err := s.html2pdfConverter.GeneratePDF(ctx, htmlContent, cfg)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("cannot generate PDF: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
pdfData, err := io.ReadAll(pdfReader)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("cannot read PDF data: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return pdfData, nil
|
||||||
|
}
|
||||||
|
|||||||
@@ -3310,6 +3310,15 @@ type Mutation {
|
|||||||
exportSignableVersionDocumentPDF(
|
exportSignableVersionDocumentPDF(
|
||||||
input: ExportSignableDocumentVersionPDFInput!
|
input: ExportSignableDocumentVersionPDFInput!
|
||||||
): ExportSignableDocumentVersionPDFPayload!
|
): ExportSignableDocumentVersionPDFPayload!
|
||||||
|
exportProcessingActivitiesPDF(
|
||||||
|
input: ExportProcessingActivitiesPDFInput!
|
||||||
|
): ExportProcessingActivitiesPDFPayload!
|
||||||
|
exportDataProtectionImpactAssessmentsPDF(
|
||||||
|
input: ExportDataProtectionImpactAssessmentsPDFInput!
|
||||||
|
): ExportDataProtectionImpactAssessmentsPDFPayload!
|
||||||
|
exportTransferImpactAssessmentsPDF(
|
||||||
|
input: ExportTransferImpactAssessmentsPDFInput!
|
||||||
|
): ExportTransferImpactAssessmentsPDFPayload!
|
||||||
createVendorRiskAssessment(
|
createVendorRiskAssessment(
|
||||||
input: CreateVendorRiskAssessmentInput!
|
input: CreateVendorRiskAssessmentInput!
|
||||||
): CreateVendorRiskAssessmentPayload!
|
): CreateVendorRiskAssessmentPayload!
|
||||||
@@ -3941,6 +3950,21 @@ input ExportSignableDocumentVersionPDFInput {
|
|||||||
documentVersionId: ID!
|
documentVersionId: ID!
|
||||||
}
|
}
|
||||||
|
|
||||||
|
input ExportProcessingActivitiesPDFInput {
|
||||||
|
organizationId: ID!
|
||||||
|
filter: ProcessingActivityFilter
|
||||||
|
}
|
||||||
|
|
||||||
|
input ExportDataProtectionImpactAssessmentsPDFInput {
|
||||||
|
organizationId: ID!
|
||||||
|
filter: DataProtectionImpactAssessmentFilter
|
||||||
|
}
|
||||||
|
|
||||||
|
input ExportTransferImpactAssessmentsPDFInput {
|
||||||
|
organizationId: ID!
|
||||||
|
filter: TransferImpactAssessmentFilter
|
||||||
|
}
|
||||||
|
|
||||||
input DeleteDocumentInput {
|
input DeleteDocumentInput {
|
||||||
documentId: ID!
|
documentId: ID!
|
||||||
}
|
}
|
||||||
@@ -4600,6 +4624,18 @@ type ExportSignableDocumentVersionPDFPayload {
|
|||||||
data: String!
|
data: String!
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type ExportProcessingActivitiesPDFPayload {
|
||||||
|
data: String!
|
||||||
|
}
|
||||||
|
|
||||||
|
type ExportDataProtectionImpactAssessmentsPDFPayload {
|
||||||
|
data: String!
|
||||||
|
}
|
||||||
|
|
||||||
|
type ExportTransferImpactAssessmentsPDFPayload {
|
||||||
|
data: String!
|
||||||
|
}
|
||||||
|
|
||||||
type UpdateDocumentPayload {
|
type UpdateDocumentPayload {
|
||||||
document: Document!
|
document: Document!
|
||||||
}
|
}
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -1220,6 +1220,15 @@ type EvidenceEdge struct {
|
|||||||
Node *Evidence `json:"node"`
|
Node *Evidence `json:"node"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type ExportDataProtectionImpactAssessmentsPDFInput struct {
|
||||||
|
OrganizationID gid.GID `json:"organizationId"`
|
||||||
|
Filter *DataProtectionImpactAssessmentFilter `json:"filter,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type ExportDataProtectionImpactAssessmentsPDFPayload struct {
|
||||||
|
Data string `json:"data"`
|
||||||
|
}
|
||||||
|
|
||||||
type ExportDocumentVersionPDFInput struct {
|
type ExportDocumentVersionPDFInput struct {
|
||||||
DocumentVersionID gid.GID `json:"documentVersionId"`
|
DocumentVersionID gid.GID `json:"documentVersionId"`
|
||||||
WithWatermark bool `json:"withWatermark"`
|
WithWatermark bool `json:"withWatermark"`
|
||||||
@@ -1239,6 +1248,15 @@ type ExportFrameworkPayload struct {
|
|||||||
ExportJobID gid.GID `json:"exportJobId"`
|
ExportJobID gid.GID `json:"exportJobId"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type ExportProcessingActivitiesPDFInput struct {
|
||||||
|
OrganizationID gid.GID `json:"organizationId"`
|
||||||
|
Filter *ProcessingActivityFilter `json:"filter,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type ExportProcessingActivitiesPDFPayload struct {
|
||||||
|
Data string `json:"data"`
|
||||||
|
}
|
||||||
|
|
||||||
type ExportSignableDocumentVersionPDFInput struct {
|
type ExportSignableDocumentVersionPDFInput struct {
|
||||||
DocumentVersionID gid.GID `json:"documentVersionId"`
|
DocumentVersionID gid.GID `json:"documentVersionId"`
|
||||||
}
|
}
|
||||||
@@ -1247,6 +1265,15 @@ type ExportSignableDocumentVersionPDFPayload struct {
|
|||||||
Data string `json:"data"`
|
Data string `json:"data"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type ExportTransferImpactAssessmentsPDFInput struct {
|
||||||
|
OrganizationID gid.GID `json:"organizationId"`
|
||||||
|
Filter *TransferImpactAssessmentFilter `json:"filter,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type ExportTransferImpactAssessmentsPDFPayload struct {
|
||||||
|
Data string `json:"data"`
|
||||||
|
}
|
||||||
|
|
||||||
type File struct {
|
type File struct {
|
||||||
ID gid.GID `json:"id"`
|
ID gid.GID `json:"id"`
|
||||||
MimeType string `json:"mimeType"`
|
MimeType string `json:"mimeType"`
|
||||||
|
|||||||
@@ -3745,6 +3745,84 @@ func (r *mutationResolver) ExportSignableVersionDocumentPDF(ctx context.Context,
|
|||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ExportProcessingActivitiesPDF is the resolver for the exportProcessingActivitiesPDF field.
|
||||||
|
func (r *mutationResolver) ExportProcessingActivitiesPDF(ctx context.Context, input types.ExportProcessingActivitiesPDFInput) (*types.ExportProcessingActivitiesPDFPayload, error) {
|
||||||
|
r.MustBeAuthorized(ctx, input.OrganizationID, authz.ActionExportProcessingActivitiesPDF)
|
||||||
|
|
||||||
|
prb := r.ProboService(ctx, input.OrganizationID.TenantID())
|
||||||
|
|
||||||
|
var snapshotIDPtr *gid.GID
|
||||||
|
if input.Filter != nil {
|
||||||
|
snapshotIDPtr = input.Filter.SnapshotID
|
||||||
|
}
|
||||||
|
processingActivityFilter := coredata.NewProcessingActivityFilter(&snapshotIDPtr)
|
||||||
|
|
||||||
|
pdf, err := prb.ProcessingActivities.ExportPDF(ctx, input.OrganizationID, processingActivityFilter)
|
||||||
|
if err != nil {
|
||||||
|
var errNotFound *coredata.ErrNoProcessingActivitiesFound
|
||||||
|
if errors.As(err, &errNotFound) {
|
||||||
|
return nil, gqlutils.NotFound(errNotFound)
|
||||||
|
}
|
||||||
|
panic(fmt.Errorf("cannot export processing activities PDF: %w", err))
|
||||||
|
}
|
||||||
|
|
||||||
|
return &types.ExportProcessingActivitiesPDFPayload{
|
||||||
|
Data: fmt.Sprintf("data:application/pdf;base64,%s", base64.StdEncoding.EncodeToString(pdf)),
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ExportDataProtectionImpactAssessmentsPDF is the resolver for the exportDataProtectionImpactAssessmentsPDF field.
|
||||||
|
func (r *mutationResolver) ExportDataProtectionImpactAssessmentsPDF(ctx context.Context, input types.ExportDataProtectionImpactAssessmentsPDFInput) (*types.ExportDataProtectionImpactAssessmentsPDFPayload, error) {
|
||||||
|
r.MustBeAuthorized(ctx, input.OrganizationID, authz.ActionExportDataProtectionImpactAssessmentsPDF)
|
||||||
|
|
||||||
|
prb := r.ProboService(ctx, input.OrganizationID.TenantID())
|
||||||
|
|
||||||
|
var snapshotIDPtr *gid.GID
|
||||||
|
if input.Filter != nil {
|
||||||
|
snapshotIDPtr = input.Filter.SnapshotID
|
||||||
|
}
|
||||||
|
dpiaFilter := coredata.NewDataProtectionImpactAssessmentFilter(&snapshotIDPtr)
|
||||||
|
|
||||||
|
pdf, err := prb.DataProtectionImpactAssessments.ExportPDF(ctx, input.OrganizationID, dpiaFilter)
|
||||||
|
if err != nil {
|
||||||
|
var errNotFound *coredata.ErrNoDataProtectionImpactAssessmentsFound
|
||||||
|
if errors.As(err, &errNotFound) {
|
||||||
|
return nil, gqlutils.NotFound(errNotFound)
|
||||||
|
}
|
||||||
|
panic(fmt.Errorf("cannot export data protection impact assessments PDF: %w", err))
|
||||||
|
}
|
||||||
|
|
||||||
|
return &types.ExportDataProtectionImpactAssessmentsPDFPayload{
|
||||||
|
Data: fmt.Sprintf("data:application/pdf;base64,%s", base64.StdEncoding.EncodeToString(pdf)),
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ExportTransferImpactAssessmentsPDF is the resolver for the exportTransferImpactAssessmentsPDF field.
|
||||||
|
func (r *mutationResolver) ExportTransferImpactAssessmentsPDF(ctx context.Context, input types.ExportTransferImpactAssessmentsPDFInput) (*types.ExportTransferImpactAssessmentsPDFPayload, error) {
|
||||||
|
r.MustBeAuthorized(ctx, input.OrganizationID, authz.ActionExportTransferImpactAssessmentsPDF)
|
||||||
|
|
||||||
|
prb := r.ProboService(ctx, input.OrganizationID.TenantID())
|
||||||
|
|
||||||
|
var snapshotIDPtr *gid.GID
|
||||||
|
if input.Filter != nil {
|
||||||
|
snapshotIDPtr = input.Filter.SnapshotID
|
||||||
|
}
|
||||||
|
tiaFilter := coredata.NewTransferImpactAssessmentFilter(&snapshotIDPtr)
|
||||||
|
|
||||||
|
pdf, err := prb.TransferImpactAssessments.ExportPDF(ctx, input.OrganizationID, tiaFilter)
|
||||||
|
if err != nil {
|
||||||
|
var errNotFound *coredata.ErrNoTransferImpactAssessmentsFound
|
||||||
|
if errors.As(err, &errNotFound) {
|
||||||
|
return nil, gqlutils.NotFound(errNotFound)
|
||||||
|
}
|
||||||
|
panic(fmt.Errorf("cannot export transfer impact assessments PDF: %w", err))
|
||||||
|
}
|
||||||
|
|
||||||
|
return &types.ExportTransferImpactAssessmentsPDFPayload{
|
||||||
|
Data: fmt.Sprintf("data:application/pdf;base64,%s", base64.StdEncoding.EncodeToString(pdf)),
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
// CreateVendorRiskAssessment is the resolver for the createVendorRiskAssessment field.
|
// CreateVendorRiskAssessment is the resolver for the createVendorRiskAssessment field.
|
||||||
func (r *mutationResolver) CreateVendorRiskAssessment(ctx context.Context, input types.CreateVendorRiskAssessmentInput) (*types.CreateVendorRiskAssessmentPayload, error) {
|
func (r *mutationResolver) CreateVendorRiskAssessment(ctx context.Context, input types.CreateVendorRiskAssessmentInput) (*types.CreateVendorRiskAssessmentPayload, error) {
|
||||||
r.MustBeAuthorized(ctx, input.VendorID, authz.ActionCreateVendorRiskAssessment)
|
r.MustBeAuthorized(ctx, input.VendorID, authz.ActionCreateVendorRiskAssessment)
|
||||||
|
|||||||
Reference in New Issue
Block a user