Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

cascading delete #2041

Merged
merged 65 commits into from
Nov 19, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
65 commits
Select commit Hold shift + click to select a range
f7ab6ed
add test cases for cascading delete
sleidig Oct 18, 2023
abd5dbb
implement recursive delete of related entities
sleidig Oct 18, 2023
11d1f86
undo for all affected entities
sleidig Oct 19, 2023
0f4c0f6
add entity-schema-field flag for aggregate / composite association ty…
sleidig Oct 20, 2023
11fc6e0
mark some entity types as not hasPII, to exclude them from anonymization
sleidig Oct 20, 2023
6e00719
Update src/app/core/entity/entity-remove.service.spec.ts
sleidig Oct 30, 2023
5eca25f
Merge branch 'master' into anon-3
sleidig Oct 30, 2023
3076004
Update src/app/core/entity/schema/entity-schema-field.ts
sleidig Oct 30, 2023
288720c
make hasPII default "false"
sleidig Oct 30, 2023
58f9920
remove duplicate copy in cascading logic
sleidig Oct 30, 2023
6ae2f4c
reduce un-related changes in this PR
sleidig Oct 30, 2023
0dcf73b
update docs
sleidig Oct 30, 2023
c2dac7b
refactor: split entity-remove service into separate parts and create …
sleidig Oct 31, 2023
3c63872
Merge remote-tracking branch 'origin/master' into anon-3
sleidig Oct 31, 2023
4f49897
tests
sleidig Nov 2, 2023
eb30ff3
fixed tests to correctly check for status of entity
TheSlimvReal Nov 6, 2023
61a81bb
Update src/app/child-dev-project/schools/model/school.ts
sleidig Nov 7, 2023
2053c9f
clarify explanation comment
sleidig Nov 7, 2023
ee9ba6a
feat(*): allow to export only displayed, filtered data (#2059)
brajesh-lab Nov 6, 2023
498387f
feat(*): duplicate records from list views (#2042)
brajesh-lab Nov 6, 2023
e18cb08
refactor: lint check for prettier formatting (#2039)
rudresh Nov 6, 2023
995e85b
Merge branch 'master' into anon-3
sleidig Nov 7, 2023
b629b4d
fix test side-effects
sleidig Nov 7, 2023
2dc7d88
fix cascading tests
sleidig Nov 7, 2023
8a45a8e
simplify undo test setup
sleidig Nov 7, 2023
e651353
popup to warn user if some PII may need manual review
sleidig Nov 7, 2023
1fd139b
fix problems with advance multi-ref cascades
sleidig Nov 7, 2023
641df62
fine tuning from real-life testing
sleidig Nov 7, 2023
5fa37c7
fix prettier
sleidig Nov 7, 2023
c085c24
removed unnecessary overrides
TheSlimvReal Nov 8, 2023
474d3b1
Merge branch 'master' into anon-3
sleidig Nov 10, 2023
d3553d7
fix undo actions
sleidig Nov 10, 2023
5dfbeed
refactor: remove custom implementation for activities-overview
sleidig Nov 10, 2023
86448de
fix(core): subrecord lists update for external changes in real-time
sleidig Nov 10, 2023
71190b6
Merge branch 'subrecord-update-subscr' into anon-3
sleidig Nov 10, 2023
13e9752
progress dialog while (long-running) delete and anonymize actions are…
sleidig Nov 10, 2023
c84bf51
fix(ui): tooltip explaining disabled report calculation button (#2072)
sleidig Nov 13, 2023
025e4fc
fix: panels are only initialized once (#2071)
TheSlimvReal Nov 13, 2023
7f5841b
deps: upgrade multiple dependencies with Snyk (#2070)
sleidig Nov 13, 2023
0fa6b6b
build(deps-dev): bump axios from 1.5.1 to 1.6.1 (#2074)
dependabot[bot] Nov 13, 2023
91999c0
Merge branch 'master' into anon-3
sleidig Nov 14, 2023
fe622d0
fix forceOverwrite implementation return value for putAll
sleidig Nov 14, 2023
1712d11
fix tests
sleidig Nov 14, 2023
1cda158
properly anonymize ASER and Todo entities
sleidig Nov 14, 2023
9dc8ff5
only show one toggle for archived todos
sleidig Nov 14, 2023
c069b3b
don't display "no active entries" button while still loading
sleidig Nov 14, 2023
8848621
exclude archived participants from roll call
sleidig Nov 14, 2023
5e5947c
display indicator if date partially anonymized
sleidig Nov 14, 2023
8b7447f
simplified anon warning text (a little bit)
sleidig Nov 14, 2023
df1ecf2
Merge remote-tracking branch 'origin/master' into anon-3
sleidig Nov 14, 2023
63d4a2b
adapted delete warning text
sleidig Nov 15, 2023
1357bda
Merge branch 'master' into anon-3
sleidig Nov 15, 2023
56c4e41
correct position for code comment
sleidig Nov 19, 2023
3895d38
fix: upgrade keycloak-js from 22.0.4 to 22.0.5 (#2079)
sleidig Nov 16, 2023
7835e87
fix: controls correctly detect edited state (#2076)
TheSlimvReal Nov 16, 2023
843addc
deps: upgrade multiple dependencies with Snyk (#2082)
TheSlimvReal Nov 18, 2023
e7c0225
fix: upgrade @sentry/browser from 7.75.0 to 7.75.1 (#2083)
TheSlimvReal Nov 18, 2023
9f37db6
fix: upgrade flag-icons from 6.11.1 to 6.11.2 (#2084)
TheSlimvReal Nov 19, 2023
4e9f6a3
fix(dashboard): attendance weeks only start from Monday
sleidig Nov 19, 2023
2c3691a
fix: notes have attachments
TheSlimvReal Nov 16, 2023
4d20a42
fix: file attachments can be viewed in edit mode also
sleidig Nov 19, 2023
830bc9d
fix(forms): create new dropdown options from popup config window (#2040)
sadaf895 Nov 19, 2023
42fc02e
fix: new columns correctly trigger input update (#2085)
TheSlimvReal Nov 19, 2023
9bcfbb0
show inactive for anonymized
sleidig Nov 19, 2023
7b39f5d
Merge branch 'master' into anon-3
sleidig Nov 19, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions doc/compodoc_sources/concepts/entities.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
# Entities & Entity Schema
-----
For us, an "Entity" is an object in the database (and a representation of something in the user's real world, e.g. a "Child" or "School").
Entities are at the core of the Aam Digital platform and the primary way to customize the system is to adapt and add new entity types.

The Entity Schema defines the data structure as well as how it is displayed in the UI.
Entity instances also have some generic functionality inherited from the `Entity` base class.

------
_see the sub-pages here for details of the various concepts related to the Entity system_
39 changes: 39 additions & 0 deletions doc/compodoc_sources/concepts/entity-anonymization.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
# Archive / Anonymize Entities
-----
Any entity can be archived (i.e. marked as inactive and hidden from UI by default) or anonymized (i.e. discarding most data and keeping a few selected properties for statistical reports).
This is often preferable to deleting a record completely. Deleting data also affects statistical reports, even for previous time periods.
By anonymizing records, all personal identifiable data can be removed and the remaining stub record can be stored indefinitely, as it is not subject to data protection regulations like GDPR anymore.

Anonymization is configured as part of the entity schema.
Data of fields that are not explicitly marked to be retained during anonymization is always deleted (anonymization by default).

To keep some data even after the user "anonymized" a record, configure the `anonymize` property of the `@DatabaseField` decorator:
- `anonymize: "retain"` will keep this field unchanged and prevent it from being deleted
- `anonymize: "retain-anonymized"` will trigger a special "partial" deletion that depends on the dataType (e.g. date types will be changed to 1st July of the given year, thereby largely removing details but keeping data to calculate a rough age)


## Cascading anonymization / deletion
Relationships between entities are automatically handled when the user anonymizes or deletes an entity.
Any related entities that reference the anonymized/deleted entity are checked
and - depending on their configured role - may be updated or anonymized as well.

The logic follows the scenarios shown below:
![](../../images/cascading-delete.png)


## Data Protection & GDPR regarding anonymization / pseudonomyzation
The "anonymize" function is implemented specifically for data protection rules requiring to delete personal data.
According to the EU's "General Data Protection Regulation" (GDPR) "anonymous" data does not fall under its regulations:

- GDPR is not applicable to anonymous data: "The principles of data protection should therefore not apply to [...] personal data rendered anonymous in such a manner that the data subject is not or no longer identifiable." [<sup>GDPR Recital 26</sup>](https://gdpr-info.eu/recitals/no-26/)
- "To determine whether a natural person is identifiable, account should be taken of all the means reasonably likely to be used, such as singling out, either by the controller or by another person to identify the natural person directly or indirectly."
- "To ascertain whether means are reasonably likely to be used to identify the natural person, account should be taken of all objective factors, such as the costs of and the amount of time required for identification, taking into consideration the available technology at the time of the processing and technological developments."
- "Pseudonymisation enables the personal data to become unidentifiable unless more information is available whereas anonymization allows the processing of personal data to irreversibly prevent re-identification." [<sup>source</sup>](https://www.privacycompany.eu/blogpost-en/what-are-the-differences-between-anonymisation-and-pseudonymisation)
- _also see this [good overview of anonymization misunderstandings and considerations](https://edps.europa.eu/system/files/2021-04/21-04-27_aepd-edps_anonymisation_en_5.pdf)_

In the case of records being retained "anonymized" in Aam Digital, we provide a context that makes re-identification even harder:
- only authorized users of the system can access even the anonymized record (where only a few properties have been retained). Unless the organisation actively shares the data, it remains as securely protected as the personal data managed in Aam Digital.
- those authorized users with access to the anonymized records (and therefor a theoretical chance to attempt re-identification) are team members of an organization. They have been screened to be responsible persons and are usually legally bound to keep information confidential.
- by default only a few, explicitly selected properties in anonymized records are retained (data minimization by default). As such, both re-identification likelihood and the impact in case of re-identification are reduced as far as possible.

--> If our anonymization process is configured thoughfully on a case by case basis to only retain a few data fields that are not easy indirect identifiers, it seems reasonably unlikely that the person can be identified after the anonymization process. Therefore, GDPR should not apply to these records and it is legitimate to retain these for statistical reporting.
39 changes: 1 addition & 38 deletions doc/compodoc_sources/concepts/entity-schema-system.md
Original file line number Diff line number Diff line change
@@ -1,13 +1,5 @@
# Entities & Entity Schema
# Entity Schema
-----
For us, an "Entity" is an object in the database (and a representation of something in the user's real world, e.g. a "Child" or "School").
Entities are at the core of the Aam Digital platform and the primary way to customize the system is to adapt and add new entity types.

The Entity Schema defines the data structure as well as how it is displayed in the UI.
Entity instances also have some generic functionality inherited from the `Entity` base class.


## Entity Schema
The Entity Schema defines details of the properties of an entity type.
We define an entity type and its schema in code through a plain TypeScript class and some custom annotations.
Read more on the background and practical considerations in [How to create a new Entity Type](../how-to-guides/create-a-new-entity-type.html).
Expand Down Expand Up @@ -64,32 +56,3 @@ The `description` field allows adding further explanation which will be displaye
### Metadata (created, updated)
Each record automatically holds basic data of timestamp and user who created and last updated the record.
(see `Entity` class)

### Archive / Anonymize
Any entity can be archived (i.e. marked as inactive and hidden from UI by default) or anonymized (i.e. discarding most data and keeping a few selected properties for statistical reports).
This is often preferable to deleting a record completely. Deleting data also affects statistical reports, even for previous time periods.
By anonymizing records, all personal identifiable data can be removed and the remaining stub record can be stored indefinitely, as it is not subject to data protection regulations like GDPR anymore.

Anonymization is configured as part of the entity schema.
Data of fields that are not explicitly marked to be retained during anonymization is always deleted (anonymization by default).

To keep some data even after the user "anonymized" a record, configure the `anonymize` property of the `@DatabaseField` decorator:
- `anonymize: "retain"` will keep this field unchanged and prevent it from being deleted
- `anonymize: "retain-anonymized"` will trigger a special "partial" deletion that depends on the dataType (e.g. date types will be changed to 1st July of the given year, thereby largely removing details but keeping data to calculate a rough age)

#### Data Protection & GDPR regarding anonymization / pseudonomyzation
The "anonymize" function is implemented specifically for data protection rules requiring to delete personal data.
According to the EU's "General Data Protection Regulation" (GDPR) "anonymous" data does not fall under its regulations:

- GDPR is not applicable to anonymous data: "The principles of data protection should therefore not apply to [...] personal data rendered anonymous in such a manner that the data subject is not or no longer identifiable." [<sup>GDPR Recital 26</sup>](https://gdpr-info.eu/recitals/no-26/)
- "To determine whether a natural person is identifiable, account should be taken of all the means reasonably likely to be used, such as singling out, either by the controller or by another person to identify the natural person directly or indirectly."
- "To ascertain whether means are reasonably likely to be used to identify the natural person, account should be taken of all objective factors, such as the costs of and the amount of time required for identification, taking into consideration the available technology at the time of the processing and technological developments."
- "Pseudonymisation enables the personal data to become unidentifiable unless more information is available whereas anonymization allows the processing of personal data to irreversibly prevent re-identification." [<sup>source</sup>](https://www.privacycompany.eu/blogpost-en/what-are-the-differences-between-anonymisation-and-pseudonymisation)
- _also see this [good overview of anonymization misunderstandings and considerations](https://edps.europa.eu/system/files/2021-04/21-04-27_aepd-edps_anonymisation_en_5.pdf)_

In the case of records being retained "anonymized" in Aam Digital, we provide a context that makes re-identification even harder:
- only authorized users of the system can access even the anonymized record (where only a few properties have been retained). Unless the organisation actively shares the data, it remains as securely protected as the personal data managed in Aam Digital.
- those authorized users with access to the anonymized records (and therefor a theoretical chance to attempt re-identification) are team members of an organization. They have been screened to be responsible persons and are usually legally bound to keep information confidential.
- by default only a few, explicitly selected properties in anonymized records are retained (data minimization by default). As such, both re-identification likelihood and the impact in case of re-identification are reduced as far as possible.

--> If our anonymization process is configured thoughfully on a case by case basis to only retain a few data fields that are not easy indirect identifiers, it seems reasonably unlikely that the person can be identified after the anonymization process. Therefore, GDPR should not apply to these records and it is legitimate to retain these for statistical reporting.
14 changes: 12 additions & 2 deletions doc/compodoc_sources/summary.json
Original file line number Diff line number Diff line change
Expand Up @@ -38,8 +38,18 @@
"file": "concepts/extendability.md"
},
{
"title": "Entity Schema",
"file": "concepts/entity-schema-system.md"
"title": "Entity System",
"file": "concepts/entities.md",
"children": [
{
"title": "Entity Schema",
"file": "concepts/entity-schema-system.md"
},
{
"title": "Archiving, Anonymizing and Deleting Entities",
"file": "concepts/entity-anonymization.md"
}
]
},
{
"title": "Configuration",
Expand Down
Binary file added doc/images/cascading-delete.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
[value]="(currentIndex / children.length) * 100"
></mat-progress-bar>

<div class="progress-nav">
<div class="progress-nav flex-row">
<div style="margin-left: -10px">
<button
mat-icon-button
Expand All @@ -25,12 +25,12 @@
</button>
</div>
<div
class="progress-label"
class="progress-label flex-grow"
[style.visibility]="currentIndex < children.length ? 'visible' : 'hidden'"
>
{{ currentIndex + 1 }} / {{ children.length }}
</div>
<div style="margin-right: -10px">
<div>
<button
mat-icon-button
(click)="goToNext()"
Expand All @@ -48,6 +48,19 @@
<fa-icon icon="angle-double-right"></fa-icon>
</button>
</div>

<div *ngIf="inactiveParticipants?.length > 0" style="margin-right: -10px">
<button
mat-icon-button
(click)="includeInactive()"
[disabled]="isFinished"
color="warn"
matTooltip="Excluded some archived participants. Click to include."
i18n-matTooltip
>
<fa-icon icon="warning"></fa-icon>
</button>
</div>
</div>

<app-child-block
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,8 +39,6 @@

.progress-nav {
margin-top: -4px;
display: flex;
flex-direction: row;
justify-content: space-between;
align-items: center;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,8 @@ import {
} from "@angular/platform-browser";
import Hammer from "hammerjs";
import { ConfigurableEnumService } from "../../../../core/basic-datatypes/configurable-enum/configurable-enum.service";
import { MatTooltipModule } from "@angular/material/tooltip";
import { ConfirmationDialogService } from "../../../../core/common-components/confirmation-dialog/confirmation-dialog.service";

// Only allow horizontal swiping
@Injectable()
Expand Down Expand Up @@ -69,6 +71,7 @@ class HorizontalHammerConfig extends HammerGestureConfig {
NgClass,
RollCallTabComponent,
HammerModule,
MatTooltipModule,
],
providers: [
{
Expand Down Expand Up @@ -114,12 +117,14 @@ export class RollCallComponent implements OnChanges {
availableStatus: AttendanceStatusType[];

children: Child[] = [];
inactiveParticipants: Child[];

constructor(
private enumService: ConfigurableEnumService,
private entityMapper: EntityMapperService,
private formDialog: FormDialogService,
private loggingService: LoggingService,
private confirmationDialog: ConfirmationDialogService,
) {}

async ngOnChanges(changes: SimpleChanges) {
Expand Down Expand Up @@ -165,8 +170,9 @@ export class RollCallComponent implements OnChanges {

private async loadParticipants() {
this.children = [];
this.inactiveParticipants = [];
for (const childId of this.eventEntity.children) {
let child;
let child: Child;
try {
child = await this.entityMapper.load(Child, childId);
} catch (e) {
Expand All @@ -179,7 +185,12 @@ export class RollCallComponent implements OnChanges {
this.eventEntity.removeChild(childId);
continue;
}
this.children.push(child);

if (child.isActive) {
this.children.push(child);
} else {
this.inactiveParticipants.push(child);
}
}
this.sortParticipants();
}
Expand Down Expand Up @@ -254,4 +265,15 @@ export class RollCallComponent implements OnChanges {
showDetails() {
this.formDialog.openFormPopup(this.eventEntity, [], NoteDetailsComponent);
}

async includeInactive() {
const confirmation = await this.confirmationDialog.getConfirmation(
$localize`Also include archived participants?`,
$localize`This event has some participants who are "archived". We automatically remove them from the attendance list for you. Do you want to also include archived participants for this event?`,
);
if (confirmation) {
this.children = [...this.children, ...this.inactiveParticipants];
this.inactiveParticipants = [];
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,6 @@ export class RecurringActivity extends Entity {
validators: {
required: true,
},
anonymize: "retain",
})
title: string = "";

Expand All @@ -68,7 +67,6 @@ export class RecurringActivity extends Entity {
label: $localize`:Label for the interaction type of a recurring activity:Type`,
dataType: "configurable-enum",
innerDataType: INTERACTION_TYPE_CONFIG_ID,
anonymize: "retain",
})
type: InteractionType;

Expand All @@ -77,7 +75,6 @@ export class RecurringActivity extends Entity {
label: $localize`:Label for the participants of a recurring activity:Participants`,
dataType: "entity-array",
additional: Child.ENTITY_TYPE,
anonymize: "retain",
})
participants: string[] = [];

Expand All @@ -86,7 +83,6 @@ export class RecurringActivity extends Entity {
label: $localize`:Label for the linked schools of a recurring activity:Groups`,
dataType: "entity-array",
additional: School.ENTITY_TYPE,
anonymize: "retain",
})
linkedGroups: string[] = [];

Expand All @@ -95,7 +91,6 @@ export class RecurringActivity extends Entity {
label: $localize`:Label for excluded participants of a recurring activity:Excluded Participants`,
dataType: "entity-array",
additional: Child.ENTITY_TYPE,
anonymize: "retain",
})
excludedParticipants: string[] = [];

Expand All @@ -104,7 +99,6 @@ export class RecurringActivity extends Entity {
label: $localize`:Label for the assigned user(s) of a recurring activity:Assigned user(s)`,
dataType: "entity-array",
additional: User.ENTITY_TYPE,
anonymize: "retain",
})
assignedTo: string[] = [];

Expand Down
14 changes: 13 additions & 1 deletion src/app/child-dev-project/children/aser/model/aser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,15 +22,26 @@ import { SkillLevel } from "./skill-levels";
import { WarningLevel } from "../../../warning-level";
import { ConfigurableEnumDatatype } from "../../../../core/basic-datatypes/configurable-enum/configurable-enum-datatype/configurable-enum.datatype";
import { PLACEHOLDERS } from "../../../../core/entity/schema/entity-schema-field";
import { Child } from "../../model/child";

@DatabaseEntity("Aser")
export class Aser extends Entity {
@DatabaseField() child: string; // id of Child entity
static override hasPII = true;

@DatabaseField({
dataType: "entity",
additional: Child.ENTITY_TYPE,
entityReferenceRole: "composite",
})
child: string;

@DatabaseField({
label: $localize`:Label for date of the ASER results:Date`,
defaultValue: PLACEHOLDERS.NOW,
anonymize: "retain-anonymized",
})
date: Date;

@DatabaseField({
label: $localize`:Label of the Hindi ASER result:Hindi`,
dataType: "configurable-enum",
Expand All @@ -55,6 +66,7 @@ export class Aser extends Entity {
innerDataType: "math-levels",
})
math: SkillLevel;

@DatabaseField({
label: $localize`:Label for the remarks of a ASER result:Remarks`,
})
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,18 +19,24 @@ import { Entity } from "../../../../core/entity/model/entity";
import { DatabaseEntity } from "../../../../core/entity/database-entity.decorator";
import { DatabaseField } from "../../../../core/entity/database-field.decorator";
import { WarningLevel } from "../../../warning-level";
import { Child } from "../../model/child";

/**
* Model Class for the Health Checks that are taken for a Child.
* It stores the Child's ID in a String and both, the height and weight in cm as a number, and the Date
*/
@DatabaseEntity("HealthCheck")
export class HealthCheck extends Entity {
static override hasPII = true;

static create(contents: Partial<HealthCheck>) {
return Object.assign(new HealthCheck(), contents);
}

@DatabaseField({
dataType: "entity",
additional: Child.ENTITY_TYPE,
entityReferenceRole: "composite",
anonymize: "retain",
})
child: string;
Expand Down
1 change: 1 addition & 0 deletions src/app/child-dev-project/children/model/child.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ export class Child extends Entity {
static label = $localize`:label for entity:Participant`;
static labelPlural = $localize`:label (plural) for entity:Participants`;
static color = "#1565C0";
static override hasPII = true;

static create(name: string): Child {
const instance = new Child();
Expand Down
Loading