Dies ist eine alte Version des Dokuments!


User Deprovisionierung via Attribute Query

Dies ist keine fertige Schritt für Schritt Anleitung wie man eine „shibbolisierte“ Webanwendung (die Daten über Ihre Nutzer speichert) bereinigt, sondern die aufgeführten Punkte sollen lediglich als Gedankenanstoß dienen, wie man das Thema User Deprovisionierung angehen kann und was es zu beachten gilt.

Problem hierbei ist meist, dass man bei dem Shibboleth-Verfahren nur dann Informationen über einen Nutzer erhält, wenn sich dieser aktiv einloggt.
(Ausgenommen: man hat seinen Dienst noch über eine weitere Schnittstelle an das eigene IDM-System angeschlossen)

Also wie entscheiden wir nun ob ein Nutzer noch existiert oder nicht?
Überlegen wir mal kurz welche Möglichkeiten sich uns bieten, wenn wir nun „alte“ Nutzer identifizieren und sperren oder gar entfernen wollen:

  1. Sperren und Löschen des Nutzers nach definierter Inaktivität dessen
    • Positiv: einfachster Weg ohne die Notwendigkeit irgendwelcher Erweiterungen
    • Negativ: Nutzererfahren unschön wenn man nach längerer Inaktivität einen leeren Account vorfindet und/oder sich in regelmäßigen Abständen einloggen muss (eventuell vorher per Mail benachrichtigen?!)
  2. Abfrage des IDM-Systems (regelmäßig oder nach definierter Inaktivität des Nutzers) über eine weitere Schnittstelle
    • Positiv: Nutzer wird nur gesperrt/gelöscht wenn er wirklich nicht mehr existiert
    • Negativ: größerer Aufwand durch Anbindung über weitere Schnittstelle (eventuell Probleme mit Datenschutz und anderen technischen Vorkehrungen etc.)
  3. Abfrage des Shibboleth-IdP (regelmäßig oder nach definierter Inaktivität des Nutzers) via Attribute-Query
    • Positiv: Nutzung einer bereits existierenden Schnittstelle
    • Negativ: es sind teilweise Anpassungen am SP und/oder IDM nötig

Betrachten wir die letzte Variante mit Hilfe des Shibboleth-eigenen Board-Mittels namens Attribute-Query.
Ursprünglich diente dieses Verfahren bei SAML1 der direkten Übertragung der Nutzer-Attribute zwischen IdP und SP über einen Backchannel.
Hier stellte der SP nach erfolgreichem Login des Nutzers eine separate Anfrage an den IdP, welcher alle Attribute über den Nutzer auslieferte, die beim heutigen SAML2 über den Frontchannel übergeben werden.

Um solch einen Query stellen zu können muss der SP im Besitz eines gültigen NameIdentifiers sein, damit der IdP diesen Auflösen kann.
Wenn ein SP einen Query zu einem beliebigen Zeitpunkt stellen möchte (unabhängig vom Vorhandensein eines Login-Contextes des Nutzers), so kommt hier lediglich die „persistentId“ in Frage.
Diese muss auf Seiten des IdP dazu in einer Datenbank gespeichert werden, damit Sie rückwärts aufgelöst werden kann.

Anforderungen

  • persistentId wird an SP ausgeliefert
  • persistentId wird auf IdP Seite gespeichert
  • persistentId wird auf SP Seite gespeichert
  • Nutzer hat sich wenigstens einmal am SP angemeldet
  • Attribute Query Profil ist für RelyingParty freigeschalten
  • SP hat Möglichkeit einen Query zu stellen
  • SP erreicht den IdP direkt (ohne Umweg/Redirect über den Browser des Client)
  • es wird wenigstens ein Attribut an den SP ausgeliefert

Wie stellt man einen Query?

Standardmäßig bringt der SP ein „resolvertest“-Skript mit.
Dieses empfiehlt sich jedoch nur für einen initialen Test, um zu Prüfen ob alle Einstellungen passen und der Attribute Query korrekt beantwortet wird.
Für den Produktiv-Einsatz arbeitet es zu langsam!

# ./resolvertest -n +9Blu1I8v96axDXHj01Gmpg36fM= -i https://your-idp.de/idp/shibboleth -saml2 -f urn:oasis:names:tc:SAML:2.0:nameid-format:persistent

