Sergii Stotskyi im Interview

„CASL ist eine isomorphe JavaScript-Bibliothek für die Permission-Verwaltung: Vielseitig, deklarativ, typensicher und klein“

Ann-Cathrin Klose

© Shutterstock / vladwel

CASL ist eine Library, die das Management von Permissions erleichtert. Im Interview haben wir mit dem CASL-Entwickler Sergii Stotskyi über die Library gesprochen. Wann braucht man CASL, was hat sich zu Version 4.0 alles geändert und wo liegen typische Stolpersteine?

JAXenter: Hallo Sergii, du bist ja der Entwickler von CASL und darüber wollen wir uns jetzt ein wenig unterhalten. Fangen wir doch mal ganz grundlegend an: Was ist CASL überhaupt?

Sergii Stotskyi: CASL ist eine isomorphe JavaScript-Bibliothek für die Permission-Verwaltung. Das fancy Wort “isomorph” bedeutet, dass man die Bibliothek sowohl im Frontend als auch im Backend auf genau die gleiche Weise verwenden kann.

Was kann ich noch zu CASL sagen?

CASL ist vielseitig, man kann mit einer einfachen Claim-basierten Zugriffskontrolle beginnen und seine Lösung zu einer vollumfänglichen Variante auf Basis von Attributen skalieren.

CASL ist deklarativ. Es erlaubt die Definition von Permissions in-memoy mit einer domain-spezifischen Sprache, die fast Wort für Wort den geschäftlichen Anforderungen entspricht.

CASL ist typensicher. Es ist in TypeScript geschrieben, was Apps sicherer und die Entwicklererfahrung angenehmer macht.

CASL ist klein – es hat eine Größe von nur ~4,5kb mingzipped und kann sogar noch kleiner werden, dank Treeshaking. Die minimale Größe ist ~1,5kb.

JAXenter: Für welche Art von Projekt sollte CASL zum Einsatz kommen?

Stotskyi: Immer, wenn zu den Requirements einer Anwendung gehört, dass eine Zugriffskontrolle implementiert werden muss. CASL implementiert im Kern ABAC (Attribute Based Access Control), kann aber auch erfolgreich genutzt werden, um RBAC (Role Based Access Control) zu implementieren, und funktioniert sogar für Claim-basierte Versionen.

CASL kann außerdem mit Datenbanken integriert werden, sodass man es zur Abfrage von Dokumenten verwenden kann, auf die ein Zugriff möglich ist. Derzeit werden MongoDB und Mongoose unterstützt. Die Implementierung eines SQL-Supports ist für die nächste Zeit geplant. Soweit ich weiß, gibt es auch erfolgreiche Integrationen von CASL mit Objection.js, Sequelize und GraphQL.

JAXenter: Du hast CASL 4.0 ja in TypeScript neu geschrieben. Das wird im JavaScript-Ökosystem häufig gemacht. Warum hast du dich für diesen Schritt entschieden?

Stotskyi: TypeScript ist eins der heißesten Themen der letzten Jahre in der JavaScript-Community. Ich habe die Erfahrung gemacht, dass Enterprise-Anwendungen meistens in statisch typisierten Sprachen geschrieben sind. Dadurch kann man sicherstellen, dass der geschriebene Code auf der Build-Stufe dann auch valide ist, indem man die Types prüft. Außerdem bieten moderne IDEs quasi sofort Hinweise an, sodass Entwickler Fehler sogar schon vor der Build-Phase entdecken können. Dadurch steigt das Vertrauen in die Anwendung, die so entsteht. Ich möchte, dass Anwender von CASL sich darauf verlassen können, dass ihre App sicher ist.

CASL hat TypeScript schon seit den frühen Versionen unterstützt, allerdings handgeschriebene Declaration-Dateien verwendet. Es war mühsam, die zu aktualisieren und ich habe das immer wieder vergessen, wenn ich ein neues Feature veröffentlicht habe.

CASL 4.0 ist komplett in TypeScript neu geschrieben. Jetzt kann ich mir sicher sein, dass die Types mit den neusten Features übereinstimmen. Außerdem sind die neuen Types fortgeschrittener und hilfreicher im Vergleich zu den selbst geschriebenen. Dadurch können IDEs Hinweise ausgeben, welche Actions oder Subjects man nutzen kann oder welche MongoDB-Operatoren in Conditions verwendet werden können. So wird verhindert, dass an diesen Stellen Typos entstehen.

