Versionen im Vergleich

Schlüssel

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

Inhalt
maxLevel2

Um DSGVO Anforderungen einfacher umsetzen zu können wurden im Nuclos Core ab Version 4.2022.7 einige Erweiterungen geschaffen. Diese Seite gibt eine kleine Übersicht um welche Erweiterungen es sich genau handelt, und wie diese beispielhaft eingesetzt werden können.

Nuclos Systemobjekte

Nuclos Benutzer

Benutzer können nun über die API gelöscht werden:

org.nuclos.api.provider.UserProvider#delete
Codeblock
languagejava
titleBeispiel Job zum entfernen gesperrter Benutzer
linenumberstrue
collapsetrue
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()))));
	}
}



Statistiken der Benutzer Sitzungen (T_AD_SESSIONS) werden mit dem neuen Systemparameter PRIVACY_DELETE_SESSIONLOG_AFTER_PERIOD (Zeitraum in Tagen) automatisch um 0 Uhr gelöscht.

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());
	}
}