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.ADDverbietet 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.CHANGEverbietet 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_PAGESkönnen Seiten mittels des ThumbnailTools nicht mehr umsortiert werden.
Annotation DENY.CHANGE
Nach Setzen der Berechtigung
IndividualAnnotationPermission.DENY.CHANGEoderDocumentAnnotationPermission.DENY.CHANGEkö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.REMOVEoderDocumentAnnotationPermission.DENY.REMOVEist 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.SHOWwerden die betroffenen Annotationen nicht mehr angezeigt.
Annotation DENY.ADD
Nach Setzen der Berechtigung
DocumentAnnotationPermission.DENY.ADDist 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.CHANGEundDocumentAnnotationPermission.DENY.REMOVEkann 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.
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.
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();
}
}
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; überPermissionsMapper.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 überpertainsTo(), so dass auch nicht registrierte Instanzen auf die Standardnamen abgebildet werden. Für alle anderen Intents gibt es diesen Fallback nicht.