Skip to main content

Berechtigungskonzept

Das jadice web toolkit adaptiert das Berechtigungskonzept der jadice document platform. Berechtigungen können analog der jadice document platform serverseitig auf Dokumente oder Annotationen angebracht werden. Beim Transport der Dokumentdaten und Annotationen auf den Client werden die Berechtigungen mit übertragen, so dass die Tools auf Clientseite entsprechend reagieren können.

Binden von Berechtigungen an Objekte​

Berechtigungen können an folgende Objekte gebunden sein:

  • Document : Die Berechtigungen gelten global für das gesamte Dokument.

    Beispiel: Die Berechtigung DocumentAnnotationPermission.DENY.ADD verbietet dem Benutzer das Anbringen von Annotationen auf dem Dokument.

  • Annotation : Die Berechtigungen gelten nur für einzelne Instanzen von Annotationen.

    Beispiel: Die Berechtigung IndividualAnnotationPermission.DENY.CHANGE verbietet dem Benutzer das Verändern einer einzelnen Annotation.

Besonderheit bei Annotationsberechtigungen: Verbietet eine dokumentbezogene Berechtigung eine Aktion, so wird die individuelle Berechtigung erst gar nicht ausgewertet. Erlaubt hingegen die dokumentbezogene Berechtigung eine Aktion, so wird im Anschluss noch die individuelle Berechtigung ausgewertet. Nur wenn diese nicht gesetzt ist oder die Aktion zulässt, so ist die Aktion auch tatsächlich erlaubt.

Berechtigungen​

Dokumentberechtigungen

  • DocumentPermission.DENY.ADD_PAGES / ALLOW.ADD_PAGES

  • DocumentPermission.DENY.REMOVE_PAGES / ALLOW.REMOVE_PAGES

  • DocumentPermission.DENY.REORDER_PAGES / ALLOW.REORDER_PAGES

Dokumentbezogene Annotationsberechtigungen

  • DocumentAnnotationPermission.DENY.ADD / ALLOW.ADD

  • DocumentAnnotationPermission.DENY.CHANGE / ALLOW.CHANGE

  • DocumentAnnotationPermission.DENY.REMOVE / ALLOW.REMOVE

Individuelle Annotationsberechtigungen

  • IndividualAnnotationPermission.DENY.CHANGE / ALLOW.CHANGE

  • IndividualAnnotationPermission.DENY.REMOVE / ALLOW.REMOVE

  • IndividualAnnotationPermission.DENY.SHOW / ALLOW.SHOW

  • IndividualAnnotationPermission.DENY.WRITE / ALLOW.WRITE

Typische Szenarien​

DENY.REORDER_PAGES

Nach Setzen der Berechtigung DocumentPermission.DENY.REORDER_PAGES können Seiten mittels des ThumbnailTools nicht mehr umsortiert werden.

Annotation DENY.CHANGE

Nach Setzen der Berechtigung IndividualAnnotationPermission.DENY.CHANGE oder DocumentAnnotationPermission.DENY.CHANGE können Annotationen nicht mehr verschoben werden. Die Wrangler zum Größer- und Kleinerziehen werden nicht mehr angezeigt und die Annotations-Toolbar bietet keine Annotationseditoren zum Ändern der Annotation (z.B. Farben, Textgröße) mehr an.

Annotation DENY.REMOVE

Nach Setzen der Berechtigung IndividualAnnotationPermission.DENY.REMOVE oder DocumentAnnotationPermission.DENY.REMOVE ist der Löschen-Button in der Annotations-Toolbar deaktiviert, so dass die Annotation nicht mehr gelöscht werden kann.

Annotation DENY.SHOW

Nach Setzen der Berechtigung IndividualAnnotationPermission.DENY.SHOW werden die betroffenen Annotationen nicht mehr angezeigt.

Annotation DENY.ADD

Nach Setzen der Berechtigung DocumentAnnotationPermission.DENY.ADD ist das Erstellen neuer Annotationen nicht mehr zugelassen. Die Buttons zum Erstellen neuer Annotationen in der AnnotationProfileAwareToolbar sind deshalb inaktiv und ausgegraut. Zusätzlich wird die Berechtigung im CreateAnnotationCommand überprüft.

Annotation DENY.CHANGE und DENY.REMOVE zum Schwärzen von Akten

Die Berechtigungen DocumentAnnotationPermission.DENY.CHANGE und DocumentAnnotationPermission.DENY.REMOVE kann man sich dahingehend zunutze machen, dass der darunterliegende Dokumentinhalt vollständig vor nicht berechtigten Benutzern versteckt wird.

