Compare commits

..

6 Commits

Author SHA1 Message Date
Johannes Vogel
08a3157f1d use new kinds for audit log 2022-03-18 09:30:42 +01:00
sjvans
c9ecef4e21 Merge branch 'main' into audit-logging 2022-02-08 13:48:35 +01:00
sjvans
46f1be4395 cleanup 2022-02-08 13:47:32 +01:00
sjvans
b932637400 Update manifest.json 2022-02-08 13:45:36 +01:00
sjvans
3c6d49b88e in development, write audit logs to custom sink 2022-02-08 13:41:30 +01:00
sjvans
6928ae907a initial 2022-02-03 17:57:35 +01:00
46 changed files with 1956 additions and 9461 deletions

View File

@@ -1,8 +0,0 @@
version: 2
updates:
- package-ecosystem: npm
directory: /
versioning-strategy: increase-if-necessary
schedule:
interval: daily

File diff suppressed because it is too large Load Diff

View File

@@ -1,12 +0,0 @@
{
"name": "approuter",
"dependencies": {
"@sap/approuter": "^10"
},
"engines": {
"node": "^16"
},
"scripts": {
"start": "node node_modules/@sap/approuter/approuter.js"
}
}

View File

@@ -3,15 +3,14 @@ const $ = sel => document.querySelector(sel)
const GET = (url) => axios.get('/browse'+url)
const POST = (cmd,data) => axios.post('/browse'+cmd,data)
const books = Vue.createApp ({
const books = new Vue ({
data() {
return {
el:'#app',
data: {
list: [],
book: undefined,
order: { quantity:1, succeeded:'', failed:'' },
user: {}
}
order: { quantity:1, succeeded:'', failed:'' }
},
methods: {
@@ -38,21 +37,12 @@ const books = Vue.createApp ({
book.stock = res.data.stock
books.order = { quantity, succeeded: `Successfully ordered ${quantity} item(s).` }
} catch (e) {
books.order = { quantity, failed: e.response.data.error ? e.response.data.error.message : e.response.data }
books.order = { quantity, failed: e.response.data.error.message }
}
}
}
}).mount("#app")
})
// initially fill list of books
books.fetch()
// show user info on request
document.addEventListener('keydown', async (event) => {
if (event.key === 'u') {
try {
books.user = (await axios.get('/user/User')).data
} catch (err) { }
}
})

View File

@@ -5,26 +5,19 @@
<title> Capire Books </title>
<link rel="stylesheet" href="https://unpkg.com/primitive-ui/dist/css/main.css">
<script src="https://cdn.jsdelivr.net/npm/axios/dist/axios.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/vue@3/dist/vue.global.prod.js"></script>
<script src="https://cdn.jsdelivr.net/npm/vue"></script>
<style>
.hovering tr:hover td { color:cyan; background: #123; cursor: pointer; }
.rating-stars { color:teal }
.succeeded { color:teal }
.failed { color:red }
.user {text-align: end; color: grey;}
</style>
</head>
<body class="small-container", style="margin-top: 70px;">
<div id='app'>
<div v-if="user.ID && user.ID !== 'anonymous'" class="user">
<div>User: {{ user.ID }}</div>
<div>Locale: {{ user.locale }}</div>
<div>Tenant: {{ user.tenant }}</div>
</div>
<h1> Capire Books </h1>
<h1> {{ document.title }} </h1>
<input type="text" placeholder="Search..." @input="search">

View File

@@ -1,19 +0,0 @@
{
"authenticationMethod": "route",
"routes": [
{
"source": "^/app/(.*)$",
"target": "$1",
"localDir": ".",
"authenticationType": "xsuaa",
"cacheControl": "no-cache, no-store, must-revalidate"
},
{
"source": "^/(.*)$",
"target": "$1",
"destination": "srv-api",
"authenticationType": "xsuaa",
"csrfProtection": false
}
]
}

View File

@@ -1,136 +0,0 @@
{
"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"
}
}
}

View File

@@ -1,5 +0,0 @@
[
"src/gen/**/*.hdbview",
"src/gen/**/*.hdbindex",
"src/gen/**/*.hdbconstraint"
]

View File