# persistentId: https://your-idp.de/idp/shibboleth!https://your-sp.de/shibboleth!+9Blu1I8v96axDXHj01Gmpg36fM=
# givenName: Max
# surname: Mustermann
# ...

Hat man Zugriff zum IdP kann man die übermittelten Attribute auf Vollständigkeit überprüfen, indem man am IdP nochmal den resolvertest ausführt.

# wget https://your-idp.de/idp/profile/admin/resolvertest?requester=https://your-sp.de&principal=uid-des-test-nutzers

# persistentId: https://your-idp.de/idp/shibboleth!https://your-sp.de/shibboleth!+9Blu1I8v96axDXHj01Gmpg36fM=
# givenName: Max
# surname: Mustermann
# ...

Will man nun vom SP aus automatisiert Anfragen stellen so empfiehlt es sich auf den Attribute Handler bzw. die Erweiterung der Gakunin-Föderation zurückgreifen.

/etc/shibboleth/shibboleth2.xml
<!-- ... -->
	<OutOfProcess>
		<Extensions>
			<Library path="attributequery-handler.so" fatal="true"/>
		</Extensions>
	</OutOfProcess>
 
	<InProcess>
		<Extensions>
			<Library path="attributequery-handler-lite.so" fatal="true"/>
		</Extensions>
	</InProcess>
 
	<ApplicationDefaults...>
		<Sessions...>
			<!-- ... -->
			<Handler type="AttributeQuery" Location="/AttributeQuery" acl="127.0.0.1 ::1" />
			<!-- ... -->
		</Sessions>
		<!-- ... -->
	</ApplicationDefaults>
<!-- ... -->

Vergessen Sie nicht den „shibd“ neu zu starten!
Danach kann ein Aufruf z.B. direkt mit CURL durchgeführt werden.

!!!ACHTUNG!!!
persistentId muss codiert übergeben werden, da Sonderzeichen zu Misserfolg führen!!!
Stichwort URLencoding!!!

# curl -k "https://your-sp.de/Shibboleth.sso/AttributeQuery?entityID=https://your-idp.de/idp/shibboleth" --data-urlencode "nameId=+9Blu1I8v96axDXHj01Gmpg36fM="

Alternativ kann dies auch mit Hilfe des mitgelieferten Python-Skriptes ausgeführt werden.

# /etc/shibboleth/attributequery.py https://your-sp.de/Shibboleth.sso/AttributeQuery https://your-idp.de/idp/shibboleth +9Blu1I8v96axDXHj01Gmpg36fM=

Hat man seine ersten Queries erfolgreich gestellt, kommen schnell die Fragen auf:
Wann kann ich den Nutzer löschen? und Wie verlässlich ist die Rückgabe?

Wenn wir davon ausgehen dass ein Query das gleiche Attribut-Set zurück liefert, wie ein normaler Login-Vorgang, dann sollte man den Nutzer löschen können, sobald eine leere Menge zurück kommt.
Aber ACHTUNG!!!
Folgende Fallstricke können fälschlicherweise zu einem leeren oder auch nicht leeren Ergebnis führen:

leeres Ergebnis

  • persistentId wurde falsch übergeben
  • persistentId existiert nicht auf Seiten des IdP
  • persistentId wird auf Seiten des IdP generell nicht gespeichert
  • Nutzer ist nicht mehr im LDAP (obwohl noch im IDM vorhanden - Synchronisierungsproblem)
  • LDAP-Verbindung ist abgebrochen
  • Query ist fehlgeschlagen

nicht leeres Ergebnis

  • Statische Attribute im Resolver liefern immer Werte (obwohl Nutzer nicht mehr existent)
  • Nutzer wurde im LDAP nicht entfernt (obwohl im IDM nicht mehr vorhanden - Synchronisierungsproblem)
  • Nutzer Passwort wurde im LDAP geändert (damit er sich nicht mehr einloggen kann)

Prinzipiell kann behauptet werden, dass man einem leeren Ergebnis nicht trauen kann und sollte!
Doch wie schließt man nun die diversen Fehler bei der Übertragung aus?

