Web API Checklist

Es ist mit ein paar Herausforderungen verbunden, wenn die API, die vorher nur aus dem gesicherten Intranet erreichbar war, plötzlich über das internet verfügbar gemacht wird. Die anzahl der potenziellen Benutzer schießt in die Höhe und damit auch die Anzahl der Menschen, die Unsinn damit treiben mögen.

Jeder hat eine öffentliche Web-API!

“Meine API wird ja nur von meiner App aufgerufen." Habe ich tatsächlich schon mehrfach gehört. Aber wenn die App von öffentlichen Rechnern funktionieren soll, muss die API öffentlich erreichbar sein.

Die Checkliste

Die Liste erhebt keinen Anspruch auf Vollständigkeit. Es sind die Punkte, die mir als erstes einfallen, weil sie in meinen Projekten häufig auftauchen.

Zugriffsrechte

  • Zugriffsrechte festlegen und testen. Das klingt ersteinmal ziemlich naheliegend. Aber es reicht jetzt nicht mehr, ob ein Button im Frontend nicht mehr auftaucht. In jedem Request muss deshalb die BenutzerId und eine Liste der Gruppen mitgeschickt werden. Das lässt sich mit einem AWT relativ einfach abbilden. Das Backend muss jetzt überprüfen, ob dieser Benutzer berechtigt ist etwas mit diesen Daten zu machen. Wir möchten nicht, dass ein Benutzer René die Daten von Klaus ändern kann, nur weil beide in der Gruppe Benutzer sind. Diese Dinge müssen getestet werden. Automatisiert. Immer. Bei jedem Build.

    Ich würde zusätzlich die Gruppenrechte noch einmal im Reverse Proxy überprüfen und um Zweifelsfall gar nicht erst an das Backend weiterleiten. Es gibt dafür schöne Plugins für zb Nginx.

  • Security Provider nicht selber Programmieren. So wie man keine Datenbanken selber entwickelt, sollte man auch IDM und Logins nicht selber implementieren. Es gibt dazu gute, geprüfte Implementationen wie zB Keycloak. AM und IDM sind komplexe Themen. Wenn es nicht der Kern Businesscase von deinem Unternehmen ist, lass es einfach.

  • Keine fortlaufenden IDs verwenden. Wenn tatsächlich einmal etwas schief geht (und das wird es), ist es hilfreich, wenn nicht jeder Idiot sofort mit einem 2 Zeilen Script alle Daten abgreifen kann. Wenn ich mit einfachem Hochzählen der Id immer einen Treffer lande, wird es auch schwierig, die Ip des bots als illegalen Zugreifer zu identifizieren und länger zu sperren. (s.u.)

    Das geht zB mit UUIDs aber auch mit Pseudo Encrypt oder XTEA.

  • 404 statt 403. Wenn René die Daten von Klaus anfragt, sollte er ein Nicht Gefunden (404) als Antwort erhalten und nicht ein Keine Berechtigung (403). Wir müssen dem Hacker René ja nicht unbedingt mitteilen, dass er mit der Id einen Treffer gelandet hat. Wenn ich Daten bereits sehen kann, weil ich vielleicht Leserechte habe, dann antworten wir natürlich mit 403. Sonst dreht der Support durch.

Richtigkeit der Daten

  • Listen Anfragen beschränken Niemand, wirklich niemand sollte mit einem API Call die ganze Datenbank auslesen dürfen. Sowas wie Gib mir alle Artikel mit einem Limit von 5 Millionen macht keinen Sinn. Das maximal zulässige Limit sollte so klein wie möglich gewählt werden.

  • Eingabedaten beschränken. Die maximale Größe einen Post Objekts sollte eingeschränkt sein. Die Überprüfung sollte so weit vorne in der Kette wie möglich erfolgen - sollte also im Reverse Proxy eingestellt werden.

  • Eingabedaten validieren Praktisch jede Programmiersprache hat eine einfach zu verwendende Bibliothek zur Datenvalidierung. (zB Go Validator) Benutzt sie. Idealerweise kommt eine unsinnige Anfrage nicht einmal bis zur Business Logik - geschweige denn bis zur Datenbank. Kann man mit Fuzzing überprüfen.

Lastbegrenzung

  • Aufruffrequenz beschränken. Selbst wenn wir einen sehr ungeduldigen Kunden haben, der wirklich sehr schnell klicken kann, gibt es vermutlich ein sinnvolles Limit, das eine regulär laufende Anwendung nicht überschreiten wird. Rate Limiter sind so üblich, dass man sie meist nicht selbst implementieren muss. Hier ein Beispiel für Nginx.

  • Fehlerhafte anfragen blocken. Das ist im prinzip ähnlich wie der vorherige Punkt, nur dass hier bei häufigen Anfragen, die mit Fehlern beantwortet werden, die anfragende Ip länger gesperrt wird. Das geht zB mit dem Klassiker fail2ban




Transaktionklammer in Go

Mit Closures kann man ziemlich lustige Sachen machen in Go. Beispielsweise transaktionalen Code klammern:

Eine nichtsnutzige Transaktion

Um das Beispiel einfach zu halten, bauen wir uns einen Transaktionstyp der ein bisschen so aussieht, wie der echte aus der Db lib.

 9
10
11
12
13
14
15
16
17
18
19
20
type transaction struct{}

func (t *transaction) exec(s string) error {
	fmt.Println("exec: ", s)
	return nil
}
func (t *transaction) commit() {
	fmt.Println("commit")
}
func (t *transaction) rollback() {
	fmt.Println("rollback")
}

So weit so unspektakulär:

Ich brauche eine Transaktion, damit ich etwas machen kann (exec) und wenn ich mit allem fertig bin, möchte ich die Transaktion wieder schließen.

Ist etwas schief gegangen, soll ein rollback erfolgen, so dass keine Änderungen in der Datenbank landen, ist alles ok, wird die Transaktion mit einem commit abgeschlossen.

Die Transaktionsklammer

Dafür könnte man eine Wrapper Funktion bauen:

23
24
25
26
27
28
29
30
31
32
33
34
func withTx(f func(*transaction) error) {
	tx := &transaction{}
	var err error
	defer func() {
		if err == nil {
			tx.commit()
		} else {
			tx.rollback()
		}
	}()
	err = f(tx)
}
  • 24: erstellt die Transaktion
  • 25: definiert die Variable, die den Fehlerstatus halten soll. Da sie in der Closure von defer verwendet wird, muss sie bekannt sein, bevor sie referenziert wird.
  • 26-32: je nachdem ob err gesetzt ist, wird die Transaktion mit rollback oder commit finalisiert. Da es im defer ist, passiert das auch, wenn irgendwer mit einer panic aussteigt.
  • 33: ruft die Funktion auf und übergeben die Transaktion

Die Funktion withTx könnte irgendwo in einem DB Paket liegen.

Anwendung

Wie verwende ich das ganze nun?

Ich klammere einfach meinen transaktionalen Code mit der withTx Funktion und einer Closure:

36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
func main() {

	s1 := "first"
	s2 := "second"

	withTx(func(t *transaction) error {
		err := t.exec(s1)
		return err
	})

	withTx(func(t *transaction) error {
		err := t.exec(s2)
		err = errors.New("fail") // force fail
		return err
	})
}

Die Transaktion bekomme ich übergeben und ich muss lediglich err zurückgeben. Ich finde, das ist lesbarer, als wenn man den Code zum erzeugen und schließen der Transaktion n-fach kopiert.

In Playground ausprobieren