JAXenter: Welche anderen wichtigen Neuerungen bringt CASL 4.0 mit?

Stotskyi: Die wichtigsten Ziele für Version 4.0 waren:

  • umfassender TypeScript-Support
  • bessere Dokumentation
  • besserer Support für Tree-Shaking

Der Support für TypeScript ist viel besser geworden! In 4.0 akzeptiert die Ability-Klasse zwei optionale generische Parameter. Der erste davon beschränkt, welche Aktionen mit welchen Subjects möglich sind; der zweite definiert die Form des Conditions-Objekts. Standardmäßig verwendet die Ability-Klasse MongoDB-Conditions, sodass man nur einen Parameter spezifizieren muss – die Application Abilities.

Zum Beispiel: In einer Blog-App haben wir Article, Comment und User, auf denen wir CRUD-Operations ausführen können:

 
import { Ability } from '@casl/ability';

type AppAbilities = [
  'read' | 'update' | 'delete' | 'create',
  'Article' | 'Comment' | 'User'
];
const ability = new Ability<AppAbilities>();

ability.can('raed', 'Post'); // typo is intentional

Wenn man die Abilities prüft, wird die IDE nun Vorschläge machen, welche Optionen zur Verfügung stehen und TypeScript stellt sicher, dass man keinen Tippfehler eingebaut hat! Das Beispiel oben wird also nicht kompiliert und gibt einen Fehler aus, dass es keine Action namens raed gibt. Das ist noch nicht alles. Man kann seinen Code noch strikter machen, indem man die möglichen Kombinationen aus Action und Subject definiert. Weitere Informationen dazu gibt es in der Dokumentation.

33 Prozent der Issues im CASL Repository sind Fragen. Daraus ließ sich ableiten, dass die Dokumentation verbessert werden muss. Die Docs-App wurde von Grund auf neu geschrieben und verwendet jetzt rollup und lit-element, aber das ist eine andere Geschichte. Die Dokumentation von CASL ist jetzt anfängerfreundlicher und hat eine Cookbook-Sektion, die Empfehlungen gibt, wann, wie und warum man die Permissions-Logik seiner Anwendung ansehen sollte.

Es ist mein Ziel, CASL sehr umfangreich zu machen und zur gleichen Zeit einen minimalen Einfluss auf die entstehende Bundle-Größe zu nehmen. Das ist für Frontend-Anwendungen wichtig. Darum ist 4.0 kleiner und hat besseren Support für Tree-Shaking. Um das zu erreichen, musste ich einige Breaking Changes vornehmen:

  • AbilityBuilder.extract: die Methode wurde durch ihren Constructor ersetzt
  • AbilityBuilder.define wurde ersetzt durch die Funktion defineAbility. Normalerweise wird die Funktion nicht in Anwendungen genutzt, sodass sie dank Tree-Shaking nun entfernt werden kann.
  • Ability.addAlias wurde ersetzt durch createAliasResolver dessen Verwendung klarer ist. Vorher wurde folgender Code genutzt:
 