Sicherer wäre es, wenn man ein Attribut bekommt, was den Nutzerstatus abbildet und an Hand dessen man entscheidet, ob der Nutzer deaktiviert bzw. gelöscht werden kann.
Denn wenn man einen definierten Wert für das Attribut erhält kann man zumindest davon ausgehen, dass alles funktioniert hat und der Query erfolgreich abgearbeitet wurde und auch sonst keine Probleme bei der Abfrage des LDAP oder sonst irgendwo bei der Kommunikation zwischen den Systemen aufgetreten sind!

Um den LDAP nicht mit Karteileichen vollzumüllen, so wäre eine Möglichkeit die Struktur des LDAP wie folgt zu erweitern:

  • ou=users,dc=einrichtung,dc=de (nur aktive Nutzer)
  • ou=archive,dc=einrichtung,dc=de
    • ou=users,ou=archive,dc=einrichtung,dc=de (nur archivierte Nutzer)
      • ou=disabled,ou=users,ou=archive,dc=einrichtung,dc=de (gelöscht, kann aber wieder angelegt werden)
      • ou=locked,ou=users,ou=archive,dc=einrichtung,dc=de (vorübergehend gesperrt, z.B. aus Sicherheitsgründen)
      • ou=deleted,ou=users,ou=archive,dc=einrichtung,dc=de (endgültig gelöschte Nutzer, falls die Identitäten noch gespeichert werden sollen, z.B. um sicherzustellen, dass User IDs nicht neu vergeben werden)

Scheidet ein Nutzer aus oder muss z.B. auf Grund eines Sicherheitsvorfalls kurzfristig deaktiviert werden, so verschiebt man diesen zunächst nach locked.
Damit kann er sich schon mal nicht mehr am IdP authentifizieren (da nicht mehr im baseDN).
Kommt der Nutzer nach x-Tagen nicht mehr zurück an die Einrichtung so kann man ihn dauerhaft löschen und nach disabled oder deleted verschieben.

Generell reicht es zu, wenn die Einträge unter ou=archive nur noch die „uid“ beinhalten und nicht mehr alle Attribute. (das spart Speicher ^^)

Jetzt definieren wir im IdP das Attribut schacUserStatus, das den Status der betreffenden Identität abbilden soll:

Vokabular für schacUserStatus:

  • Attributwert: urn:schac:userStatus:de:aai.dfn.de:idmStatus:STATUS, wobei STATUS die folgenden Werte annehmen kann:
  • active (optional) (entspricht AD 'enabled')
  • locked (optional) (vorübergehend gesperrt, z.B. aus Sicherheitsgründen)
  • disabled (optional) (gelöscht, kann aber wieder angelegt werden)
  • deleted (mandatory) (Account und Benutzerinformationen gelöscht)
conf/attribute-resolver.xml
<!--...-->
	<!-- look in dn of archiveLDAP for inactive or blocked -->
	<!-- schacUserStatus -->
	<AttributeDefinition xsi:type="ScriptedAttribute" id="schacUserStatus">
		<InputDataConnector ref="archiveLDAP" />
		<DisplayName xml:lang="de">Benutzerstatus</DisplayName>
		<DisplayName xml:lang="en">Userstatus</DisplayName>
		<DisplayDescription xml:lang="de">Status eines Benutzers für einen Dienst</DisplayDescription>
		<DisplayDescription xml:lang="en">set of status of a person as user of services</DisplayDescription>
		<AttributeEncoder xsi:type="SAML1String" name="urn:mace:terena.org:schac:attribut-def:schacUserStatus" />
		<AttributeEncoder xsi:type="SAML2String" name="urn:oid:1.3.6.1.4.1.25178.1.2.19" friendlyName="schacUserStatus" />
		<Script>
			<![CDATA[
				if (typeof entryDN != "undefined" && entryDN.getValues().size() > 0) {
					var prefix = "urn:schac:userStatus:de:aai.dfn.de:";
					var disabled= "ou=disabled,ou=users,ou=archive,dc=einrichtung,dc=de";
					var locked  = "ou=locked,ou=users,ou=archive,dc=einrichtung,dc=de";
                                        var deleted = "ou=deleted,ou=users,ou=archive,dc=einrichtung,dc=de";
 
					for (i=0; i<entryDN.getValues().size(); i++) {
						var tmp = entryDN.getValues().get(i);
 
						if (tmp.endsWith(disabled)) {
							schacUserStatus.addValue(prefix + "idmStatus:disabled");
						}
						else if (tmp.endsWith(locked)) {
							schacUserStatus.addValue(prefix + "idmStatus:locked");
						}
						else if (tmp.endsWith(deleted)) {
							schacUserStatus.addValue(prefix + "idmStatus:deleted");
						}
						else {
							schacUserStatus.addValue(prefix + "idmStatus:active");
						}
					}
				}
			]]>
		</Script>
	</AttributeDefinition>
 
	<DataConnector id="archiveLDAP" xsi:type="LDAPDirectory"
		ldapURL="%{idp.attribute.resolver.LDAP.ldapURL}"
		baseDN="%{idp.attribute.resolver.LDAP.archiveDN}" 
		principal="%{idp.attribute.resolver.LDAP.bindDN}"
		principalCredential="%{idp.attribute.resolver.LDAP.bindDNCredential}"
		searchScope="SUBTREE"
		maxResultSize="0"
		useStartTLS="%{idp.attribute.resolver.LDAP.useStartTLS}">
 
		<FilterTemplate>
			<![CDATA[
				%{idp.attribute.resolver.LDAP.archiveSearchFilter}
			]]>
		</FilterTemplate>
	</DataConnector>
