MonitoringHive: Wer welche Gebäudedaten sehen darf
Mehrere Parteien teilen sich eine Plattform zur Gebäudeüberwachung. Jede darf nur sehen, wozu sie berechtigt ist, und jeder Entscheid muss sich nachträglich erklären lassen.
- Status
- In Entwicklung. Das hier beschriebene Berechtigungsmodell ist umgesetzt; die Lücken stehen am Schluss.
- Meine Rolle
- Architektur und Umsetzung. MonitoringHive ist mein eigenes Projekt.
- Umfang
- Autorisierung im Backend: der Policy-Evaluator, wie die Services ihn durchsetzen und wie Entscheide protokolliert werden
- Zuletzt geprüft
- 28. September 2026
MonitoringHive verbindet Gebäudetechnik (Lüftung, Zähler, Beleuchtung) mit einem zentralen Dashboard. Eine kleine Box im Gebäude sendet Daten nur nach aussen; vor Ort sind keine Ports offen. Dieselben Daten nutzen mehrere Parteien: die Betreiber der Gebäude, Servicepartner, die die Plattform unter eigener Marke anbieten, Gerätehersteller und ich als Plattformbetreiber.
Der Ausgangspunkt
Der erste Prototyp prüfte device.read, ohne zu prüfen, auf welches Gerät die Person zugreifen darf. Die Mandantentrennung hing davon ab, dass jede Abfrage ihren Filter mitbringt.
Die Regeln bleiben auf dem Server
Die Boxen in den Gebäuden erhalten nie eine Kopie der Regeln. Sie prüfen nur signierte Befehle, und genau eine Komponente signiert sie. Der Preis: Jede Prüfung braucht das Backend. Dafür gilt ein entzogenes Recht sofort überall und nicht erst, wenn sich die jeweilige Box das nächste Mal synchronisiert.
Entscheide
Zentrale Berechtigungsprüfung. Jede fachliche Autorisierung läuft über eine einzige Schnittstelle, evaluate(context, action, resource), mit genau einer Implementierung im Auth-Service. Die Schnittstelle liegt in einer Bibliothek ohne Framework-Abhängigkeiten, und ein Test lässt den Build scheitern, sobald Spring oder JPA hineingeraten.
Standardmässig ablehnen, mit Begründung. Der Evaluator prüft in fester Reihenfolge: Ist der Kontext vollständig, ist die Person aktiv, ist die Berechtigung bekannt, gehört die Ressource zum angefragten Mandanten, dann Rollenrechte, dann eine Freigabe auf genau dieser Ebene, dann eine Freigabe an eine Gruppe, zu der die Person gehört. Was durchfällt, wird abgelehnt, mit einem Grund aus einer festen Liste. Ein Kunde kann nie ausserhalb seines eigenen Mandanten handeln:
// Step 4 — tenant/resource relationship.
String resourceTenantId = tenantIdOf(resource);
if (resourceTenantId != null && !resourceTenantId.equals(ctx.targetTenantId())) {
return deny(DenyReason.DENY_RESOURCE_TENANT_MISMATCH, ctx);
}
if (ctx.actorClass() == ActorClass.CUSTOMER
&& !ctx.actorHomeTenantId().equals(ctx.targetTenantId())) {
return deny(DenyReason.DENY_TENANT_MISMATCH, ctx);
}
Unverändert aus AccessPolicyEvaluatorImpl.java, Zeilen 109–117. Code-Stand 17. Juli 2026.
Datenbankfilter und Architekturtests. PostgreSQL beschränkt die mandantenbezogenen Tabellen mit Row-Level Security auf den aktuellen Mandanten. Auch eine Abfrage ohne Mandantenfilter liefert deshalb keine Datensätze eines anderen Mandanten. Und weil die Berechtigungsprüfung der Services Handler ohne Annotation durchlässt, schlägt ein Architekturtest fehl, sobald ein öffentlicher Endpunkt weder geschützt noch ausdrücklich freigegeben ist. Ein zweiter schlägt fehl, wenn Code ausserhalb der RLS-Schicht die Mandanteneinstellung berührt. Scheitert ein Schritt beim Nachschlagen der Rechte, antwortet der Service mit 403.
Nachweis
Jede Prüfung schreibt genau eine Zeile ins Entscheid-Log und löst genau ein Audit-Ereignis aus. Der Log-Eintrag verwendet eine eigene Transaktion und bleibt deshalb erhalten, wenn die ursprüngliche Operation zurückgerollt wird. Er enthält den Ablehnungsgrund oder die Freigabe, die den Zugriff erlaubt hat.
Änderungen an Berechtigungen lassen sich zuerst als reiner Probelauf ausführen, der zeigt, wer vorher und nachher was sieht. Angewendete Änderungen werden separat protokolliert.
Zu den Tests gehören:
- Mandant A kann die Benutzer von Mandant B nicht lesen, durchgesetzt von der Datenbank
- eine Gruppenfreigabe gilt nur für genau das Gerät, für das sie erteilt wurde
- wer aus einer Gruppe entfernt wird, verliert die geerbte Freigabe
- eine Kunden-Administratorin kann keine Rolle vergeben, die mehr Rechte hat als ihre eigene
- eine Prüfung erzeugt genau eine Log-Zeile und ein Ereignis
Test-Referenzen
RlsIsolationTest.tenant_a_cannot_read_tenant_b_usersGroupGrantEvaluationIntegrationTest.the_group_grant_applies_only_at_its_exact_scopeGroupGrantEvaluationIntegrationTest.a_removed_member_loses_the_inherited_grantUserRbacIntegrationTest.customer_admin_cannot_grant_a_role_exceeding_its_own_permissionsPolicyDecisionLogIntegrationTest.one_evaluator_call_produces_one_log_row_and_one_event
@Test
void the_group_grant_applies_only_at_its_exact_scope() {
String admin = actor("admin");
String member = actor("member");
String groupId = teamWithDeviceGrant(admin, DEVICE_ID);
groupService.addMember(TENANT, groupId, member, admin, NOW);
// Same member + permission, but a DIFFERENT device the team was not granted → DENY.
PolicyDecision d = evaluateDeviceRead(member, OTHER_DEVICE_ID);
assertThat(d.deny()).isTrue();
assertThat(d.denyReason()).isEqualTo(DenyReason.DENY_NO_GRANT);
}
Unverändert aus GroupGrantEvaluationIntegrationTest.java. Code-Stand 17. Juli 2026.
Noch offen
- Einwilligung für mandantenübergreifenden Zugriff. Für Partner und Hersteller gibt es noch keinen Einwilligungsschritt.
- Notfallzugriff (Break-Glass). Heute wird jede solche Anfrage abgelehnt.
- Attributbasierte Bedingungen und Delegation.
- Freigaben, die vom Mandanten auf seine Standorte und Geräte vererbt werden. Heute gilt eine Freigabe auf genau einer Ebene.
- Den Evaluator in einen eigenen Service auslagern. Vorerst läuft er im Auth-Service.
Technik: Java 21, Spring Boot mit Spring Modulith, PostgreSQL, Keycloak, ArchUnit und Testcontainers.