Bei dieser Berechtigungskombination wird, abhängig von der AnnotationRenderStrategy siehe Beeinflussung des Annotationsrendering über eine AnnotationRenderStrategy, die Annotation serverseitig auf das Dokument gerendert. Clientseitig werden nur noch die Bounds der Annotation angezeigt. Sensibler Inhalt bleibt also den Benutzern verborgen. Selbst die listige Abfrage der Data-URLs (z.B. über die DevTools des Browsers) verhilft dem Benutzer nicht zum Originaldokument - auch die über diese URLs geladenen Kacheln enthalten bei entsprechend eingeschränkten Berechtigungen bereits die Annotationen, die den Dokumentinhalt maskieren. Es besteht auf diesem Weg also keine Möglichkeit mehr, unautorisierte Daten aus den empfangenen Daten zu sniffen.

Hinweis

Die Annotationen müssen serverseitig im DocumentDataProvider zuerst (also vor dem Dokument) über den Reader geladen werden, damit deren serverseitiges Einbrennen auf dem Dokument sichergestellt ist.

Codebeispiel: Serverseitiges Setzen von Berechtigungen​

Mit folgender Änderung im ClassPathWithAnnoDocumentDataProvider in der Methode read soll demonstriert werden, wie Berechtigungen serverseitig gesetzt werden können.

Bitte beachten: Serverseitig sind die Implementierungen com.levigo.jadice.annotation.auth.DocumentPermission, com.levigo.jadice.annotation.auth.DocumentAnnotationPermission bzw. com.levigo.jadice.annotation.auth.IndividualAnnotationPermission zu verwenden; clientseitig jedoch deren serialisierbare Entsprechungen com.levigo.jadice.annotation.auth.web.DocumentPermission, com.levigo.jadice.annotation.auth.web.DocumentAnnotationPermission bzw. com.levigo.jadice.annotation.auth.web.IndividualAnnotationPermission.

    ...
import com.levigo.jadice.annotation.auth.DocumentAnnotationPermission;
import com.levigo.jadice.annotation.auth.IndividualAnnotationPermission;
...