@@ -1,98 +0,0 @@
---
_schema-version: '3.1'
ID: capire.bookshop
version: 1.0.0
description: "A simple self-contained bookshop service."
parameters:
enable-parallel-deployments: true
build-parameters:
before-all:
- builder: custom
commands:
- npx -p @sap/cds-dk cds build --production
modules:
- name: bookshop-srv
type: nodejs
path: gen/srv
parameters:
buildpack: nodejs_buildpack
build-parameters:
builder: npm-ci
provides:
- name: srv-api # required by consumers of CAP services (e.g. approuter)
properties:
srv-url: ${default-url}
- name: mtx-api # potentially required by approuter
properties:
mtx-url: ${default-url}
requires:
- name: bookshop-auth
- name: bookshop-db
- name: bookshop-registry
properties:
SUBSCRIPTION_URL: ${protocol}://\${tenant_subdomain}-${default-uri}
SUBSCRIPTION_URL_REPLACEMENT_RULES: [ [ '-srv', '' ] ]
- name: bookshop
type: approuter.nodejs
path: app/ # from cds.env.folders. Consider also cds.env.build.target -> gen/app
parameters:
keep-existing-routes: true
disk-quota: 256M
memory: 256M
requires:
- name: srv-api
group: destinations
properties:
name: srv-api # must be used in xs-app.json as well
url: ~{srv-url}
forwardAuthToken: true
- name: bookshop-auth
- name: mtx-api
group: destinations
properties:
name: mtx-api # must be used in xs-app.json as well
url: ~{mtx-url}
properties:
TENANT_HOST_PATTERN: "^(.*)-${default-uri}"
resources:
- name: bookshop-auth
type: org.cloudfoundry.managed-service
parameters:
service: xsuaa
service-plan: application
path: ./xs-security.json
config:
xsappname: bookshop-${org}-${space}
tenant-mode: shared
- name: bookshop-db
type: org.cloudfoundry.managed-service
parameters:
service: service-manager
service-plan: container
properties:
hdi-service-name: ${service-name}
- name: bookshop-registry
type: org.cloudfoundry.managed-service
requires:
- name: mtx-api
parameters:
service: saas-registry
service-plan: application
config:
xsappname: bookshop-${org}-${space}
appName: bookshop-${org}-${space}
displayName: bookshop
description: A simple CAP project.
category: 'Category'
appUrls:
getDependencies: ~{mtx-api/mtx-url}/mtx/v1/provisioning/dependencies
onSubscription: ~{mtx-api/mtx-url}/mtx/v1/provisioning/tenant/{tenantId}
onSubscriptionAsync: false
onUnSubscriptionAsync: false
callbackTimeoutMillis: 300000

File diff suppressed because it is too large Load Diff

View File

@@ -2,19 +2,9 @@
"name": "@capire/bookshop",
"version": "1.0.0",
"description": "A simple self-contained bookshop service.",
"files": [
"app",
"srv",
"db",
"index.cds",
"index.js"
],
"dependencies": {
"@sap/cds": "^5",
"@sap/cds-mtx": "^2",
"@sap/xssec": "^3",
"@sap/cds": "^5.0.4",
"express": "^4.17.1",
"hdb": "^0.19.0",
"passport": "0.4.1"
},
"scripts": {
@@ -26,24 +16,7 @@
"requires": {
"db": {
"kind": "sql"
},
"[production]": {
"db": {
"kind": "hana-mt"
},
"auth": {
"kind": "xsuaa"
},
"multitenancy": true,
"approuter": {
"kind": "cloudfoundry"
}
}
},
"mtx": {
"element-prefix": "Z_",
"namespace-blocklist": [],
"extension-allowlist": []
}
}
}

View File

@@ -1,11 +0,0 @@
@requires : 'authenticated-user'
service UserService {
@odata.singleton
entity User {
ID : String;
locale : String;
tenant : String;
}
}

View File

@@ -1,11 +0,0 @@
const cds = require('@sap/cds');
module.exports = cds.service.impl((srv) => {
srv.on('READ', 'User', ({ user }) => {
return {
ID: user.id,
locale: user.locale,
tenant: user.tenant,
};
});
});

View File