<!--...-->
conf/ldap.properties
<!--...-->
	idp.attribute.resolver.LDAP.archiveDN           = ou=archive,dc=einrichtung,dc=de
	idp.attribute.resolver.LDAP.archiveSearchFilter = (uid=$requestContext.principalName)
<!--...-->
conf/attribute-filter.xml
<!--...-->
	<AttributeRule attributeID="schacUserStatus">
		<PermitValueRule xsi:type="ANY" />
	</AttributeRule>
<!--...-->

Bekommt ein SP nun bei einem Query einen der folgenden Werte, so kann er den Nutzer verlässlich sperren oder gar löschen.

  • urn:schac:userStatus:de:aai.dfn.de:idmStatus:disabled
  • urn:schac:userStatus:de:aai.dfn.de:idmStatus:locked
  • urn:schac:userStatus:de:aai.dfn.de:idmStatus:deleted

Als Beispiel für eine Attribute Query, die ein solches Attribut liefert, kann dieser URL genutzt werden:
https://testsp3.aai.dfn.de/Shibboleth.sso/AttributeResolver?entityID=https://testidp.aai.dfn.de/idp/shibboleth&nameId=MCE6NXEQ3FC3PUKY4M75EYCOWN4TGKBH&format=urn:oasis:names:tc:SAML:2.0:nameid-format:persistent

Wer darf eigentlich Queries stellen?

Da Queries nur mit Angabe der persistentId funktionieren, so kann man per RelyingParty-Config einschränken, dass nur die SPs Queries stellen dürfen für die auch die persistentId freigegeben ist.
Dies lässt sich unter Angabe einer Activation Condition simpel lösen.
ACHTUNG: Diese Einstellung ist nur zu empfehlen, wenn keine SAML1 SPs mehr bedient werden!!!

conf/relying-party.xml
<!--...-->
	<bean id="shibboleth.DefaultRelyingParty" parent="RelyingParty">
		<property name="profileConfigurations">
			<list>
				<bean parent="SAML2.SSO" p:postAuthenticationFlows="#{{'attribute-release'}}" p:nameIDFormatPrecedence="#{{'urn:oasis:names:tc:SAML:2.0:nameid-format:transient'}}" />
				<ref bean="SAML2.Logout" />
				<ref bean="SAML2.ArtifactResolution" />
			</list>
		</property>
	</bean>
 
	<util:list id="shibboleth.RelyingPartyOverrides">
		<bean parent="RelyingParty" p:activationCondition-ref="SP-consumes-persistentId">
			<property name="profileConfigurations">
				<list>
					<bean parent="SAML2.SSO" p:postAuthenticationFlows="#{{'attribute-release'}}" p:nameIDFormatPrecedence="#{{'urn:oasis:names:tc:SAML:2.0:nameid-format:persistent', 'urn:oasis:names:tc:SAML:2.0:nameid-format:transient'}}" />
					<ref bean="SAML2.Logout" />
					<ref bean="SAML2.AttributeQuery" />
					<ref bean="SAML2.ArtifactResolution" />
				</list>
			</property>
		</bean>
	</util:list>
