Add processing activity exports
Signed-off-by: Sacha Al Himdani <sacha@getprobo.com>
This commit is contained in:
@@ -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