Versionen im Vergleich

Schlüssel

  • Diese Zeile wurde hinzugefügt.
  • Diese Zeile wurde entfernt.
  • Formatierung wurde geändert.
Auszug
hiddentrue

Nuclos DSGVO: GDPR, UserProvider delete, PRIVACY_DELETE_SESSIONLOG_AFTER_PERIOD, T_AD_SESSIONS, T_UD_HISTORY, Historisierung, 4.2022.7.

globe with meridians Sprache: Deutsch · English

light bulb DSGVO Umsetzungen

DSGVO-Funktionen im Nuclos-Core – Benutzer per API löschen, Sitzungslog aufräumen und Historisierung steuern.

Status
colourGreen
titleKonzept
Status
colourBlue
titleEntwickler
Status
colourGreen
titleStand: Jul 2026
Status
colourGrey
titlegilt fuer Nuclos 4.2026.x

Panel
bgColor#F4F5F7

Auf dieser Seite

Inhalt
maxLevel2

...

minLevel2

Zur einfacheren Umsetzung von DSGVO-Anforderungen wurden im Nuclos-Core ab Version 4.2022.7

...

mehrere Erweiterungen geschaffen.

...

...

Benutzer über die API löschen

Benutzer können

...

über

...

org.nuclos.api.provider.UserProvider#delete gelöscht werden – z.​B. per Job, der lange gesperrte Benutzer entfernt:

Codeblock
languagejava

...

...

import java.util.ArrayList;
import java.util.Calendar;
import java.util.List;

import org.nuclos.api.context.JobContext;
import org.nuclos.api.exception.BusinessException;
import org.nuclos.api.provider.QueryProvider;
import org.nuclos.api.provider.UserProvider;
import org.nuclos.api.rule.JobRule;
import org.nuclos.api.rule.TransactionalJobRule;
import org.nuclos.system.User;

public class DSGVOGesperrteUserLoeschen implements JobRule, TransactionalJobRule {
	@Override
	public void execute(final JobContext context) throws BusinessException {
		User user = (User) context.getTransactionalObject();
		context.joblog("delete user " + user.getUsername());
		UserProvider.delete(user);
	}

	@Override
	public List<Object> getTransactionalObjects(final JobContext context) {
		Calendar calendar = Calendar.getInstance();
		calendar.add(Calendar.DAY_OF_YEAR, -90); // Letzte Änderung am User ist 90 Tage her
		return new ArrayList<>(QueryProvider.execute(QueryProvider.create(User.class)
				.where(User.Locked.eq(true))
				.and(User.ChangedAt.Lt(calendar.getTime()))));
	}
}

Weitere Erweiterungen

  • Sitzungs-Statistiken

...

  • (T_AD_SESSIONS) werden

...

  • über den Systemparameter PRIVACY_DELETE_SESSIONLOG_AFTER_PERIOD (

...

  • Tage) automatisch um 0 Uhr gelöscht.
  • History (T_UD_HISTORY): Änderungen werden historisiert; die Aufbewahrung lässt sich pro Attribut im BO-Wizard konfigurieren.

Verwandte Seiten

gear Systemtabellen


Systemtabellen.

Öffnen →

Nuclos History

Welcher Benutzer für welchen Wert in einem Businessobjekt verantwortlich ist, kann in Nuclos über dieHistorisierungsfunktion nachverfolgt werden, sofern eingeschaltet. Die Änderungen werden in der System Tabelle T_UD_HISTORY historisiert.

Da bestimmte Attribute länger aufbewahrt werden müssen als andere, ist eine Unterscheidung nach Attributen nötig, welche im Businessobjekt Wizard am Attribut konfiguriert werden kann. Ein Eingabefeld "Anzahl Tage, nach denen die Statusinformationen anonymisiert werden" hinter der Aktivierungs-Checkbox der Historisierungsfunktion erlaubt die Steuerung.

Einmal wöchentlich Sonntags um 0 Uhr wird die Nuclos History anonymisiert. Benutzername, wie auch die Metainformationen "Erstellt von" / "Geändert von" werden dann durch ***** ersetzt.

Nuclet Businessobjekte

Datensätze bzw. einzelne Attribute können vollkommen unterschiedlicher Lösch- oder Anonymisierungsfristen unterliegen, damit ist eine individuelle Lösung pro Nuclet unumgänglich. Auch wird auf fachliche Sachverhalte wie Pflichtfeld oder nicht, Verwendung in weiterer Businesslogik wie dem Versand von Mails o.ä., Rücksicht genommen werden müssen.

Wir empfehlen die Anonymisierung von Daten innerhalb eines Jobs, ein Beispiel finden Sie am Ende dieser Seite.

Nuclos bietet über die API nun eine Änderung der Metainformationen "Erstellt von" und "Geändert von" an:

org.nuclos.api.businessobject.facade.Modifiable#setCreatedBy
org.nuclos.api.businessobject.facade.Modifiable#setChangedBy

Im Normalfall lässt Nuclos eine Änderung des "Erstellt von" nicht zu, und setzt automatisch "Geändert von" auf den aktuellen Benutzer. Erst wenn die laufende Transaktion mit

org.nuclos.api.transaction.CurrentTransaction#disableBusinessObjectMetaUpdate

markiert wird, werden bei einem Speichervorgang die neuen Werte geschrieben. Die Versionsnummer wird nicht hochgezählt. Andere parallel laufende Transaktionen sind davon nicht betroffen.