<!--...-->
conf/activation-conditions.xml
<!--...-->
	<bean id="SP-consumes-persistentId" parent="shibboleth.Conditions.RelyingPartyId">
		<constructor-arg name="candidates">
			<list>
				<value>https://your-sp.de/shibboleth</value>
				<value>https://another-sp.de/shibboleth</value>
			</list>
		</constructor-arg>
	</bean>
<!--...-->

Wozu sollen Queries genutzt werden?

  • User-Synchronisierung
  • User-Deprovisionierung

Möchte man via Query Nutzerdaten synchron halten, so sind keine weiteren Einstellungen nötig.
Es empfiehlt sich jedoch im Vorfeld mit dem Datenschutzbeauftragten dieses Vorgehen im Vorfeld zu besprechen, da hier ohne Einverständnis des Nutzers personenbezogene Daten ausgetauscht werden!!!

Will man Queries ausschließlich zur User-Deprovisionierung benutzen, so kann man sämtliche anderen Attribute für diesen Kanal deaktivieren.
Dies bringt unter anderem folgende Vorteile mit sich:

  • Minimierung der openLDAP Abfragen
  • Lastminimierung bei scripted Attributes
  • Datenschutz + Datensparsamkeit!

Dazu laden wir uns zunächst folgende JAR-File idp-predicate-impl-1.0.0.jar herunter und legen diese unter „./edit-webapp/WEB-INF/lib/“ ab.
Anschließend den IdP neubauen.

# ./bin/build.sh

Danke noch mal an Steffen Hofmann (FU Berlin), der diese Datei zur Verfügung gestellt hat.
Mit Hilfe des enthaltenen Predicate ist es uns möglich eine Entscheidung zu treffen ob es sich um einen Attribute-Query handelt oder nicht.
Darauf aufbauend erstellen wir uns beliebige Activation Conditions, um die Erstellung und Freigabe von Attributen zu steuern.

conf/activation-conditions.xml
<!--...-->
	<!-- condition is true if request is NOT an attribute-query -->
	<bean id="no-query" parent="shibboleth.Conditions.NOT">
		<constructor-arg>
			<list>
				<ref bean="RequestedAttributeQueryProfileIdPredicate" />
			</list>
		</constructor-arg>
	</bean>
 
	<!-- condition is true if request is NOT an attribute-query and if sp is one of the following -->
	<bean id="SP-consumes-isMemberOf" parent="shibboleth.Conditions.AND">
		<constructor-arg>
			<list>
				<ref bean="no-query" />
				<bean parent="shibboleth.Conditions.RelyingPartyId">
					<constructor-arg name="candidates">
						<list>
							<value>https://your-sp.de/shibboleth</value>
							<value>https://another-sp.de/shibboleth</value>
						</list>
					</constructor-arg>
				</bean>
			</list>
		</constructor-arg>
	</bean>
 
	<!-- condition is true if request IS an attribute-query and if sp consumes the persistentId -->
	<bean id="SP-consumes-schacUserStatus" parent="shibboleth.Conditions.AND">
		<constructor-arg>
			<list>
				<ref bean="RequestedAttributeQueryProfileIdPredicate" />
				<ref bean="SP-consumes-persistentId" />
			</list>
		</constructor-arg>
	</bean>
<!--...-->

Nun aktivieren wir die Bedingungen im Resolver.
Dadurch werden normale Attribute nur noch bei einem normalen Login gebaut und an den Filter weitergereicht werden, aber nicht bei einem Query.
Das schacUserStatus Attribut hingegen wird nur noch bei einem Query erstellt.