@@ -1,70 +0,0 @@
{
"scopes": [
{
"name": "$XSAPPNAME.admin",
"description": "admin"
},
{
"name": "$XSAPPNAME.MtxDiagnose",
"description": "Diagnose MTX"
},
{
"name": "$XSAPPNAME.mtcallback",
"description": "Subscribe to applications",
"grant-as-authority-to-apps": [
"$XSAPPNAME(application,sap-provisioning,tenant-onboarding)"
]
},
{
"name": "$XSAPPNAME.mtdeployment",
"description": "Deploy applications"
},
{
"name": "$XSAPPNAME.ExtendCDS",
"description": "Extend CDS applications"
},
{
"name": "$XSAPPNAME.ExtendCDSdelete",
"description": "Extend CDS applications with undeployments"
}
],
"attributes": [],
"role-templates": [
{
"name": "admin",
"description": "generated",
"scope-references": [
"$XSAPPNAME.admin"
],
"attribute-references": []
},
{
"name": "MultitenancyAdministrator",
"description": "Administrate multitenant applications",
"scope-references": [
"$XSAPPNAME.MtxDiagnose",
"$XSAPPNAME.mtdeployment",
"$XSAPPNAME.mtcallback"
]
},
{
"name": "ExtensionDeveloper",
"description": "Extend application",
"scope-references": [
"$XSAPPNAME.ExtendCDS"
]
},
{
"name": "ExtensionDeveloperUndeploy",
"description": "Undeploy extension",
"scope-references": [
"$XSAPPNAME.ExtendCDSdelete"
]
}
],
"authorities": [
"$XSAPPNAME.MtxDiagnose",
"$XSAPPNAME.mtdeployment",
"$XSAPPNAME.mtcallback"
]
}

View File

@@ -16,10 +16,10 @@
<script id="sap-ushell-bootstrap" src="https://sapui5.hana.ondemand.com/test-resources/sap/ushell/bootstrap/sandbox.js"></script>
<script id="sap-ui-bootstrap" src="https://sapui5.hana.ondemand.com/resources/sap-ui-core.js"
data-sap-ui-libs="sap.m, sap.ushell, sap.collaboration, sap.ui.layout"
data-sap-ui-compatVersion="edge"
data-sap-ui-theme="sap_horizon"
data-sap-ui-frameOptions="allow"
data-sap-ui-libs="sap.m, sap.ushell, sap.collaboration, sap.ui.layout"
data-sap-ui-compatVersion="edge"
data-sap-ui-theme="sap_fiori_3"
data-sap-ui-frameOptions="allow"
></script>
<script>
sap.ui.getCore().attachInit(()=> sap.ushell.Container.createRenderer().placeAt("content"))

28
gdpr/.cdsrc.json Normal file
View 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"
]
}
}
]
}
}

1
gdpr/.env Normal file
View File

@@ -0,0 +1 @@
PORT = 4007

4
gdpr/.etc/deploy.sh Normal file
View 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
View 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

View 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"
}]
}

View 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
View 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
View 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
};

View 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
1 ID modifiedAt createdAt createdBy modifiedBy customer_ID street town country_code
2 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
3 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

View 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
1 ID modifiedAt createdAt createdBy modifiedBy email firstName lastName creditCardNo dateOfBirth
2 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
3 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

View 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
1 ID up__ID quantity product_ID title price
2 4bd2c9df-c19f-47b8-a921-3cde0d863b52 29f15ef6-4a13-47d4-aef4-329a403b49eb 1 201 Wuthering Heights 11.11
3 6c42a40d-5f7c-4c2f-816b-a73c7c28d722 29f15ef6-4a13-47d4-aef4-329a403b49eb 1 271 Catweazle 15
4 748555fc-2cb0-42b5-a361-dd19a50bd682 31c2bd15-5146-4418-b574-866a08911de7 2 252 Eleonora 28

