Compare commits
6 Commits
main
...
audit-log-
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
08a3157f1d | ||
|
|
c9ecef4e21 | ||
|
|
46f1be4395 | ||
|
|
b932637400 | ||
|
|
3c6d49b88e | ||
|
|
6928ae907a |
28
gdpr/.cdsrc.json
Normal file
28
gdpr/.cdsrc.json
Normal file
@@ -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"
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
4
gdpr/.etc/deploy.sh
Normal file
4
gdpr/.etc/deploy.sh
Normal file
@@ -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
|
||||
7
gdpr/.etc/undeploy.sh
Normal file
7
gdpr/.etc/undeploy.sh
Normal file
@@ -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
|
||||
16
gdpr/.pdm/pdm-binding-config.json
Normal file
16
gdpr/.pdm/pdm-binding-config.json
Normal file
@@ -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"
|
||||
}]
|
||||
}
|
||||
8
gdpr/.pdm/pdm-instance-config.json
Normal file
8
gdpr/.pdm/pdm-instance-config.json
Normal file
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"xs-security": {
|
||||
"xsappname": "capire-gdpr",
|
||||
"authorities": ["$ACCEPT_GRANTED_AUTHORITIES"]
|
||||
},
|
||||
"fullyQualifiedApplicationName": "capire-gdpr",
|
||||
"appConsentServiceEnabled": true
|
||||
}
|
||||
317
gdpr/app/fiori.cds
Normal file
317
gdpr/app/fiori.cds
Normal file
@@ -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'
|
||||
}, ],
|
||||
}, );
|
||||
56
gdpr/db/data-privacy.cds
Normal file
56
gdpr/db/data-privacy.cds
Normal file
@@ -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
|
||||
};
|
||||
3
gdpr/db/data/sap.capire.gdpr-CustomerPostalAddresses.csv
Normal file
3
gdpr/db/data/sap.capire.gdpr-CustomerPostalAddresses.csv
Normal file
@@ -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
|
||||
|
3
gdpr/db/data/sap.capire.gdpr-Customers.csv
Normal file
3
gdpr/db/data/sap.capire.gdpr-Customers.csv
Normal file
@@ -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
|
||||
|
4
gdpr/db/data/sap.capire.orders-Orders.Items.csv
Normal file
4
gdpr/db/data/sap.capire.orders-Orders.Items.csv
Normal file
@@ -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
|
||||
|
3
gdpr/db/data/sap.capire.orders-Orders.csv
Normal file
3
gdpr/db/data/sap.capire.orders-Orders.csv
Normal file
@@ -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
|
||||
|
30
gdpr/db/schema.cds
Normal file
30
gdpr/db/schema.cds
Normal file
@@ -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;
|
||||
};
|
||||
136
gdpr/db/src/.hdiconfig
Normal file
136
gdpr/db/src/.hdiconfig
Normal file
@@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
31
gdpr/manifest.yml
Normal file
31
gdpr/manifest.yml
Normal file
@@ -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
|
||||
49
gdpr/package.json
Normal file
49
gdpr/package.json
Normal file
@@ -0,0 +1,49 @@
|
||||
{
|
||||
"name": "@capire/gdpr",
|
||||
"version": "0.0.1",
|
||||
"dependencies": {
|
||||
"@capire/orders": "../orders",
|
||||
"@sap/audit-logging": "^5.1.0",
|
||||
"@sap/cds": "^5.9",
|
||||
"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]": {
|
||||
"kind": "audit-log-to-console"
|
||||
},
|
||||
"[production]": {
|
||||
"kind": "audit-log-service"
|
||||
}
|
||||
},
|
||||
"db": {
|
||||
"kind": "sql"
|
||||
},
|
||||
"uaa": {
|
||||
"kind": "xsuaa"
|
||||
}
|
||||
},
|
||||
"features": {
|
||||
"audit_personal_data": true,
|
||||
"fiori_preview": true,
|
||||
"[production]": {
|
||||
"kibana_formatter": true
|
||||
}
|
||||
},
|
||||
"hana": {
|
||||
"deploy-format": "hdbtable"
|
||||
}
|
||||
}
|
||||
}
|
||||
35
gdpr/readme.md
Normal file
35
gdpr/readme.md
Normal file
@@ -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`?
|
||||
20
gdpr/services-manifest.yml
Normal file
20
gdpr/services-manifest.yml
Normal file
@@ -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
|
||||
43
gdpr/srv/auth.js
Normal file
43
gdpr/srv/auth.js
Normal file
@@ -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()
|
||||
}
|
||||
10
gdpr/srv/gdpr-service.cds
Normal file
10
gdpr/srv/gdpr-service.cds
Normal file
@@ -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;
|
||||
}
|
||||
24
gdpr/srv/pdm-service.cds
Normal file
24
gdpr/srv/pdm-service.cds
Normal file
@@ -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
|
||||
});
|
||||
|
||||
};
|
||||
26
gdpr/srv/server.js
Normal file
26
gdpr/srv/server.js
Normal file
@@ -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
|
||||
14
gdpr/xs-security.json
Normal file
14
gdpr/xs-security.json
Normal file
@@ -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"
|
||||
}]
|
||||
}
|
||||
@@ -167,4 +167,4 @@
|
||||
"registrationIds": [],
|
||||
"archeType": "transactional"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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",
|
||||
|
||||
Reference in New Issue
Block a user