import { Ability } from '@casl/ability';

  Ability.addAlias('modify', ['create', 'update']);
  const ability = new Ability();

  ability.can('modify', 'Post');
  ```

Jetzt können wir folgendes schreiben:

  import { Ability, createAliasResolver } from '@casl/ability';

  const resolveAction = createAliasResolver({ modify: ['create', 'update'] });
  const ability = new Ability([], { resolveAction });

  ability.can('modify', 'Post');
  • da die Aliasing-Funktionalität refactored wurde und jetzt auch Tree-shakable ist, wurde der Standard-Alias crud entfernt und muss nun manuell definiert werden.
  • Kleinere Breaking Changes in den ergänzenden Packages. Mit „kleiner“ meine ich Änderungen, die die Type-Änderungen in @casl/ability und das Entfernen der Standard-Instantiierung der Ability-Instanz in allen Packages widerspiegeln.

Wie immer können die Breaking Changes und der Migration Guide im Changelog.md der jeweiligen Packages gefunden werden.

Eines der mächtigsten neuen Features, die hinzugekommen sind, ist die Möglichkeit der Individualisierung von Ability. Ab 4.0 ist es möglich, eigene Conditions Matcher zu implementieren. Statt der MongoDB Abfragesprache können wir also normale Funktionen oder JSON Schema verwenden. Wir können sogar jede Object Validation Library verwenden (z. B. joi). Grenzen setzen uns nur unsere Vorstellungskraft und das TypeScript-Interface 😉 Mehr Details dazu können in der Dokumentation gefunden werden.

JAXenter: CASL bietet individuelle Packages für Frameworks wie Vue.js oder Angular an. Befinden sich alle davon auf dem gleichen Feature-Level oder gibt es Unterschiede, die man bei der Verwendung beachten muss?

Stotskyi: Ich bemühe mich darum, die ergänzen Packages auf dem gleichen Stand wie die das Framework zu halten. Persönlich habe ich nur mit Vue und Angular Erfahrungen im kommerziellen Einsatz gesammelt, aber ich habe viel über React und Aurelia gelesen. Ich helfe in meiner Freizeit auch Freunden bei ihren Projekten. Das erlaubt es mir, die ergänzenden Packages in Hinblick auf die Entwickler-Erfahrung (DX) zu testen.

Die Packages für Vue und React umfassen eine <can>-Komponente, mit der die Sichtbarkeit von UI-Elementen auf Basis der Zustimmung des Nutzers ein- und ausgeschaltet werden kann. Das funktioniert in den meisten Fällen gut, aber manchmal muss man imperativen Code schreiben. Das war in Vue-Apps bisher einfacher, weil wir einfach eine $can-Methode schreiben konnten:

 
export default {
  methods: {
    createPost() {
      if (!this.$can('create', 'Post')) {
        alert('You are not allowed to create posts');
        return;
      }
      // implementation
    }
  }
}

Da React aber ja jetzt Hooks hat, und dank des Beitrags von David Acevedo, haben wir kürzlich den useAbility Hook in @casl/react veröffentlicht, der die imperative Verwendung von CASL in React-Anwendungen vereinfacht. Der Hook erlaubt uns, die Ability-Instanz zu verwenden und die entsprechende React-Komponente zu aktualisieren, wenn die Regeln von Ability aktualisiert werden.

 
import { useAbility } from '@casl/react';
import { AbilityContext } from './Can';

export default () => {
  const ability = useAbility(AbilityContext);
  const createPost = () => { /* implementation */ };

  return ability.can('create', 'Post')
    ? <button onClick={createPost}>Create Post</button>
    : null;
};

Die Anfrage, @casl/angular auf Angular 9.0 (veröffentlicht am 6. Februar) zu aktualisieren, wurde am 13. Februar erstellt. Noch an selben Tag wurde das Request implementiert und geschlossen. Es gibt jedoch eine Sache, die mich an der Angular-Integration stört. Sie wurde mit Impure Pipes geschrieben. Impure Pipes könnten irgendwann zu einem Performance-Bottleneck werden. Das Request, pure Pipes zu erlauben, auf asynchrone Quellen zu subscriben und sich selbst zu aktualisieren, wurde am 9. März 2017 erstellt, ist aber bis heute, am 20. April 2020 nicht implementiert worden. Das ist schade. Darum habe ich eine Issue erstellt um Support für das can Structural Directive für @casl/angular hinzuzufügen. Dieses Directive funktioniert genau wie Pipes, aber der Change Detection Cycle hat eine bessere Performance.

@casl/aurelia ist das vermutlich das am wenigsten genutzte ergänzende Package, vermutlich weil die Aurelia-Community an sich vergleichsweise klein ist. Soweit ich mich erinnern kann, gab es in den letzten drei Jahren kein einziges Request wegen Fehlern, die behoben werden müssen oder weil etwas im Code mit Bezug zu Aurelia aktualisiert werden sollte. Aurelia ist Angular aber sehr ähnlich, darum ist auch der Aurelia-Support dem von Angular sehr ähnlich. @casl/aurelia stellt einen Value Converter zur Verfügung (analog zu Angulars Pipe). Zumindest hat Aurelia aber nicht das Performance-Problem, das aus Angulars Impure Pipes entsteht. 🙂

JAXenter: Hast du einen praktischen Tipp für Leute, die in die Arbeit mit CASL einsteigen?

Stotskyi: Nach alldem fragen sich vermutlich einige, wo man mehr Informationen bekommt. Ich würde dazu raten, mit dem CASL Guide zu beginnen. Dann sollte man etwas über das ergänzende Package für das Framework der Wahl lesen. Wenn man dann noch Beispiele für Integrationen mit den beliebtesten Frameworks sucht (Frontend und Backend), sollte man einen Blick in mein Medium-Blog und das CASL Examples Monorepo werfen (noch in Arbeit).

Abschließend möchte ich noch über ein paar typische Stolpersteine sprechen:

Wenn man mit CASL arbeitet, sollte man darüber nachdenken, was der Nutzer in der Anwendung tun kann, nicht darüber, wer er ist oder welche Rolle er hat. Rollen können leicht auf Gruppen von Actions abgebildet werden. Diese Art von Umweg hilft dabei, neue Rollen kinderleicht in eine Anwendung einfügen zu können.

Es gibt eine Falle, in die Entwickler sehr häufig tappen:

Ability und AbilityBuilder haben Methoden, die gleich benannt sind (can und cannot), aber es ist wichtig zu verstehen, dass sie etwas anderes bedeuten und unterschiedliche Funktionen haben. Wenn wir unsere Permissions so definieren:

 
import { defineAbility } from '@casl/ability';

const ability = defineAbility((can) => {
  can('read', 'Article', { userId: 1 });
});

…können wir das nicht auf genau die gleiche Weise prüfen, also ist der folgende Code falsch:

ability.can('read', 'Article', { userId: 1 });

Auf den ersten Blick sieht das unsinnig aus. Es wirkt logisch, dass man Permissions auf die gleiche Weise definieren und checken können sollte. Aber es gibt zwei Gründe dafür, dass das so nicht implementiert ist:

1. Das Conditions Object (3. Argument von AbilityBuilder.can) ist kein reines Objekt, sondern kann ein Subset einer MongoDB Query enthalten. Was erwarten wir also von diesem Permissions-Check?

 
  import { defineAbility } from '@casl/ability';

  const ability = defineAbility((can) => {
    can('read', 'Article', {
      userId: { $eq: 1 },
      createdAt: { $lte: Date.now() }
    });
  });

2. Ich bevorzuge es, wenn Objekte Informationen darüber enthalten, was sie sind, sodass man unterscheiden kann, ob ein Objekt ein Artikel, eine Seite oder ein Kommentar ist. Dieses Problem könnte komplexer werden, wenn man eine Reihe von Objekten hat, deren Form sich überschneidet. Selbst TypeScript hilft da nicht!

 
  interface Comment {
    body: string
    authorId: number
  }

  interface Article {
    title: string
    body: string
    authorId: number
  }

  const article: Article = {
    "title": "CASL",
    "body": "...",
    "authorId": 1
  };
  const comment: Comment = article; // bug here! No error from TypeScript. Did you know that?
  console.log(comment);

Das ist bei Klassen übrigens nicht der Fall. Eine Instanz einer Klasse umfasst immer eine Referenz auf den Constructor, der verwendet wurde, um sie zu erstellen. Dadurch kann der Type leicht aus einer Instanz gelesen werden.

Das sind wichtige Gründe dafür, dass die Prüfung von Permissions in CASL so aussieht (als korrekte Version des Beispiels oben):

 
import { subject as an } from '@casl/ability';

ability.can('read', an('Article', { userId: 1 }));

Aber keine Sorge, CASL gibt keinen Laufzeitfehler aus, wenn es einen Versuch der inkorrekten Benutzung bemerkt. Wer mehr über die eingebaute Subject-Type-Detection-Logik wissen möchte, kann etwas darüber in der Dokumentation lesen.

Ich hoffe, euch hat die Lektüre Spaß gemacht und ihr werdet CASL in eurem nächsten Projekt benutzen!

JAXenter: Vielen Dank für das Interview!

Sergii Stotskyi is a technical lead with more than 11 years of experience in web development, 5 years of which he managed teams of 3-7 people. He likes Open Source, reading books and skating. Hates laziness 🙂
Geschrieben von
Ann-Cathrin Klose
Ann-Cathrin Klose
Ann-Cathrin Klose hat allgemeine Sprachwissenschaft, Geschichte und Philosophie an der Johannes Gutenberg-Universität Mainz studiert. Bereits seit Februar 2015 arbeitete sie als redaktionelle Assistentin bei Software & Support Media und ist seit Oktober 2017 Redakteurin. Zuvor war sie als freie Autorin tätig, ihre ersten redaktionellen Erfahrungen hat sie bei einer Tageszeitung gesammelt.
Kommentare

Hinterlasse einen Kommentar

avatar
4000
  Subscribe  
Benachrichtige mich zu: