Add processing activity exports
Signed-off-by: Sacha Al Himdani <sacha@getprobo.com>
This commit is contained in:
@@ -16,6 +16,10 @@ import {
|
||||
useConfirm,
|
||||
Tabs,
|
||||
TabItem,
|
||||
IconArrowDown,
|
||||
IconChevronDown,
|
||||
Spinner,
|
||||
Dropdown,
|
||||
} from "@probo/ui";
|
||||
import { useTranslate } from "@probo/i18n";
|
||||
import { usePageTitle } from "@probo/hooks";
|
||||
@@ -32,7 +36,8 @@ import { useOrganizationId } from "/hooks/useOrganizationId";
|
||||
import { useParams } from "react-router";
|
||||
import { CreateProcessingActivityDialog } from "./dialogs/CreateProcessingActivityDialog";
|
||||
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 type { NodeOf } from "/types";
|
||||
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) {
|
||||
const { __ } = useTranslate();
|
||||
const organizationId = useOrganizationId();
|
||||
@@ -233,12 +268,169 @@ export default function ProcessingActivitiesPage({ queryRef }: ProcessingActivit
|
||||
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 (
|
||||
<div className="space-y-6">
|
||||
{isSnapshotMode && snapshotId && (
|
||||
<SnapshotBanner snapshotId={snapshotId} />
|
||||
)}
|
||||
<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" && (
|
||||
isAuthorized("Organization", "createProcessingActivity") && (
|
||||
<CreateProcessingActivityDialog
|
||||
@@ -272,13 +464,13 @@ export default function ProcessingActivitiesPage({ queryRef }: ProcessingActivit
|
||||
<Table>
|
||||
<Thead>
|
||||
<Tr>
|
||||
<Th>{__("Name")}</Th>
|
||||
<Th>{__("Purpose")}</Th>
|
||||
<Th>{__("Data Subject")}</Th>
|
||||
<Th>{__("Lawful Basis")}</Th>
|
||||
<Th>{__("Location")}</Th>
|
||||
<Th>{__("International Transfers")}</Th>
|
||||
{hasAnyAction && <Th>{__("Actions")}</Th>}
|
||||
<Th className="px-3">{__("Name")}</Th>
|
||||
<Th className="px-3">{__("Purpose")}</Th>
|
||||
<Th className="px-3">{__("Data Subject")}</Th>
|
||||
<Th className="px-3">{__("Lawful Basis")}</Th>
|
||||
<Th className="px-3">{__("Location")}</Th>
|
||||
<Th className="px-3">{__("International Transfers")}</Th>
|
||||
{hasAnyAction && <Th className="px-3">{__("Actions")}</Th>}
|
||||
</Tr>
|
||||
</Thead>
|
||||
<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"
|
||||
ActionListContacts Action = "listContacts"
|
||||
ActionListContinualImprovements Action = "listContinualImprovements"
|
||||
ActionListRightsRequests Action = "listRightsRequests"
|
||||
ActionListRightsRequests Action = "listRightsRequests"
|
||||
ActionListControls Action = "listControls"
|
||||
ActionListData Action = "listData"
|
||||
ActionListDocuments Action = "listDocuments"
|
||||
@@ -235,39 +235,42 @@ const (
|
||||
ActionDeleteVendorDataPrivacyAgreement Action = "deleteVendorDataPrivacyAgreement"
|
||||
ActionDeleteVendorService Action = "deleteVendorService"
|
||||
|
||||
ActionAcceptInvitation Action = "acceptInvitation"
|
||||
ActionAssessVendor Action = "assessVendor"
|
||||
ActionAssignTask Action = "assignTask"
|
||||
ActionBulkDeleteDocuments Action = "bulkDeleteDocuments"
|
||||
ActionBulkExportDocuments Action = "bulkExportDocuments"
|
||||
ActionBulkPublishDocumentVersions Action = "bulkPublishDocumentVersions"
|
||||
ActionBulkRequestSignatures Action = "bulkRequestSignatures"
|
||||
ActionCancelSignatureRequest Action = "cancelSignatureRequest"
|
||||
ActionSignDocument Action = "signDocument"
|
||||
ActionConfirmEmail Action = "confirmEmail"
|
||||
ActionDisableSAML Action = "disableSAML"
|
||||
ActionEnableSAML Action = "enableSAML"
|
||||
ActionExportDocumentVersionPDF Action = "exportDocumentVersionPDF"
|
||||
ActionExportSignableVersionDocumentPDF Action = "exportSignableVersionDocumentPDF"
|
||||
ActionExportFramework Action = "exportFramework"
|
||||
ActionGenerateDocumentChangelog Action = "generateDocumentChangelog"
|
||||
ActionGenerateFrameworkStateOfApplicability Action = "generateFrameworkStateOfApplicability"
|
||||
ActionImportFramework Action = "importFramework"
|
||||
ActionImportMeasure Action = "importMeasure"
|
||||
ActionInitiateDomainVerification Action = "initiateDomainVerification"
|
||||
ActionInviteUser Action = "inviteUser"
|
||||
ActionPublishDocumentVersion Action = "publishDocumentVersion"
|
||||
ActionRemoveMember Action = "removeMember"
|
||||
ActionRequestSignature Action = "requestSignature"
|
||||
ActionSendSigningNotifications Action = "sendSigningNotifications"
|
||||
ActionUnassignTask Action = "unassignTask"
|
||||
ActionUploadAuditReport Action = "uploadAuditReport"
|
||||
ActionUploadMeasureEvidence Action = "uploadMeasureEvidence"
|
||||
ActionUploadTrustCenterNDA Action = "uploadTrustCenterNDA"
|
||||
ActionUploadVendorBusinessAssociateAgreement Action = "uploadVendorBusinessAssociateAgreement"
|
||||
ActionUploadVendorComplianceReport Action = "uploadVendorComplianceReport"
|
||||
ActionUploadVendorDataPrivacyAgreement Action = "uploadVendorDataPrivacyAgreement"
|
||||
ActionVerifyDomain Action = "verifyDomain"
|
||||
ActionAcceptInvitation Action = "acceptInvitation"
|
||||
ActionAssessVendor Action = "assessVendor"
|
||||
ActionAssignTask Action = "assignTask"
|
||||
ActionBulkDeleteDocuments Action = "bulkDeleteDocuments"
|
||||
ActionBulkExportDocuments Action = "bulkExportDocuments"
|
||||
ActionBulkPublishDocumentVersions Action = "bulkPublishDocumentVersions"
|
||||
ActionBulkRequestSignatures Action = "bulkRequestSignatures"
|
||||
ActionCancelSignatureRequest Action = "cancelSignatureRequest"
|
||||
ActionSignDocument Action = "signDocument"
|
||||
ActionConfirmEmail Action = "confirmEmail"
|
||||
ActionDisableSAML Action = "disableSAML"
|
||||
ActionEnableSAML Action = "enableSAML"
|
||||
ActionExportDocumentVersionPDF Action = "exportDocumentVersionPDF"
|
||||
ActionExportSignableVersionDocumentPDF Action = "exportSignableVersionDocumentPDF"
|
||||
ActionExportProcessingActivitiesPDF Action = "exportProcessingActivitiesPDF"
|
||||
ActionExportDataProtectionImpactAssessmentsPDF Action = "exportDataProtectionImpactAssessmentsPDF"
|
||||
ActionExportTransferImpactAssessmentsPDF Action = "exportTransferImpactAssessmentsPDF"
|
||||
ActionExportFramework Action = "exportFramework"
|
||||
ActionGenerateDocumentChangelog Action = "generateDocumentChangelog"
|
||||
ActionGenerateFrameworkStateOfApplicability Action = "generateFrameworkStateOfApplicability"
|
||||
ActionImportFramework Action = "importFramework"
|
||||
ActionImportMeasure Action = "importMeasure"
|
||||
ActionInitiateDomainVerification Action = "initiateDomainVerification"
|
||||
ActionInviteUser Action = "inviteUser"
|
||||
ActionPublishDocumentVersion Action = "publishDocumentVersion"
|
||||
ActionRemoveMember Action = "removeMember"
|
||||
ActionRequestSignature Action = "requestSignature"
|
||||
ActionSendSigningNotifications Action = "sendSigningNotifications"
|
||||
ActionUnassignTask Action = "unassignTask"
|
||||
ActionUploadAuditReport Action = "uploadAuditReport"
|
||||
ActionUploadMeasureEvidence Action = "uploadMeasureEvidence"
|
||||
ActionUploadTrustCenterNDA Action = "uploadTrustCenterNDA"
|
||||
ActionUploadVendorBusinessAssociateAgreement Action = "uploadVendorBusinessAssociateAgreement"
|
||||
ActionUploadVendorComplianceReport Action = "uploadVendorComplianceReport"
|
||||
ActionUploadVendorDataPrivacyAgreement Action = "uploadVendorDataPrivacyAgreement"
|
||||
ActionVerifyDomain Action = "verifyDomain"
|
||||
)
|
||||
|
||||
var (
|
||||
@@ -287,27 +290,30 @@ var Permissions = map[uint16]map[Action][]Role{
|
||||
|
||||
ActionListSignableDocuments: InternalRoles,
|
||||
|
||||
ActionListDocuments: NonEmployeeRoles,
|
||||
ActionGetHorizontalLogoUrl: NonEmployeeRoles,
|
||||
ActionPeoples: NonEmployeeRoles,
|
||||
ActionTotalCount: NonEmployeeRoles,
|
||||
ActionListFrameworks: NonEmployeeRoles,
|
||||
ActionListControls: NonEmployeeRoles,
|
||||
ActionListVendors: NonEmployeeRoles,
|
||||
ActionListPeople: NonEmployeeRoles,
|
||||
ActionListMeasures: NonEmployeeRoles,
|
||||
ActionListRisks: NonEmployeeRoles,
|
||||
ActionListAssets: NonEmployeeRoles,
|
||||
ActionListData: NonEmployeeRoles,
|
||||
ActionListAudits: NonEmployeeRoles,
|
||||
ActionListNonconformities: NonEmployeeRoles,
|
||||
ActionListObligations: NonEmployeeRoles,
|
||||
ActionListContinualImprovements: NonEmployeeRoles,
|
||||
ActionListRightsRequests: NonEmployeeRoles,
|
||||
ActionListProcessingActivities: NonEmployeeRoles,
|
||||
ActionListSnapshots: NonEmployeeRoles,
|
||||
ActionConfirmEmail: NonEmployeeRoles,
|
||||
ActionAcceptInvitation: NonEmployeeRoles,
|
||||
ActionListDocuments: NonEmployeeRoles,
|
||||
ActionGetHorizontalLogoUrl: NonEmployeeRoles,
|
||||
ActionPeoples: NonEmployeeRoles,
|
||||
ActionTotalCount: NonEmployeeRoles,
|
||||
ActionListFrameworks: NonEmployeeRoles,
|
||||
ActionListControls: NonEmployeeRoles,
|
||||
ActionListVendors: NonEmployeeRoles,
|
||||
ActionListPeople: NonEmployeeRoles,
|
||||
ActionListMeasures: NonEmployeeRoles,
|
||||
ActionListRisks: NonEmployeeRoles,
|
||||
ActionListAssets: NonEmployeeRoles,
|
||||
ActionListData: NonEmployeeRoles,
|
||||
ActionListAudits: NonEmployeeRoles,
|
||||
ActionListNonconformities: NonEmployeeRoles,
|
||||
ActionListObligations: NonEmployeeRoles,
|
||||
ActionListContinualImprovements: NonEmployeeRoles,
|
||||
ActionListRightsRequests: NonEmployeeRoles,
|
||||
ActionListProcessingActivities: NonEmployeeRoles,
|
||||
ActionExportProcessingActivitiesPDF: NonEmployeeRoles,
|
||||
ActionExportDataProtectionImpactAssessmentsPDF: NonEmployeeRoles,
|
||||
ActionExportTransferImpactAssessmentsPDF: NonEmployeeRoles,
|
||||
ActionListSnapshots: NonEmployeeRoles,
|
||||
ActionConfirmEmail: NonEmployeeRoles,
|
||||
ActionAcceptInvitation: NonEmployeeRoles,
|
||||
|
||||
ActionListTrustCenterFiles: CoreRoles,
|
||||
ActionGetTrustCenter: CoreRoles,
|
||||
@@ -694,6 +700,7 @@ var Permissions = map[uint16]map[Action][]Role{
|
||||
ActionGetDataProtectionOfficer: NonEmployeeRoles,
|
||||
ActionGetDataProtectionImpactAssessment: NonEmployeeRoles,
|
||||
ActionGetTransferImpactAssessment: NonEmployeeRoles,
|
||||
ActionExportProcessingActivitiesPDF: NonEmployeeRoles,
|
||||
|
||||
ActionUpdateProcessingActivity: EditRoles,
|
||||
ActionDeleteProcessingActivity: EditRoles,
|
||||
|
||||
@@ -35,6 +35,12 @@ func (e ErrDataProtectionImpactAssessmentNotFound) Error() string {
|
||||
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 (
|
||||
DataProtectionImpactAssessment struct {
|
||||
ID gid.GID `db:"id"`
|
||||
@@ -151,6 +157,56 @@ WHERE
|
||||
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(
|
||||
ctx context.Context,
|
||||
conn pg.Conn,
|
||||
@@ -429,4 +485,3 @@ WHERE dpia.tenant_id = @tenant_id AND dpia.organization_id = @organization_id AN
|
||||
|
||||
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"
|
||||
)
|
||||
|
||||
type ErrNoProcessingActivitiesFound struct{}
|
||||
|
||||
func (e ErrNoProcessingActivitiesFound) Error() string {
|
||||
return "no processing activities found"
|
||||
}
|
||||
|
||||
type (
|
||||
ProcessingActivity struct {
|
||||
ID gid.GID `db:"id"`
|
||||
@@ -231,6 +237,70 @@ WHERE
|
||||
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(
|
||||
ctx context.Context,
|
||||
conn pg.Conn,
|
||||
|
||||
@@ -52,3 +52,10 @@ func (f *ProcessingActivityFilter) SQLFragment() string {
|
||||
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"
|
||||
"time"
|
||||
|
||||
"go.probo.inc/probo/pkg/gid"
|
||||
"go.probo.inc/probo/pkg/page"
|
||||
"github.com/jackc/pgx/v5"
|
||||
"go.gearno.de/kit/pg"
|
||||
"go.probo.inc/probo/pkg/gid"
|
||||
"go.probo.inc/probo/pkg/page"
|
||||
)
|
||||
|
||||
type (
|
||||
@@ -99,6 +99,7 @@ func (s *Snapshots) CountByOrganizationID(
|
||||
conn pg.Conn,
|
||||
scope Scoper,
|
||||
organizationID gid.GID,
|
||||
filter *SnapshotFilter,
|
||||
) (int, error) {
|
||||
q := `
|
||||
SELECT
|
||||
@@ -108,12 +109,14 @@ FROM
|
||||
WHERE
|
||||
%s
|
||||
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}
|
||||
maps.Copy(args, scope.SQLArguments())
|
||||
maps.Copy(args, filter.SQLArguments())
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
type ErrNoTransferImpactAssessmentsFound struct{}
|
||||
|
||||
func (e ErrNoTransferImpactAssessmentsFound) Error() string {
|
||||
return "no transfer impact assessments found"
|
||||
}
|
||||
|
||||
type (
|
||||
TransferImpactAssessment struct {
|
||||
ID gid.GID `db:"id"`
|
||||
@@ -151,6 +157,56 @@ WHERE
|
||||
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(
|
||||
ctx context.Context,
|
||||
conn pg.Conn,
|
||||
@@ -429,4 +485,3 @@ WHERE tia.tenant_id = @tenant_id AND tia.organization_id = @organization_id AND
|
||||
|
||||
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"
|
||||
"time"
|
||||
|
||||
"go.probo.inc/probo/pkg/gid"
|
||||
"go.probo.inc/probo/pkg/page"
|
||||
"github.com/jackc/pgx/v5"
|
||||
"go.gearno.de/kit/pg"
|
||||
"go.probo.inc/probo/pkg/gid"
|
||||
"go.probo.inc/probo/pkg/page"
|
||||
)
|
||||
|
||||
type (
|
||||
@@ -850,6 +850,74 @@ WHERE %s
|
||||
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(
|
||||
ctx context.Context,
|
||||
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"
|
||||
"time"
|
||||
|
||||
"go.probo.inc/probo/pkg/coredata"
|
||||
"github.com/yuin/goldmark"
|
||||
"github.com/yuin/goldmark/extension"
|
||||
gmhtml "github.com/yuin/goldmark/renderer/html"
|
||||
"go.probo.inc/probo/pkg/coredata"
|
||||
)
|
||||
|
||||
const (
|
||||
firstPageTOCItems = 21
|
||||
otherPageTOCItems = 28
|
||||
)
|
||||
|
||||
var (
|
||||
//go:embed template.html
|
||||
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{
|
||||
"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() },
|
||||
"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) },
|
||||
"formatContent": func(content string) template.HTML {
|
||||
md := goldmark.New(
|
||||
@@ -56,9 +71,111 @@ var (
|
||||
"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)))
|
||||
},
|
||||
"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))
|
||||
|
||||
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 (
|
||||
@@ -82,6 +199,71 @@ type (
|
||||
State coredata.DocumentVersionSignatureState
|
||||
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 (
|
||||
@@ -99,3 +281,30 @@ func RenderHTML(data DocumentData) ([]byte, error) {
|
||||
|
||||
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) {
|
||||
eqFunc := templateFuncs["eq"].(func(string, string) bool)
|
||||
eqFunc := templateFuncs["eq"].(func(any, any) bool)
|
||||
assert.True(t, eqFunc("test", "test"))
|
||||
assert.False(t, eqFunc("test", "other"))
|
||||
})
|
||||
@@ -266,7 +266,7 @@ func TestHTMLEscaping(t *testing.T) {
|
||||
|
||||
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, "<malicious>tag")
|
||||
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 (
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"time"
|
||||
|
||||
"go.gearno.de/kit/pg"
|
||||
"go.probo.inc/probo/pkg/coredata"
|
||||
"go.probo.inc/probo/pkg/docgen"
|
||||
"go.probo.inc/probo/pkg/gid"
|
||||
"go.probo.inc/probo/pkg/html2pdf"
|
||||
"go.probo.inc/probo/pkg/page"
|
||||
"go.probo.inc/probo/pkg/validator"
|
||||
)
|
||||
|
||||
type DataProtectionImpactAssessmentService struct {
|
||||
svc *TenantService
|
||||
svc *TenantService
|
||||
html2pdfConverter *html2pdf.Converter
|
||||
}
|
||||
|
||||
type (
|
||||
@@ -298,3 +302,130 @@ func (s *DataProtectionImpactAssessmentService) Delete(
|
||||
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 (
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"go.gearno.de/kit/pg"
|
||||
"go.probo.inc/probo/pkg/coredata"
|
||||
"go.probo.inc/probo/pkg/docgen"
|
||||
"go.probo.inc/probo/pkg/gid"
|
||||
"go.probo.inc/probo/pkg/html2pdf"
|
||||
"go.probo.inc/probo/pkg/page"
|
||||
"go.probo.inc/probo/pkg/validator"
|
||||
)
|
||||
|
||||
type ProcessingActivityService struct {
|
||||
svc *TenantService
|
||||
svc *TenantService
|
||||
html2pdfConverter *html2pdf.Converter
|
||||
}
|
||||
|
||||
type (
|
||||
@@ -382,3 +387,159 @@ func (s ProcessingActivityService) CountForOrganizationID(
|
||||
|
||||
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.ContinualImprovements = &ContinualImprovementService{svc: tenantService}
|
||||
tenantService.RightsRequests = &RightsRequestService{svc: tenantService}
|
||||
tenantService.ProcessingActivities = &ProcessingActivityService{svc: tenantService}
|
||||
tenantService.DataProtectionImpactAssessments = &DataProtectionImpactAssessmentService{svc: tenantService}
|
||||
tenantService.TransferImpactAssessments = &TransferImpactAssessmentService{svc: tenantService}
|
||||
tenantService.ProcessingActivities = &ProcessingActivityService{
|
||||
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.CustomDomains = &CustomDomainService{
|
||||
svc: tenantService,
|
||||
|
||||
@@ -192,7 +192,8 @@ func (s *SnapshotService) CountForOrganizationID(
|
||||
ctx,
|
||||
func(conn pg.Conn) (err error) {
|
||||
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 {
|
||||
return fmt.Errorf("cannot count snapshots: %w", err)
|
||||
}
|
||||
|
||||
@@ -17,17 +17,21 @@ package probo
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"time"
|
||||
|
||||
"go.gearno.de/kit/pg"
|
||||
"go.probo.inc/probo/pkg/coredata"
|
||||
"go.probo.inc/probo/pkg/docgen"
|
||||
"go.probo.inc/probo/pkg/gid"
|
||||
"go.probo.inc/probo/pkg/html2pdf"
|
||||
"go.probo.inc/probo/pkg/page"
|
||||
"go.probo.inc/probo/pkg/validator"
|
||||
)
|
||||
|
||||
type TransferImpactAssessmentService struct {
|
||||
svc *TenantService
|
||||
svc *TenantService
|
||||
html2pdfConverter *html2pdf.Converter
|
||||
}
|
||||
|
||||
type (
|
||||
@@ -298,3 +302,130 @@ func (s *TransferImpactAssessmentService) Delete(
|
||||
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(
|
||||
input: ExportSignableDocumentVersionPDFInput!
|
||||
): ExportSignableDocumentVersionPDFPayload!
|
||||
exportProcessingActivitiesPDF(
|
||||
input: ExportProcessingActivitiesPDFInput!
|
||||
): ExportProcessingActivitiesPDFPayload!
|
||||
exportDataProtectionImpactAssessmentsPDF(
|
||||
input: ExportDataProtectionImpactAssessmentsPDFInput!
|
||||
): ExportDataProtectionImpactAssessmentsPDFPayload!
|
||||
exportTransferImpactAssessmentsPDF(
|
||||
input: ExportTransferImpactAssessmentsPDFInput!
|
||||
): ExportTransferImpactAssessmentsPDFPayload!
|
||||
createVendorRiskAssessment(
|
||||
input: CreateVendorRiskAssessmentInput!
|
||||
): CreateVendorRiskAssessmentPayload!
|
||||
@@ -3941,6 +3950,21 @@ input ExportSignableDocumentVersionPDFInput {
|
||||
documentVersionId: ID!
|
||||
}
|
||||
|
||||
input ExportProcessingActivitiesPDFInput {
|
||||
organizationId: ID!
|
||||
filter: ProcessingActivityFilter
|
||||
}
|
||||
|
||||
input ExportDataProtectionImpactAssessmentsPDFInput {
|
||||
organizationId: ID!
|
||||
filter: DataProtectionImpactAssessmentFilter
|
||||
}
|
||||
|
||||
input ExportTransferImpactAssessmentsPDFInput {
|
||||
organizationId: ID!
|
||||
filter: TransferImpactAssessmentFilter
|
||||
}
|
||||
|
||||
input DeleteDocumentInput {
|
||||
documentId: ID!
|
||||
}
|
||||
@@ -4600,6 +4624,18 @@ type ExportSignableDocumentVersionPDFPayload {
|
||||
data: String!
|
||||
}
|
||||
|
||||
type ExportProcessingActivitiesPDFPayload {
|
||||
data: String!
|
||||
}
|
||||
|
||||
type ExportDataProtectionImpactAssessmentsPDFPayload {
|
||||
data: String!
|
||||
}
|
||||
|
||||
type ExportTransferImpactAssessmentsPDFPayload {
|
||||
data: String!
|
||||
}
|
||||
|
||||
type UpdateDocumentPayload {
|
||||
document: Document!
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1220,6 +1220,15 @@ type EvidenceEdge struct {
|
||||
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 {
|
||||
DocumentVersionID gid.GID `json:"documentVersionId"`
|
||||
WithWatermark bool `json:"withWatermark"`
|
||||
@@ -1239,6 +1248,15 @@ type ExportFrameworkPayload struct {
|
||||
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 {
|
||||
DocumentVersionID gid.GID `json:"documentVersionId"`
|
||||
}
|
||||
@@ -1247,6 +1265,15 @@ type ExportSignableDocumentVersionPDFPayload struct {
|
||||
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 {
|
||||
ID gid.GID `json:"id"`
|
||||
MimeType string `json:"mimeType"`
|
||||
|
||||
@@ -3745,6 +3745,84 @@ func (r *mutationResolver) ExportSignableVersionDocumentPDF(ctx context.Context,
|
||||
}, 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.
|
||||
func (r *mutationResolver) CreateVendorRiskAssessment(ctx context.Context, input types.CreateVendorRiskAssessmentInput) (*types.CreateVendorRiskAssessmentPayload, error) {
|
||||
r.MustBeAuthorized(ctx, input.VendorID, authz.ActionCreateVendorRiskAssessment)
|
||||
|
||||
Reference in New Issue
Block a user