private void read(Reader reader, final String resource) throws JadiceException, IOException {

// Variante 1 - Setzen der Permissions pro Annotation
DefaultReaderControls controls = new DefaultReaderControls();
final AnnotationCustomizer annotationCustomizer = new AnnotationCustomizer() {

@Override
public void customize(Annotation a) {
a.getPermissions().getPermissions().add(IndividualAnnotationPermission.DENY.REMOVE);
}
};
controls.getSettings(JadiceAnnotationReaderSettings.class).getAnnotationCustomizers().add(annotationCustomizer);
reader.setReaderControls(controls);

// Variante 2 - Dokumentweites Setzen von Permissions
reader.getDocument().getPermissions().getPermissions().add(DocumentAnnotationPermission.DENY.REMOVE);

// Ab hier kommt der bereits bestehende Code
int targetIndex = reader.getTargetIndex();
{
InputStream in = getClass().getResourceAsStream(resource);
if (in == null) {
in = getClass().getClassLoader().getResourceAsStream(resource);
if (in == null)
throw new IOException("Cant find Resource on Classpath: " + resource);
}
reader.read(in);
}
{
String annoResource = resource + ".xmlanno";
InputStream in = getClass().getResourceAsStream(annoResource);
if (in == null) {
in = getClass().getClassLoader().getResourceAsStream(annoResource);
if (in == null)
return;
}

Format oldFormat = reader.getFormat();
JadiceAnnotationFormat format = new JadiceAnnotationFormat();
reader.setFormat(format);
reader.setTargetIndex(targetIndex);
reader.read(in);
reader.setFormat(oldFormat);
}

Variante 1 zeigt das Aufbringen von Berechtigungen per AnnotationCustomizer; und zwar einzeln pro Annotation (DENY.REMOVE im Beispiel). Dadurch lassen sich gezielt für einzelne Annotationen oder Annotationstypen bestimmte Berechtigungen setzen. In Variante 2 wird eine DocumentAnnotationPermission gesetzt (im Beispiel ebenfalls DENY.REMOVE). Diese ist global für das gesamte Dokument gültig, d.h. in Konsequenz sind alle Annotationen auf dem Client nicht mehr löschbar.

Hinweis

Das Setzen der Berechtigungen muss erfolgen, bevor Reader#read aufgerufen wird.

Eigene Berechtigungen (Custom Permissions)​

Neben den vordefinierten Berechtigungen können eigene Berechtigungen für anwendungsspezifische Aktionen definiert werden, z.B. "PDF-Export erlaubt" oder "Lesezeichen anlegen erlaubt". Das jadice web toolkit wertet solche Berechtigungen nicht selbst aus; sie werden aber wie die Standardberechtigungen am Document bzw. an der Annotation gehalten, zum Client transportiert und können dort von eigenem Code (z.B. in einem Command oder in der Angular-UI) abgefragt werden.

Damit eine eigene Berechtigung den Client erreicht, muss sie zusätzlich beim PermissionsMapper registriert werden. Ohne Registrierung schlägt die Serialisierung des Dokuments bzw. der Annotation mit IllegalArgumentException: Permission is not registered! fehl.

Wie Berechtigungen übertragen werden​

Eine Permission ist ein Java-Objekt und wird nicht direkt serialisiert. Stattdessen übersetzt der PermissionsMapper jede Instanz beim Senden in ein JSON-Objekt aus name und vote und beim Empfang wieder zurück:

[
{ "name": "REMOVE_INDIVIDUAL_ANNOTATION", "vote": "DENY" },
{ "name": "PDF_EXPORT", "vote": "GRANT" }
]

Den Mapper gibt es zweimal, mit identischer API:

  • Server: com.levigo.jadice.web.server.messaging.mapper.PermissionsMapper
  • Client (GWT): com.levigo.jadice.web.client.messaging.mapper.PermissionsMapper

Beide sind Singletons (PermissionsMapper.get()) und kennen von Haus aus nur die Standardberechtigungen (siehe com.levigo.jadice.document.auth.web.DefaultPermissionNames). Der name-String ist der einzige Schlüssel, über den sich Server und Client verständigen. Es gibt keinen automatischen Abgleich zwischen beiden Seiten.

Schritt 1: Intent und Permission definieren​

Ein Intent ist ein Marker-Interface für die Aktion, die berechtigt werden soll. Eigene Intents brauchen nur implements com.levigo.jadice.document.Intent.

Für die Permission selbst gibt es zwei Wege:

Weg A: Annotationsbezogene Berechtigung über die bestehenden Klassen

com.levigo.jadice.annotation.auth.web.IndividualAnnotationPermission und com.levigo.jadice.annotation.auth.web.DocumentAnnotationPermission besitzen einen öffentlichen Konstruktor (Class<? extends Intent>, Vote) und bringen equals()/hashCode() bereits mit:

public class CommentAnnotationIntent implements Intent {
}

IndividualAnnotationPermission allowComment =
new IndividualAnnotationPermission(CommentAnnotationIntent.class, Vote.GRANT);
IndividualAnnotationPermission denyComment =
new IndividualAnnotationPermission(CommentAnnotationIntent.class, Vote.DENY);

Weg B: Eigene Permission-Implementierung

Für dokumentweite Berechtigungen gibt es keinen öffentlichen Konstruktor mit frei wählbarem Intent. Hier wird Permission<Document> selbst implementiert. Die Klasse muss sowohl im Server- als auch im GWT-Client-Build kompilierbar sein (also in einem gemeinsam genutzten Modul liegen und Serializable sein), da auf beiden Seiten dieselbe Klasse registriert wird:

import java.io.Serializable;
import com.levigo.jadice.document.Document;
import com.levigo.jadice.document.Intent;
import com.levigo.jadice.document.Permission;

public final class CustomDocumentPermission implements Permission<Document>, Serializable {

public static class Factory {
public final Permission<Document> pdfExport;
public final Permission<Document> addBookmark;

Factory(Vote vote) {
pdfExport = new CustomDocumentPermission(PdfExportIntent.class, vote);
addBookmark = new CustomDocumentPermission(AddBookmarkIntent.class, vote);
}
}

public static final Factory ALLOW = new Factory(Vote.GRANT);
public static final Factory DENY = new Factory(Vote.DENY);

private String intentClassName;
private Vote vote;

private CustomDocumentPermission(Class<? extends Intent> intentClass, Vote vote) {
this.intentClassName = intentClass.getName();
this.vote = vote;
}

@Override
public boolean pertainsTo(Class<? extends Intent> intentClass) {
return intentClass.getName().equals(intentClassName);
}

@Override
public Vote vote(Document domain, Intent intent) {
return vote;
}

@Override
public boolean equals(Object o) {
if (this == o)
return true;
if (o == null || getClass() != o.getClass())
return false;
CustomDocumentPermission that = (CustomDocumentPermission) o;
return intentClassName.equals(that.intentClassName) && vote == that.vote;
}

@Override
public int hashCode() {
return 31 * intentClassName.hashCode() + vote.hashCode();
}
}
Hinweis

equals() und hashCode() müssen implementiert werden. Der PermissionsMapper verwendet die Permission-Instanz als HashMap-Schlüssel. Ohne eigene Implementierung funktioniert das Mapping nur, solange exakt dieselbe Objektinstanz verwendet wird, die auch registriert wurde.

Schritt 2: Beim PermissionsMapper registrieren​

Die Registrierung erfolgt einmalig beim Start, und zwar mit demselben name-String und denselben Vote-Werten auf beiden Seiten. Üblicherweise hält man die Namen in einem gemeinsam genutzten Enum:

public enum CustomPermissionNames {
PDF_EXPORT, ADD_BOOKMARK
}

Server, z.B. in einer Spring-Bean:

import com.levigo.jadice.web.server.messaging.mapper.PermissionsMapper;

@Component
public class CustomPermissionsConfig {

@PostConstruct
public void registerMappings() {
PermissionsMapper mapper = PermissionsMapper.get();
mapper.registerMapping(CustomDocumentPermission.ALLOW.pdfExport, CustomPermissionNames.PDF_EXPORT.name(), Vote.GRANT);
mapper.registerMapping(CustomDocumentPermission.DENY.pdfExport, CustomPermissionNames.PDF_EXPORT.name(), Vote.DENY);
mapper.registerMapping(CustomDocumentPermission.ALLOW.addBookmark, CustomPermissionNames.ADD_BOOKMARK.name(), Vote.GRANT);
mapper.registerMapping(CustomDocumentPermission.DENY.addBookmark, CustomPermissionNames.ADD_BOOKMARK.name(), Vote.DENY);
}
}

Client, z.B. im GWT-EntryPoint:

import com.levigo.jadice.web.client.messaging.mapper.PermissionsMapper;

@Override
public void onModuleLoad() {
PermissionsMapper mapper = PermissionsMapper.get();
mapper.registerMapping(CustomDocumentPermission.ALLOW.pdfExport, CustomPermissionNames.PDF_EXPORT.name(), Vote.GRANT);
mapper.registerMapping(CustomDocumentPermission.DENY.pdfExport, CustomPermissionNames.PDF_EXPORT.name(), Vote.DENY);
mapper.registerMapping(CustomDocumentPermission.ALLOW.addBookmark, CustomPermissionNames.ADD_BOOKMARK.name(), Vote.GRANT);
mapper.registerMapping(CustomDocumentPermission.DENY.addBookmark, CustomPermissionNames.ADD_BOOKMARK.name(), Vote.DENY);
// ... übrige Initialisierung
}

Pro Berechtigung sind zwei Registrierungen nötig, eine für GRANT und eine für DENY. Eine erneute Registrierung desselben Schlüssels überschreibt den vorherigen Eintrag ohne Fehler.

Schritt 3: Berechtigung setzen​

Das Setzen erfolgt serverseitig wie bei den Standardberechtigungen (siehe Codebeispiel oben), also vor Reader#read:

// Dokumentweit
reader.getDocument().getPermissions().getPermissions().add(CustomDocumentPermission.DENY.pdfExport);

// Pro Annotation über einen AnnotationCustomizer
a.getPermissions().getPermissions().add(denyComment);

Beim Senden an den Client übersetzt der Mapper die Instanz automatisch in { "name": "PDF_EXPORT", "vote": "DENY" }; beim Empfang einer vom Client gesendeten Änderung wird sie wieder in die registrierte Instanz zurückübersetzt.

Clientseitige Auswertung​

Das jadice web toolkit kennt die Bedeutung eigener Berechtigungen nicht. Die Auswertung, etwa das Deaktivieren eines Export-Buttons, muss in eigenem Client-Code erfolgen, z.B. in einem eigenen Command über document.getPermissions().

Bei Verwendung des Angular-Clients kommen die Berechtigungen als { name, vote }-Objekte an. Das TypeScript-Enum PermissionName in permissions.interface.ts ist manuell gepflegt und enthält nur die Standardnamen. Eigene Namen können entweder als String verglichen oder in einem eigenen Enum geführt werden. Die eingebauten Hilfsfunktionen (mayAdd, mayChange, mayRemove, mayShow) werten ausschließlich die Standardberechtigungen aus.

Fallstricke​

  • Nur auf einer Seite registriert: Serverseitig fehlende Registrierung führt beim Laden des Dokuments zu IllegalArgumentException: Permission is not registered!. Clientseitig fehlende Registrierung führt zur selben Exception beim Deserialisieren; über PermissionsMapper.get().setThrowExceptionOnMissingMapping(false) kann der Client unbekannte Berechtigungen stattdessen ignorieren.
  • Abweichender name-String: Server und Client müssen exakt denselben String verwenden. Tippfehler fallen erst zur Laufzeit auf.
  • Fehlendes equals()/hashCode(): siehe Hinweis oben.
  • Standard-Intents in eigenen Permissions: Für die Standard-Intents (AddPagesIntent, RemovePagesIntent, ReorderIntent, ChangeAnnotationIntent, RemoveAnnotationIntent, ShowAnnotationIntent, WriteAnnotationIntent) greift serverseitig ein Fallback über pertainsTo(), so dass auch nicht registrierte Instanzen auf die Standardnamen abgebildet werden. Für alle anderen Intents gibt es diesen Fallback nicht.