conf/attribute-resolver.xml
<!--...-->
	# Die Angabe einer ActivationCondition an einem normalen Attribut führt leider nicht dazu, dass die LDAP-Abfragen nur gestellt werden, wenn der SP in der Condition steht
	# Sondern:
	#	- Das Attribut wird erst gebaut (LDAP-Abfrage) und erst danach greift die Condition und regelt die Weitergabe des Attributs an die Filter
	#	- Die Condition MUSS daher an die LDAP-Dependency (DataConnector) gehangen werden, um die Ausführung der LDAP-Anfragen zu steuern/vermeiden
 
	<AttributeDefinition xsi:type="Simple" id="uid">
		<InputDataConnector ref="myLDAP" attributeNames="uid"/>
		<!--...-->
	</AttributeDefinition>
 
	<DataConnector id="myLDAP" xsi:type="LDAPDirectory" activationConditionRef="no-query"
		<!--...-->
	</DataConnector>
 
	# Bei Attributen, die zusätzliche Conditions erhalten sollen können diese direkt am Attribut referenziert werden
 
	<AttributeDefinition xsi:type="Simple" id="isMemberOf" activationConditionRef="SP-consumes-isMemberOf">
		<InputDataConnector ref="groupLDAP" attributeNames="cn"/>
		<!--...-->
	</AttributeDefinition>
 
	<DataConnector id="groupLDAP" xsi:type="LDAPDirectory" activationConditionRef="SP-consumes-isMemberOf"
		<!--...-->
	</DataConnector>
 
	<AttributeDefinition xsi:type="ScriptedAttribute" id="schacUserStatus" activationConditionRef="SP-consumes-schacUserStatus">
		<InputDataConnector ref="archiveLDAP" />
		<!--...-->
	</AttributeDefinition>
 
	<DataConnector id="archiveLDAP" xsi:type="LDAPDirectory" activationConditionRef="SP-consumes-schacUserStatus"
		<!--...-->
	</DataConnector>
<!--...-->

Zusammenfassend hat man somit folgende Punkte realisiert:

  • attributeQueries sind nur für SPs freigeschalten, die laut relying-party.xml auch die persistentID beziehen (ohne diese ist eine Abfrage erst gar nicht möglich)
  • wer diese bezieht ist in der activation-conditions.xml unter der bean „SP-consumes-persistentId“ definiert
  • bei einem query soll lediglich das attribut „schacUserStatus“ (und eventuell die „affiliation“?) ausgelesen und ausgeliefert werden
  • alle anderen attribute „nur“ bei normalen logins (datenschutz und entlastung des ldap)
  • die einschränkungen müssen in der attribute-resolver.xml mittels activationConditionRef realisiert werden
  • jeder dataConnector erhält eine condition, somit werden z.B. unnötige LDAP-Abfragen vorweg vermieden
  • jedes attribut welches nur von einem dataconnector abhängt braucht nicht unbedingt eine weitere condition
  • lediglich attribute die von keinem connector oder von anderen attributen (ohne connector) abhängen benötigen eine zusätzliche condition
  • teils verknüpfung mehrerer bedingungen bei den conditions da einige attribute z.B. nur an bestimmte SPs ausgeliefert werden

Wann und wie oft sollten Queries gestellt werden?

Zunächst vorweg:
Der SP ist verantwortlich dafür Sorge zu tragen, dass der IdP während seines Betriebes nicht gestört wird, in dem er z.B. mit zu vielen Anfragen „bombardiert“ wird!!!

Daher empfiehlt es sich Nutzer nur dann zu prüfen wenn folgende Bedingungen erfüllt sind:

  • letzter login > x Tage
  • letzte Prüfung > x Tage

Desweiteren sollte man kleine Pausen zwischen einzelnen Queries lassen, damit man als SP nicht Gefahr läuft z.B. durch Mechanismen wie Fail2Ban ausgesperrt zu werden.
Als Betreiber eines IdP ist es durchaus denkbar sich ähnlich wie im Thema Abwehr Brute Force Gedanken zu machen um sich gegen zu viele Query-Anfragen zu schützen.

Am Besten man nutzt also eine Cachefile, in der man hinterlegt, wann der Nutzer das letzte Mal erfolgreich abgefragt wurde.
Es ist also nicht notwendig und nicht emfehlenswert jeden Nutzer jeden Tag zu prüfen!

Vorteile:

  • man minimiert die Anzahl an Anfragen (und damit die Last auf z.B. den IdP und den LDAP)
  • man vermeidet DOS-Attacken und eventuelle Sperrung via Fail2Ban
  • man gewährleistet, dass sich weiterhin Nutzer einloggen können

Alternativ besteht vielleicht die Möglichkeit einen separaten IdP aufzusetzen, der ausschließlich für die Abarbeitung von Queries zuständig ist.
Dabei sollte sichergestellt sein, dass alle IdP-Server via Datenbank-Replikation den gleichen Datenbestand aufweisen!!!

Denkbare Schwellwerte für Fail2Ban wären: maximal 50 Queries innerhalb von 10 Sekunden (nicht getestet und abhängig von der Menge der zu prüfenden Nutzer)

  • Zuletzt geändert: vor 3 Monaten