View 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
1 ID createdAt createdBy customer_ID OrderNo currency_code
2 29f15ef6-4a13-47d4-aef4-329a403b49eb 2019-01-31 john.doe@test.com 8e2f2640-6866-4dcf-8f4d-3027aa831cad 1 EUR
3 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
View 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
View 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
View 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
View 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
View 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`?

View 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
View 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
View 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
View 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
View 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
View 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"
}]
}

View File

@@ -25,10 +25,10 @@
<script id="sap-ushell-bootstrap" src="https://sapui5.hana.ondemand.com/test-resources/sap/ushell/bootstrap/sandbox.js"></script>
<script id="sap-ui-bootstrap" src="https://sapui5.hana.ondemand.com/resources/sap-ui-core.js"
data-sap-ui-libs="sap.m, sap.ushell, sap.collaboration, sap.ui.layout"
data-sap-ui-compatVersion="edge"
data-sap-ui-theme="sap_horizon"
data-sap-ui-frameOptions="allow"
data-sap-ui-libs="sap.m, sap.ushell, sap.collaboration, sap.ui.layout"
data-sap-ui-compatVersion="edge"
data-sap-ui-theme="sap_fiori_3"
data-sap-ui-frameOptions="allow"
></script>
<script>
sap.ui.getCore().attachInit(()=> sap.ushell.Container.createRenderer().placeAt("content"))

View File

@@ -167,4 +167,4 @@
"registrationIds": [],
"archeType": "transactional"
}
}
}

2238
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -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",
@@ -20,7 +21,6 @@
"chai": "^4.3.4",
"chai-as-promised": "^7.1.1",
"chai-subset": "^1.6.0",
"semver": "^7",
"sqlite3": "npm:@mendix/sqlite3@^5"
},
"scripts": {
@@ -28,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",

View File

@@ -4,21 +4,21 @@ const GET = (url) => axios.get('/reviews'+url)
const PUT = (cmd,data) => axios.patch('/reviews'+cmd,data)
const POST = (cmd,data) => axios.post('/reviews'+cmd,data)
const reviews = Vue.createApp ({
const reviews = new Vue ({
data() {
return {
list: [],
review: undefined,
message: {},
Ratings: Object.entries({
el:'#app',
data: {
list: [],
review: undefined,
message: {},
Ratings: Object.entries({
5 : '★★★★★',
4 : '★★★★',
3 : '★★★',
2 : '★★',
1 : '★',
}).reverse()
}
}).reverse()
},
methods: {
@@ -66,7 +66,7 @@ const reviews = Vue.createApp ({
datetime: (d) => d && new Date(d).toLocaleString(),
},
}).mount("#app")
})
// initially fill list of my reviews
reviews.fetch()

View File

@@ -5,7 +5,7 @@
<title> Capire Reviews </title>
<link rel="stylesheet" href="https://unpkg.com/primitive-ui/dist/css/main.css">
<script src="https://cdn.jsdelivr.net/npm/axios/dist/axios.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/vue@3/dist/vue.global.prod.js"></script>
<script src="https://cdn.jsdelivr.net/npm/vue"></script>
<style>
.hovering tr:hover td { color:cyan; background: #123; cursor: pointer; }
.rating-stars { color:teal }
@@ -18,7 +18,7 @@
<body class="small-container", style="margin-top: 70px;">
<div id='app'>
<h1> Capire Reviews </h1>
<h1> {{ document.title }} </h1>
<input type="text" placeholder="Search..." @input="search">

View File

@@ -32,27 +32,6 @@ describe('Consuming Services locally', () => {
})
})
}).where(`name like`, 'E%')
if (require('semver').gte(cds.version, '5.9.0')) {
expect(authors).to.containSubset([
{
name: 'Emily Brontë',
books: [
{
title: 'Wuthering Heights',
currency: { name: 'British Pound', symbol: '£' },
},
],
},
{
name: 'Edgar Allen Poe',
books: [
{ title: 'The Raven', currency: { name: 'US Dollar', symbol: '$' } },
{ title: 'Eleonora', currency: { name: 'US Dollar', symbol: '$' } },
],
},
])
return
}
expect(authors).to.containSubset([
{
name: 'Emily Brontë',

View File

@@ -35,21 +35,6 @@ describe('Hierarchical Data', ()=>{
))
it ('supports nested reads', async()=>{
if (require('semver').gte(cds.version, '5.9.0')) {
expect (await
SELECT.one.from (Cats, c=>{
c.ID, c.name.as('parent'), c.children (c=>{
c.name.as('child')
})
}) .where ({name:'Cat'})
) .to.eql (
{ ID:101, parent:'Cat', children:[
{ child:'Kitty' },
{ child:'Catwoman' },
]}
)
return
}
expect (await
SELECT.one.from (Cats, c=>{
c.ID, c.name.as('parent'), c.children (c=>{
@@ -65,25 +50,6 @@ describe('Hierarchical Data', ()=>{
})
it ('supports deeply nested reads', async()=>{
if (require('semver').gte(cds.version, '5.9.0')) {
expect (await SELECT.one.from (Cats, c=>{
c.ID, c.name, c.children (
c => { c.name },
{levels:3}
)
}) .where ({name:'Cat'})
) .to.eql (
{ ID:101, name:'Cat', children:[
{ name:'Kitty', children:[
{ name:'Kitty Cat', children:[
{ name:'Aristocat' }, ]}, // level 3
{ name:'Kitty Bat', children:[] }, ]},
{ name:'Catwoman', children:[
{ name:'Catalina', children:[] } ]},
]}
)
return
}
expect (await SELECT.one.from (Cats, c=>{
c.ID, c.name, c.children (
c => { c.name },