Vorhandene Businesslogik könnte eine Speicherung verhindern, oder zu ungewollten Schnittstellenaufrufen o.ä. führen. Um dem entgegen zu wirken kann die Business Rule Engine für die laufende Transaktion deaktiviert werden:

org.nuclos.api.transaction.CurrentTransaction#disableRuleExecution

Zusätzlich wird auch die Pflichtfeldprüfung des Nuclos Kerns deaktiviert, ähnlich dem Direktimport, da mögliche statusabhängige Pflichtfelder, welche erst nachträglich eingeführt worden sind, eine Änderung an älteren Datenbeständen sonst verhindern würden. NotNull Constraints der Datenbank bleiben unangetastet und sind immer aktiv.

Warnung

Äußerste Vorsicht bei Verwendung dieser Funktion ist geboten. Fehldatenbestände könnten sonst die Folge sein!

Große Datenmengen, gerade bei der Einführung solch einer Implementierung, werden sicherlich den Speicher des Application Servers stark belasten, wenn nicht sogar über Gebühr (OutOfMemory). Hier helfen die neuen API Methoden der Query:

org.nuclos.api.businessobject.Query#setOffset
org.nuclos.api.businessobject.Query#setChunkSize

Bekannt aus dem Datenbank und SQL Umfeld.

Eine Query kann damit auf mehrere Chunks aufgeteilt und so Stück für Stück verarbeitet werden. Aber Achtung, die Transaktion in der Datenbank, sofern nicht ebenfalls aufgeteilt, wächst bis zu einem Commit immer weiter an.

Um nicht jedes Businessobjekt einzeln zu implementieren, und die Gefahr zu umgehen eines zu vergessen, liefert die neue Methode

org.nuclos.api.provider.BusinessObjectProvider#getAllModifiableBusinessObjectClassNames

einen generischen Ansatz alle oder zumindest einen großen Teil gesammelt zu verarbeiten.

Codeblock
languagejava
titleBeispiel Job zur Anonymisierung der Nuclos Metainformationen
linenumberstrue
collapsetrue
import java.util.Arrays; import java.util.Calendar; import java.util.Date; import java.util.List; import java.util.stream.Collectors; import org.nuclos.api.businessobject.Query; import org.nuclos.api.businessobject.attribute.Attribute; import org.nuclos.api.businessobject.attribute.NumericAttribute; import org.nuclos.api.businessobject.facade.Modifiable; import org.nuclos.api.context.JobContext; import org.nuclos.api.exception.BusinessException; import org.nuclos.api.provider.BusinessObjectProvider; import org.nuclos.api.provider.QueryProvider; import org.nuclos.api.rule.JobRule; import org.nuclos.api.rule.TransactionalJobRule; import org.nuclos.api.transaction.CurrentTransaction; public class DSGVOAnonymisiereAlleMetainformationen implements JobRule, TransactionalJobRule { private JobContext context; @Override public void execute(final JobContext context) throws BusinessException { this.context = context; Class entityClass = (Class) context.getTransactionalObject(); String className = entityClass.getName(); Attribute changedBy = getAttribute(entityClass, "ChangedBy"); NumericAttribute changedAt = (NumericAttribute) getAttribute(entityClass, "ChangedAt"); Calendar calendar = Calendar.getInstance(); calendar.add(Calendar.DAY_OF_YEAR, -90); Date dateLastChange90d = calendar.getTime(); // Letzte Änderung ist 90 Tage her CurrentTransaction.disableRuleExecution(); CurrentTransaction.disableBusinessObjectMetaUpdate(); long countAnonymized = processQuery(className, QueryProvider.create(entityClass) .where(changedBy.neq("*****")) .and(changedAt.Lt(dateLastChange90d))); context.joblog(String.format("%s: %d anonymized", className, countAnonymized)); } private long processQuery(final String className, final Query query) throws BusinessException { final long chunkSize = 500; final long offset = 0; query.setChunkSize(chunkSize); query.setOffset(offset); long counter = 0; boolean existMoreEntries = true; while (existMoreEntries) { context.joblog(String.format("%s: process objects %d - %d", className, counter, (counter + chunkSize))); List entries = QueryProvider.execute(query); if (entries.isEmpty()) { existMoreEntries = false; } else { for (Object entry : entries) { Modifiable modifiable = (Modifiable) entry; modifiable.setCreatedBy("*****"); modifiable.setChangedBy("*****"); modifiable.save(); counter++; } if (entries.size() < chunkSize) { existMoreEntries = false; } } } return counter; } private Attribute getAttribute(Class entityClass, String name) { return Arrays.stream(entityClass.getDeclaredFields()) .filter(field -> field.getName().equals(name)) .findFirst() .map(field -> { try { return (Attribute) field.get(null); } catch (IllegalAccessException ex) { throw new RuntimeException(ex); } }) .orElseThrow(() -> new RuntimeException(String.format("Attribute %s.%s does not exist", entityClass, name))); } @Override public List<Object> getTransactionalObjects(final JobContext context) { return BusinessObjectProvider.getAllModifiableBusinessObjectClassNames().stream() .sorted() .map(className -> { try { return Class.forName(className); } catch (ClassNotFoundException e) { throw new RuntimeException(e); } }) .collect(Collectors.toList()); } }