diff --git a/gdpr/.cdsrc.json b/gdpr/.cdsrc.json new file mode 100644 index 00000000..bdc1b712 --- /dev/null +++ b/gdpr/.cdsrc.json @@ -0,0 +1,28 @@ +{ + "build": { + "target": "gen", + "tasks": [{ + "for": "hana", + "src": "db", + "options": { + "model": [ + "db", + "srv", + "app" + ] + } + }, + { + "for": "node-cf", + "src": "srv", + "options": { + "model": [ + "db", + "srv", + "app" + ] + } + } + ] + } +} diff --git a/gdpr/.env b/gdpr/.env new file mode 100644 index 00000000..6009b480 --- /dev/null +++ b/gdpr/.env @@ -0,0 +1 @@ +PORT = 4007 diff --git a/gdpr/.etc/deploy.sh b/gdpr/.etc/deploy.sh new file mode 100644 index 00000000..41f29d05 --- /dev/null +++ b/gdpr/.etc/deploy.sh @@ -0,0 +1,4 @@ +npm run build +cf create-service-push +cf bind-service gdpr-srv gdpr-pdm -c .pdm/pdm-binding-config.json +cf restage gdpr-srv diff --git a/gdpr/.etc/undeploy.sh b/gdpr/.etc/undeploy.sh new file mode 100644 index 00000000..ea3a6d40 --- /dev/null +++ b/gdpr/.etc/undeploy.sh @@ -0,0 +1,7 @@ +cf delete gdpr-srv -f +cf delete gdpr-db-deployer -f +cf delete-service gdpr-pdm -f +cf delete-service gdpr-auditlog -f +cf delete-service gdpr-uaa -f +cf delete-service gdpr-hdi -f +cf delete-service gdpr-logs -f diff --git a/gdpr/.pdm/pdm-binding-config.json b/gdpr/.pdm/pdm-binding-config.json new file mode 100644 index 00000000..92dc50e6 --- /dev/null +++ b/gdpr/.pdm/pdm-binding-config.json @@ -0,0 +1,16 @@ +{ + "fullyQualifiedApplicationName": "capire-gdpr", + "fullyQualifiedModuleName": "gdpr-srv", + "applicationTitle": "Capire GDPR Sample App", + "applicationTitleKey": "Capire GDPR Sample App", + "applicationURL": "https://capire-gdpr-srv.cfapps.eu10.hana.ondemand.com", + "endPoints": [{ + "type": "odatav4", + "serviceName": "PDMService", + "serviceURI": "/pdm", + "serviceTitle": "Capire GDPR Sample App PDM Service", + "serviceTitleKey": "Capire GDPR Sample App PDM Service", + "hasGdprV4Annotations": true, + "cacheControl": "no-cache" + }] +} diff --git a/gdpr/.pdm/pdm-instance-config.json b/gdpr/.pdm/pdm-instance-config.json new file mode 100644 index 00000000..7596bc2e --- /dev/null +++ b/gdpr/.pdm/pdm-instance-config.json @@ -0,0 +1,8 @@ +{ + "xs-security": { + "xsappname": "capire-gdpr", + "authorities": ["$ACCEPT_GRANTED_AUTHORITIES"] + }, + "fullyQualifiedApplicationName": "capire-gdpr", + "appConsentServiceEnabled": true +} diff --git a/gdpr/app/fiori.cds b/gdpr/app/fiori.cds new file mode 100644 index 00000000..df2f32d2 --- /dev/null +++ b/gdpr/app/fiori.cds @@ -0,0 +1,317 @@ +//////////////////////////////////////////////////////////////////////////// +// +// Note: this is designed for the GDPRService being co-located with +// orders. It does not work if GDPRService is run as a separate +// process, and is not intended to do so. +// +//////////////////////////////////////////////////////////////////////////// + + +using {GDPRService} from '../srv/gdpr-service'; + +annotate cds.UUID with @Core.Computed; + +/* + * Orders + */ +@odata.draft.enabled +annotate GDPRService.Orders with @(UI : { + SelectionFields : [ + createdAt, + createdBy + ], + LineItem : [ + { + Value : OrderNo, + Label : 'Order number' + }, + { + Value : customer.firstName, + Label : 'First Name' + }, + { + Value : customer.lastName, + Label : 'Last Name' + } + ], + HeaderInfo : { + TypeName : 'Order', + TypeNamePlural : 'Orders', + Title : { + Value : OrderNo, + Label : 'Order number' + } + }, + Identification : [ + { + Value : createdBy, + Label : 'Created by' + }, + { + Value : createdAt, + Label : 'Created at' + } + ], + HeaderFacets : [ + { + $Type : 'UI.ReferenceFacet', + Label : '{i18n>Created}', + Target : '@UI.FieldGroup#Created' + }, + { + $Type : 'UI.ReferenceFacet', + Label : '{i18n>Modified}', + Target : '@UI.FieldGroup#Modified' + }, + ], + Facets : [ + { + $Type : 'UI.ReferenceFacet', + Label : '{i18n>Details}', + Target : '@UI.FieldGroup#Details' + }, + { + $Type : 'UI.ReferenceFacet', + Label : '{i18n>OrderItems}', + Target : 'Items/@UI.LineItem' + }, + ], + FieldGroup #Details : {Data : [ + { + Value : customer_ID, + Label : 'Customer' + }, + { + Value : customer.firstName, + Label : 'First Name' + }, + { + Value : customer.lastName, + Label : 'Last Name' + }, + { + Value : currency_code, + Label : 'Currency' + } + ]}, + FieldGroup #Created : {Data : [ + { + Value : createdBy, + Label : 'Created by' + }, + { + Value : createdAt, + Label : 'Created at' + } + ]}, + FieldGroup #Modified : {Data : [ + { + Value : modifiedBy, + Label : 'Modified by' + }, + { + Value : modifiedAt, + Label : 'Modified at' + } + ]}, +}, ) { + createdAt @UI.HiddenFilter : false; + createdBy @UI.HiddenFilter : false; + customer @ValueList.entity : 'Customers'; +}; + +/* + * TODO: Order Items are not really maintainable in Fiori preview app + */ +annotate GDPRService.Orders.Items with @(UI : { + LineItem : [ + { + Value : product_ID, + Label : 'Product ID' + }, + { + Value : title, + Label : 'Product Name' + }, + { + Value : price, + Label : 'Price' + }, + { + Value : quantity, + Label : 'Quantity' + }, + ], + Identification : [ + { + Value : product_ID, + Label : 'Product ID' + }, + { + Value : title, + Label : 'Product Name' + }, + { + Value : quantity, + Label : 'Quantity' + }, + { + Value : price, + Label : 'Price' + }, + ], + Facets : [{ + $Type : 'UI.ReferenceFacet', + Label : 'Order Items', + Target : '@UI.Identification' + }, ], +}, ) { + ID @Core.Computed @UI.Hidden : true; + title @Core.Computed; + price @Core.Computed; + quantity @(Common.FieldControl : #Mandatory); +}; + +/* + * Customers + */ +@odata.draft.enabled +annotate GDPRService.Customers with @(UI : { + SelectionFields : [ + firstName, + lastName + ], + LineItem : [ + { + Value : firstName, + Label : 'First Name' + }, + { + Value : lastName, + Label : 'Last Name' + }, + { + Value : dateOfBirth, + Label : 'Date of Birth' + } + ], + HeaderInfo : { + TypeName : 'Customer', + TypeNamePlural : 'Customers', + Title : { + Value : lastName, + Label : 'Last Name' + }, + Description : { + Value : firstName, + Label : 'First Name' + } + }, + Identification : [ + { + Value : createdBy, + Label : 'Created by' + }, + { + Value : createdAt, + Label : 'Created at' + } + ], + HeaderFacets : [ + { + $Type : 'UI.ReferenceFacet', + Label : '{i18n>Created}', + Target : '@UI.FieldGroup#Created' + }, + { + $Type : 'UI.ReferenceFacet', + Label : '{i18n>Modified}', + Target : '@UI.FieldGroup#Modified' + }, + ], + Facets : [ + { + $Type : 'UI.ReferenceFacet', + Label : '{i18n>Details}', + Target : '@UI.FieldGroup#Details' + }, + { + $Type : 'UI.ReferenceFacet', + Label : '{i18n>Addresses}', + Target : 'addresses/@UI.LineItem' + }, + ], + FieldGroup #Details : {Data : [ + { + Value : dateOfBirth, + Label : 'Date of Birth' + }, + { + Value : email, + Label : 'E-Mail' + }, + { + Value : creditCardNo, + Label : 'Credit Card Number' + } + ]}, + FieldGroup #Created : {Data : [ + { + Value : createdBy, + Label : 'Created by' + }, + { + Value : createdAt, + Label : 'Created at' + } + ]}, + FieldGroup #Modified : {Data : [ + { + Value : modifiedBy, + Label : 'Modified by' + }, + { + Value : modifiedAt, + Label : 'Modified at' + } + ]}, +}, ) { + createdAt @UI.HiddenFilter : false; + createdBy @UI.HiddenFilter : false; +}; + +annotate GDPRService.CustomerPostalAddresses with @(UI : { + LineItem : [ + { + Value : town, + Label : 'Town' + }, + { + Value : street, + Label : 'Street' + }, + { + Value : country.name, + Label : 'Country' + } + ], + Identification : [ + { + Value : town, + Label : 'Town' + }, + { + Value : street, + Label : 'Street' + }, + { + Value : country_code, + Label : 'Country Code' + } + ], + Facets : [{ + $Type : 'UI.ReferenceFacet', + Label : 'Customer Postal Address', + Target : '@UI.Identification' + }, ], +}, ); diff --git a/gdpr/db/data-privacy.cds b/gdpr/db/data-privacy.cds new file mode 100644 index 00000000..d6cc056d --- /dev/null +++ b/gdpr/db/data-privacy.cds @@ -0,0 +1,56 @@ +using {sap.capire.orders} from '@capire/orders'; +using {sap.capire.gdpr} from './schema'; + +/* + * annotations for Data Privacy (Personal Data Manager and Audit Logging) + */ +annotate gdpr.Customers with @PersonalData : { + DataSubjectRole : 'Customer', + EntitySemantics : 'DataSubject' +}{ + ID @PersonalData.FieldSemantics : 'DataSubjectID'; + email @PersonalData.IsPotentiallyPersonal; + firstName @PersonalData.IsPotentiallyPersonal; + lastName @PersonalData.IsPotentiallyPersonal; + creditCardNo @PersonalData.IsPotentiallySensitive; + dateOfBirth @PersonalData.IsPotentiallyPersonal; +} + +annotate gdpr.CustomerPostalAddresses with @PersonalData : { + DataSubjectRole : 'Customer', + EntitySemantics : 'DataSubjectDetails' +}{ + customer @PersonalData.FieldSemantics : 'DataSubjectID'; + street @PersonalData.IsPotentiallyPersonal; + town @PersonalData.IsPotentiallyPersonal; + country @PersonalData.IsPotentiallyPersonal; +} + +/* + * TODO: Personal Data Manager doesn't know EntitySemantics: 'Other' and FieldSemantics: 'ContractRelatedID' + * see: https://help.sap.com/viewer/620a3ea6aaf64610accdd05cca9e3de2/Cloud/en-US/5a55fae1eb7c496c92c56071186d76b3.html + */ +annotate orders.Orders with @PersonalData : { + DataSubjectRole : 'Customer', + EntitySemantics : 'LegalGround' +}{ + ID @PersonalData.FieldSemantics : 'LegalGroundID'; + customer @PersonalData.FieldSemantics : 'DataSubjectID'; +} + +/* + * additional annotations for Audit Logging + */ +annotate gdpr.Customers with @AuditLog.Operation : { + Read : true, + Insert : true, + Update : true, + Delete : true +}; + +annotate gdpr.CustomerPostalAddresses with @AuditLog.Operation : { + Read : true, + Insert : true, + Update : true, + Delete : true +}; diff --git a/gdpr/db/data/sap.capire.gdpr-CustomerPostalAddresses.csv b/gdpr/db/data/sap.capire.gdpr-CustomerPostalAddresses.csv new file mode 100644 index 00000000..4aafe541 --- /dev/null +++ b/gdpr/db/data/sap.capire.gdpr-CustomerPostalAddresses.csv @@ -0,0 +1,3 @@ +ID;modifiedAt;createdAt;createdBy;modifiedBy;customer_ID;street;town;country_code +1e2f2640-6866-4dcf-8f4d-3027aa831cad;2019-04-04;2019-01-31;admin@business.com;admin@business.com;8e2f2640-6866-4dcf-8f4d-3027aa831cad;Hauptstrasse 11;Berlin;DE +24e718c9-ff99-47f1-8ca3-950c850777d4;2019-04-04;2019-01-30;admin@business.com;admin@business.com;74e718c9-ff99-47f1-8ca3-950c850777d4;Main Street 22;London;GB diff --git a/gdpr/db/data/sap.capire.gdpr-Customers.csv b/gdpr/db/data/sap.capire.gdpr-Customers.csv new file mode 100644 index 00000000..c94c4431 --- /dev/null +++ b/gdpr/db/data/sap.capire.gdpr-Customers.csv @@ -0,0 +1,3 @@ +ID;modifiedAt;createdAt;createdBy;modifiedBy;email;firstName;lastName;creditCardNo;dateOfBirth +8e2f2640-6866-4dcf-8f4d-3027aa831cad;2019-04-04;2019-01-31;admin@business.com;admin@business.com;john.doe@test.com;John;Doe;9977-6655-4433-2211;1970-01-01 +74e718c9-ff99-47f1-8ca3-950c850777d4;2019-04-04;2019-01-30;admin@business.com;admin@business.com;jane.doe@sap.com;Jane;Doe;2211-3344-5566-7788;1980-11-11 \ No newline at end of file diff --git a/gdpr/db/data/sap.capire.orders-Orders.Items.csv b/gdpr/db/data/sap.capire.orders-Orders.Items.csv new file mode 100644 index 00000000..c8702065 --- /dev/null +++ b/gdpr/db/data/sap.capire.orders-Orders.Items.csv @@ -0,0 +1,4 @@ +ID;up__ID;quantity;product_ID;title;price +4bd2c9df-c19f-47b8-a921-3cde0d863b52;29f15ef6-4a13-47d4-aef4-329a403b49eb;1;201;Wuthering Heights;11.11 +6c42a40d-5f7c-4c2f-816b-a73c7c28d722;29f15ef6-4a13-47d4-aef4-329a403b49eb;1;271;Catweazle;15 +748555fc-2cb0-42b5-a361-dd19a50bd682;31c2bd15-5146-4418-b574-866a08911de7;2;252;Eleonora;28 diff --git a/gdpr/db/data/sap.capire.orders-Orders.csv b/gdpr/db/data/sap.capire.orders-Orders.csv new file mode 100644 index 00000000..f7e151d6 --- /dev/null +++ b/gdpr/db/data/sap.capire.orders-Orders.csv @@ -0,0 +1,3 @@ +ID;createdAt;createdBy;customer_ID;OrderNo;currency_code +29f15ef6-4a13-47d4-aef4-329a403b49eb;2019-01-31;john.doe@test.com;8e2f2640-6866-4dcf-8f4d-3027aa831cad;1;EUR +31c2bd15-5146-4418-b574-866a08911de7;2019-01-30;jane.doe@test.com;74e718c9-ff99-47f1-8ca3-950c850777d4;2;EUR diff --git a/gdpr/db/schema.cds b/gdpr/db/schema.cds new file mode 100644 index 00000000..6a4d80d8 --- /dev/null +++ b/gdpr/db/schema.cds @@ -0,0 +1,30 @@ +using { + Country, + managed, + cuid +} from '@sap/cds/common'; +using {sap.capire.orders} from '@capire/orders'; + +namespace sap.capire.gdpr; + +extend orders.Orders with { + customer : Association to Customers; +} + +entity Customers : cuid, managed { + email : String; + firstName : String; + lastName : String; + creditCardNo : String; + dateOfBirth : Date; + addresses : Composition of many CustomerPostalAddresses + on addresses.customer = $self; +} + +entity CustomerPostalAddresses : cuid, managed { + customer : Association to Customers; + street : String(128); + town : String(128); + @assert.integrity : false + country : Country; +}; diff --git a/gdpr/db/src/.hdiconfig b/gdpr/db/src/.hdiconfig new file mode 100644 index 00000000..b1a1e281 --- /dev/null +++ b/gdpr/db/src/.hdiconfig @@ -0,0 +1,136 @@ +{ + "file_suffixes": { + "csv": { + "plugin_name": "com.sap.hana.di.tabledata.source" + }, + "hdbafllangprocedure": { + "plugin_name": "com.sap.hana.di.afllangprocedure" + }, + "hdbanalyticprivilege": { + "plugin_name": "com.sap.hana.di.analyticprivilege" + }, + "hdbcalculationview": { + "plugin_name": "com.sap.hana.di.calculationview" + }, + "hdbcollection": { + "plugin_name": "com.sap.hana.di.collection" + }, + "hdbconstraint": { + "plugin_name": "com.sap.hana.di.constraint" + }, + "hdbdropcreatetable": { + "plugin_name": "com.sap.hana.di.dropcreatetable" + }, + "hdbflowgraph": { + "plugin_name": "com.sap.hana.di.flowgraph" + }, + "hdbfunction": { + "plugin_name": "com.sap.hana.di.function" + }, + "hdbgraphworkspace": { + "plugin_name": "com.sap.hana.di.graphworkspace" + }, + "hdbhadoopmrjob": { + "plugin_name": "com.sap.hana.di.virtualfunctionpackage.hadoop" + }, + "hdbindex": { + "plugin_name": "com.sap.hana.di.index" + }, + "hdblibrary": { + "plugin_name": "com.sap.hana.di.library" + }, + "hdbmigrationtable": { + "plugin_name": "com.sap.hana.di.table.migration" + }, + "hdbprocedure": { + "plugin_name": "com.sap.hana.di.procedure" + }, + "hdbprojectionview": { + "plugin_name": "com.sap.hana.di.projectionview" + }, + "hdbprojectionviewconfig": { + "plugin_name": "com.sap.hana.di.projectionview.config" + }, + "hdbreptask": { + "plugin_name": "com.sap.hana.di.reptask" + }, + "hdbresultcache": { + "plugin_name": "com.sap.hana.di.resultcache" + }, + "hdbrole": { + "plugin_name": "com.sap.hana.di.role" + }, + "hdbroleconfig": { + "plugin_name": "com.sap.hana.di.role.config" + }, + "hdbsearchruleset": { + "plugin_name": "com.sap.hana.di.searchruleset" + }, + "hdbsequence": { + "plugin_name": "com.sap.hana.di.sequence" + }, + "hdbstatistics": { + "plugin_name": "com.sap.hana.di.statistics" + }, + "hdbstructuredprivilege": { + "plugin_name": "com.sap.hana.di.structuredprivilege" + }, + "hdbsynonym": { + "plugin_name": "com.sap.hana.di.synonym" + }, + "hdbsynonymconfig": { + "plugin_name": "com.sap.hana.di.synonym.config" + }, + "hdbsystemversioning": { + "plugin_name": "com.sap.hana.di.systemversioning" + }, + "hdbtable": { + "plugin_name": "com.sap.hana.di.table" + }, + "hdbtabledata": { + "plugin_name": "com.sap.hana.di.tabledata" + }, + "hdbtabletype": { + "plugin_name": "com.sap.hana.di.tabletype" + }, + "hdbtrigger": { + "plugin_name": "com.sap.hana.di.trigger" + }, + "hdbview": { + "plugin_name": "com.sap.hana.di.view" + }, + "hdbvirtualfunction": { + "plugin_name": "com.sap.hana.di.virtualfunction" + }, + "hdbvirtualfunctionconfig": { + "plugin_name": "com.sap.hana.di.virtualfunction.config" + }, + "hdbvirtualpackagehadoop": { + "plugin_name": "com.sap.hana.di.virtualpackage.hadoop" + }, + "hdbvirtualpackagesparksql": { + "plugin_name": "com.sap.hana.di.virtualpackage.sparksql" + }, + "hdbvirtualprocedure": { + "plugin_name": "com.sap.hana.di.virtualprocedure" + }, + "hdbvirtualprocedureconfig": { + "plugin_name": "com.sap.hana.di.virtualprocedure.config" + }, + "hdbvirtualtable": { + "plugin_name": "com.sap.hana.di.virtualtable" + }, + "hdbvirtualtableconfig": { + "plugin_name": "com.sap.hana.di.virtualtable.config" + }, + "properties": { + "plugin_name": "com.sap.hana.di.tabledata.properties" + }, + "tags": { + "plugin_name": "com.sap.hana.di.tabledata.properties" + }, + "txt": { + "plugin_name": "com.sap.hana.di.copyonly" + } + } +} \ No newline at end of file diff --git a/gdpr/manifest.yml b/gdpr/manifest.yml new file mode 100644 index 00000000..6151c264 --- /dev/null +++ b/gdpr/manifest.yml @@ -0,0 +1,31 @@ +--- +applications: +# ----------------------------------------------------------------------------------- +# HANA Database Content Deployer App +# ----------------------------------------------------------------------------------- +- name: gdpr-db-deployer + path: gen/db + no-route: true + health-check-type: process + memory: 256M + buildpack: nodejs_buildpack + services: + - gdpr-logs + - gdpr-hdi +# ----------------------------------------------------------------------------------- +# Backend Service +# ----------------------------------------------------------------------------------- +- name: gdpr-srv + path: gen/srv + memory: 256M + buildpack: nodejs_buildpack + routes: + - route: capire-gdpr-srv.cfapps.eu10.hana.ondemand.com + services: + - gdpr-logs + - gdpr-hdi + - gdpr-uaa + - gdpr-auditlog + # binding with parameters not yet supported -> binding done manually in .etc/deploy.sh + #- name: gdpr-pdm + # parameters: ./pdm-binding-config.json diff --git a/gdpr/package.json b/gdpr/package.json new file mode 100644 index 00000000..aa99e72e --- /dev/null +++ b/gdpr/package.json @@ -0,0 +1,48 @@ +{ + "name": "@capire/gdpr", + "version": "0.0.1", + "dependencies": { + "@capire/orders": "../orders", + "@sap/audit-logging": "^5.1.0", + "@sap/cds": "^5.7", + "express": "^4.17.1", + "hdb": "^0.19.0" + }, + "scripts": { + "build": "rm -rf gen && cds build --production", + "deploy": "sh .etc/deploy.sh", + "undeploy": "sh .etc/undeploy.sh", + "start": "cds run" + }, + "cds": { + "requires": { + "auth": { + "__comment__": "workaround to avoid approuter et al. setup", + "impl": "srv/auth.js" + }, + "audit-log": { + "[development]": { + "credentials": { + "logToConsole": true + } + } + }, + "db": { + "kind": "sql" + }, + "uaa": { + "kind": "xsuaa" + } + }, + "features": { + "audit_personal_data": true, + "fiori_preview": true, + "[production]": { + "kibana_formatter": true + } + }, + "hana": { + "deploy-format": "hdbtable" + } + } +} diff --git a/gdpr/readme.md b/gdpr/readme.md new file mode 100644 index 00000000..22c0eb8e --- /dev/null +++ b/gdpr/readme.md @@ -0,0 +1,35 @@ +# how-to + +## required services and subscriptions + +services: +- Audit Log Service +- SAP HANA Cloud +- SAP HANA Schemas & HDI Containers +- Application Logging Service +- Personal Data Manager +- Authorization and Trust Management Service + +subscriptions: +- Audit Log Viewer Service +- Personal Data Manager + +## deploy + +after adding the necessary entitlements, do: +- `cf l` to log into the respective account +- `cd gdpr` (if still in root of `cloud-cap-samples`) +- `npm run deploy`, which executes build and deployment via `.etc/deploy.sh` + +## authorization + +create roles for Audit Log Viewer Service and Personal Data Manager, and assign the roles to the respective users + +# open issues + +- deploy via mta, which can bind with parameters, and get rid of scripts in `.etc` +- use approuter to remove hacky custom auth impl (`srv/auth.js`) +- clarify annotation `EntitySemantics`, which differs between audit logging (`Other`) and personal data manager (`LegalGround`) +- annotations for order items Fiori preview app + + `Products` has `@cds.persistence.skip:'always'` +- how to reuse intial data from `common`? diff --git a/gdpr/services-manifest.yml b/gdpr/services-manifest.yml new file mode 100644 index 00000000..c1e9dbe3 --- /dev/null +++ b/gdpr/services-manifest.yml @@ -0,0 +1,20 @@ +--- +create-services: + - name: gdpr-logs # > for kibana + broker: application-logs + plan: standard + - name: gdpr-hdi # > hana + broker: hana + plan: hdi-shared + - name: gdpr-auditlog # > audit log sink + broker: auditlog + plan: standard + # gdpr-pdm needs to exist before creating gdpr-uaa for authorization grant + - name: gdpr-pdm # > personal data manager + broker: personal-data-manager-service + plan: standard + parameters: ./.pdm/pdm-instance-config.json + - name: gdpr-uaa # > uaa for authentication + broker: xsuaa + plan: application + parameters: xs-security.json diff --git a/gdpr/srv/auth.js b/gdpr/srv/auth.js new file mode 100644 index 00000000..7ca86d8a --- /dev/null +++ b/gdpr/srv/auth.js @@ -0,0 +1,43 @@ +/* + * workaround to avoid approuter et al. setup + */ + +const jwt = require('jsonwebtoken') +const tenant = process.env.VCAP_SERVICES + ? JSON.parse(process.env.VCAP_SERVICES).xsuaa[0].credentials.tenantid + : 'anonymous' + +module.exports = (req, res, next) => { + /* + * decode JWT coming from Personal Data Manager + * + * DO NOT USE FOR PRODUCTION! + * - no token validation + * - no xsappname check + */ + const bearer = req.headers.authorization && req.headers.authorization.split('Bearer ')[1] + if (bearer) { + const { client_id: id, zid: tenant, scope: roles } = jwt.decode(bearer) + req.user = { + id, + tenant, + is: role => roles.some(r => r.endsWith(`.${role}`)) + } + return next() + } + + // mock user that has every role EXCEPT PersonalDataManagerUser + const basic = req.headers.authorization && req.headers.authorization.split('Basic ')[1] + if (basic) { + const [id] = Buffer.from(basic, 'base64').toString('utf-8').split(':') + req.user = { + id, + tenant, + is: role => role !== 'PersonalDataManagerUser' + } + return next() + } + + // no bearer & no basic -> 401 + res.set('WWW-Authenticate', 'Basic realm="Users"').status(401).end() +} diff --git a/gdpr/srv/gdpr-service.cds b/gdpr/srv/gdpr-service.cds new file mode 100644 index 00000000..cf8dccc5 --- /dev/null +++ b/gdpr/srv/gdpr-service.cds @@ -0,0 +1,10 @@ +using { + sap.capire.orders, + sap.capire.gdpr +} from '../db/schema'; + +@requires : 'admin' // > authorization check +service GDPRService { + entity Customers as projection on gdpr.Customers; + entity Orders as projection on orders.Orders; +} diff --git a/gdpr/srv/pdm-service.cds b/gdpr/srv/pdm-service.cds new file mode 100644 index 00000000..4fcd0024 --- /dev/null +++ b/gdpr/srv/pdm-service.cds @@ -0,0 +1,24 @@ +using { + sap.capire.gdpr as gdpr, + sap.capire.orders as orders +} from '../db/data-privacy'; + +@requires : 'PersonalDataManagerUser' // > authorization check +service PDMService { + + entity Customers as projection on gdpr.Customers; + entity CustomerPostalAddresses as projection on gdpr.CustomerPostalAddresses; + entity Orders as projection on orders.Orders; + + /* + * additional annotations for Personal Data Manager's Search Fields + */ + annotate Customers with @(Communication.Contact : { + n : { + surname : lastName, + given : firstName + }, + bday : dateOfBirth + }); + +}; diff --git a/gdpr/srv/server.js b/gdpr/srv/server.js new file mode 100644 index 00000000..99378311 --- /dev/null +++ b/gdpr/srv/server.js @@ -0,0 +1,26 @@ +const cds = require('@sap/cds') + +/* + * in development, write audit logs to custom sink (i.e., to console in this example) + */ +cds.on('served', async () => { + if (process.env.NODE_ENV === 'production') return + + const auditLogService = await cds.connect.to('audit-log') + // use prepend to get called before the generic implementation + auditLogService.prepend(function() { + const LOG = cds.log('my custom audit logging impl') + // triggered when reading sensitive personal data + this.on('dataAccessLog', function(req) { + const { accesses } = req.data + for (const access of accesses) LOG.info(access) + }) + // triggered when modifying personal data + this.on('dataModificationLog', function(req) { + const { modifications } = req.data + for (const modification of modifications) LOG.info(modification) + }) + }) +}) + +module.exports = cds.server diff --git a/gdpr/xs-security.json b/gdpr/xs-security.json new file mode 100644 index 00000000..2530f291 --- /dev/null +++ b/gdpr/xs-security.json @@ -0,0 +1,14 @@ +{ + "xsappname": "capire-gdpr", + "tenant-mode": "shared", + "scopes": [{ + "name": "$XSAPPNAME.PersonalDataManagerUser", + "description": "Authority for Personal Data Manager", + "grant-as-authority-to-apps": [ + "$XSSERVICENAME(gdpr-pdm)" + ] + }, { + "name": "$XSAPPNAME.admin", + "description": "Administrator" + }] +} diff --git a/orders/app/orders/webapp/manifest.json b/orders/app/orders/webapp/manifest.json index 045fa70a..c30c4cc5 100644 --- a/orders/app/orders/webapp/manifest.json +++ b/orders/app/orders/webapp/manifest.json @@ -167,4 +167,4 @@ "registrationIds": [], "archeType": "transactional" } -} \ No newline at end of file +} diff --git a/package.json b/package.json index 65ea41ed..f290bece 100644 --- a/package.json +++ b/package.json @@ -10,6 +10,7 @@ "@capire/common": "./common", "@capire/data-viewer": "./data-viewer", "@capire/fiori": "./fiori", + "@capire/gdpr": "./gdpr", "@capire/hello": "./hello", "@capire/media": "./media", "@capire/orders": "./orders", @@ -27,6 +28,7 @@ "registry": "node .registry/server.js", "bookshop": "cds watch bookshop", "fiori": "cds watch fiori", + "gdpr": "cds watch gdpr", "hello": "cds watch hello", "media": "cds watch media", "mocha": "npx mocha || echo",