Zum Inhalt springen

Die Admin-API

Die gRPC-Admin-API, die Freigaben im laufenden Betrieb anlegt, ändert, deaktiviert und Berechtigungen vergibt, wo sie lauscht und was sie ablehnt.

Seit v0.8.0 können sich die Freigaben ändern, während der Server läuft. Ein admin-Block schaltet einen gRPC-Dienst ein, der Freigaben ohne Neustart und ohne Bearbeiten einer Datei anlegt, ändert und offline nimmt.

admin {
  listen       = "unix:///run/fileshare/admin.sock"   # made 0600
  state_file   = "/var/lib/fileshare/shares.json"
  source_roots = ["/srv/images", "/data"]
}

Nichts lauscht, solange der Block nicht geschrieben ist.

Was sie tut

Der Dienst ist fileshare.admin.v1.AdminService:

CreateShare, UpdateShare, DeleteShareeine Freigabe aus einem Image, einem Verzeichnis oder (seit v0.21.0) einem Volume definieren; read_only oder protocols ändern (ihre Quelle kann sich nicht ändern: löschen Sie sie und legen Sie eine neue an)
DisableShare, EnableShareeine Freigabe offline nehmen und wieder online bringen
Grant, Revokeeinem Subjekt Lese- oder Schreibzugriff geben oder ihn entziehen
ListShares, GetSharejede Freigabe, mit dem, was sie bereitstellt, über welche Protokolle, und warum nicht über die anderen
ListUsers, ListGroupswen eine Berechtigung nennen kann; jeder Benutzer mit den Protokollen, die seine Zugangsdaten beantworten können
ReloadDirectorydie Personen jetzt erneut einlesen – siehe das Verzeichnis erneut einlesen
GetServerInfoName, Version, Startzeit, Generation und Listener
ListParents, CreateVolume, ResizeVolume, SnapshotVolume, DeleteVolume, GetVolume, ListVolumesüber einen privilegierten Provisioner angelegter Speicher und daraus bereitgestellte Freigaben – siehe Volumes (seit v0.21.0)

Eine Berechtigung nennt einen Benutzer, eine Gruppe (@group in einer Konfigurationsdatei), einen oidc:groups:-Wert oder einen oidc:user:-Namen – das Vokabular, das die Konfigurationsdatei verwendet, siehe Personen, die der Identitätsanbieter nennt.

grpc.health.v1 antwortet auf demselben Listener. reflection = true im Block schaltet gRPC-Server-Reflection ein, für grpcurl.

Jede Änderung sagt, was sie mit der Bereitstellung gemacht hat

Eine Änderung antwortet mit einer Applied-Nachricht:

message Applied {
  uint64 generation = 1;          // the generation now being served
  uint64 connections_closed = 2;  // how many the previous one had open, and closed
}

Deshalb hat jeder RPC eine eigene Antwortnachricht, statt die Freigabe selbst zurückzugeben: Die .proto folgt den STANDARD-Lint-Regeln von buf, geprüft durch buf lint in der CI von fileshare, vor dem „return the resource" von AIP-131 – die Hülle ist das, was Applied transportiert.

Wo sie lauscht und wer sie aufrufen darf

Über TCP gilt gegenseitiges TLS oder gar nichts

tls_cert_file, tls_key_file und client_ca_file, alle drei, Loopback eingeschlossen: Jeder lokale Benutzer kann Loopback erreichen, und diese API entscheidet, wer wessen Dateien liest. Ein Unix-Socket wird mit 0600 angelegt, und seine Berechtigungen sind seine Zugriffskontrolle.

admin {
  listen         = "127.0.0.1:7443"
  tls_cert_file  = "/etc/fileshare/admin/server.pem"
  tls_key_file   = "/etc/fileshare/admin/server.key"
  client_ca_file = "/etc/fileshare/admin/clients.pem"
  state_file     = "/var/lib/fileshare/shares.json"
  source_roots   = ["/srv/images"]
}

Der Listener ist grpc-transports/control. Jede Änderung wird mit ihrem Urheber protokolliert: dem CN des Client-Zertifikats oder der uid des Socket-Peers.

Über HTTPS, für OIDC-Tokens

Seit v0.28.0 wird dieselbe API – derselbe Zustand, dasselbe Audit – auch über HTTPS angeboten, wenn der admin-Block einen web-Block hat: Connect, gRPC-Web und gRPC auf einem Listener, mit dem Zertifikat des tls-Blocks. Mit ihr sprechen die Web- und die nativen Oberflächen.

admin {
  listen     = "unix:///run/fileshare/admin.sock"
  state_file = "/var/lib/fileshare/shares.json"
  web {
    listen = "0.0.0.0:8443"
    issuer "https://login.example.org" {
      audience = "fileshare-a"            # what THIS server is called there
      groups   = ["fileshare-admins"]
    }
    issuer "https://login.partner.example.org" {
      audience = "fileshare-a.partner"
      subjects = ["5b0c…"]                # a person is (issuer, sub)
    }
  }
}
Feld
listeneine TCP-Adresse; ohne tls-Block abgelehnt – ein Bearer-Token ist so viel wert wie ein Passwort
issuer "<url>"ein Block je Identitätsanbieter, sein iss; doppelt angegeben wird abgelehnt
audiencedie aud, die ein Token nennen muss: wie dieser Server bei diesem Anbieter heißt; Pflicht
subjects, groupswer aufrufen darf: ein sub oder eine Gruppe im Gruppen-Claim; mindestens eines von beiden
jwks_urlüberspringt die OIDC-Discovery, für einen Anbieter, der sie nicht veröffentlicht
groups_claimder Claim, der die Gruppen trägt; Standard groups

Wer eine Antwort erhält. Ein Aufruf wird beantwortet, wenn der Verifier eines Ausstellers sein Bearer-Token annimmt – Signatur, iss, eine aud, die diesen Server nennt, Zeiten – und der Block dieses Ausstellers das Subject des Tokens oder eine seiner Gruppen nennt. Ein bei einem Aussteller eingetragenes Subject ist bei einem anderen niemand: Eine Person ist (Aussteller, sub). Geprüft wird allein anhand der Header, bevor ein Byte der Nachricht dekodiert wird.

Eine Audience je Server. Ein an mehrere Server adressiertes Token könnte von jedem von ihnen bei den anderen wiederverwendet werden (RFC 8707, RFC 9068); daher hat jeder Server seine eigene, und eine Oberfläche, die mehrere Server steuert, holt ein Token je Server.

Was ein abgewiesener Aufrufer erfährt. „Abgelehnt“, mehr nicht. Der Grund und jede vorgenommene Änderung gehen ins Audit, als oidc=<Aussteller> <sub>.

Kein CORS. Browser rufen sie nicht direkt auf: Sie erreichen sie über den Server der Oberflächen, der die Tokens hält. Anfragen sind auf 1 MiB begrenzt, und jede Phase einer Verbindung hat ein Zeitlimit. Jeder Aussteller wird beim Start kontaktiert, um seine Schlüssel zu lesen; ist einer nicht erreichbar, bricht der Start ab und nennt ihn.

Im Browser: die Konsole

go-fileshare/console ist die Admin-Konsole, die diese API aufruft: Freigaben, Details, Berechtigungen, Anlegen, Aktivieren, Deaktivieren, Löschen, Gewähren und Entziehen, und Volumes (anlegen, vergrößern, Snapshot, freigeben, löschen – ohne je Daten zu vernichten), über alle Server zugleich. Sie läuft im Browser hinter go-fileshare/portal, das Personen per OpenID Connect anmeldet und ihre Tokens verwahrt: eines je Server, an diesen Server allein adressiert. Der Browser hält nie eines.

# portal.hcl
server "paris" {
  url      = "https://fs-paris.example.org:8443"   # dieser Web-Listener
  issuer   = "https://login.example.org"
  resource = "https://fs-paris.example.org/"       # die Audience dieses Servers oben
}
ui "console" { dir = "/usr/share/fileshare-console" }

Dieselbe Konsole läuft auch nativ, ohne Portal. Sie meldet sich über den Systembrowser und einen lokalen Listener an, als öffentlicher Client (go-authn/rp, RFC 8252), und ruft jeden Server mit einem Token nur für ihn auf. Die Server stehen in ihrer eigenen console.hcl, mit denselben url und resource wie hier.

Der Datei-Explorer, go-fileshare/explorer, läuft hinter demselben Portal: durchsuchen, hochladen, herunterladen, umbenennen, kopieren und löschen, auf jeder Freigabe, die eine Person nutzen darf. Er erreicht das WebDAV jedes Servers über das Portal (/dav/<server>/...) mit einem Token für diesen Server, und das Portal liefert die Antwort inert aus (Content-Security-Policy: sandbox, als Anhang): Eine Seite auf einer Freigabe läuft nie im Namen des Portals. Er braucht fileshare v0.29.0 oder neuer, dessen WebDAV an seiner Wurzel die Freigaben auflistet und einen Upload in Stücken annimmt.

# portal.hcl, derselbe Server
server "paris" {
  # ... wie oben, und sein WebDAV:
  dav_url = "https://fs-paris.example.org"
}
ui "explorer" { dir = "/usr/share/fileshare-explorer" }

Der Explorer läuft auch nativ (explorer v0.2.0), ohne Portal, wie die Konsole: Er meldet sich über den Systembrowser als öffentlicher Client an und ruft das WebDAV jedes Servers mit einem Token nur für ihn auf. Seine Server stehen in explorer.hcl, jeder mit seiner dav_url (nur https) und der resource seines oidc-Blocks. Keiner der beiden nativen Clients folgt mit einem Token einer Umleitung: Der HTTP-Client von Go behält Authorization bei einer Umleitung auf denselben Host mit jedem Schema, was ein falsch konfigurierter Proxy mit einem 307 auf eine einfache http-Adresse in ein im Klartext gesendetes Token verwandeln würde.

Ein fehlgeschlagener Upload wird fortgesetzt (explorer v0.3.0). Die Teildatei ist nach der Datei benannt – Name, Größe und Änderungszeit –, sodass ein erneuter Upload derselben Datei dort weitermacht, wo die Teildatei endet, nachdem ihre letzten 64 KiB zurückgelesen und als die der Datei erkannt wurden. Eine andere Datei wird nie fortgesetzt. Ein Ausfall des Netzes oder des Servers behält die Teildatei; eine Ablehnung (403, eine volle Freigabe: 507) entfernt sie.

Die audience jedes Servers ist die resource, die das Portal für ihn verwendet. Eine eigene Konsole braucht es nicht, um die API zu nutzen: Jeder Connect-, gRPC-Web- oder gRPC-Client mit einem Token für diesen Server genügt.

Was sie anfasst und was nicht

Sie verwaltet die Freigaben, die sie angelegt hat. Eine in der Konfiguration geschriebene Freigabe wird mit denselben Feldern aufgelistet, und eine Änderung ihrer Definition wird mit FAILED_PRECONDITION abgelehnt: Eine an zwei Stellen definierte Freigabe ist eine Frage, die niemand beantworten will, und die Datei ist der Ort, an dem diese definiert ist. Die Freigaben der API liegen in state_file, atomar geschrieben, und werden beim nächsten Start wieder bereitgestellt – deshalb wird ein Block ohne state_file abgelehnt.

Eine Quelle muss unter source_roots liegen, aufgelöst – Links verfolgt, .. entfernt –, und es ist der aufgelöste Pfad, der gespeichert wird. Ohne source_roots kann die API überhaupt keine Freigaben anlegen: Der Prozess kann /dev und /etc lesen, und niemand wollte diese einem Aufrufer übergeben. Das wird bei jedem Start erneut geprüft, und eine Freigabe in state_file, die nicht mehr unter einer Wurzel liegt, verhindert den Start und wird dabei genannt.

Jede Freigabe hat mindestens eine Berechtigung. Eine Freigabe ohne ist für jeden offen, der sich authentifiziert. Die Datei darf das absichtlich sagen; ein API-Aufruf sollte es nicht durch Weglassen sagen. Also braucht CreateShare eine Berechtigung, und das Widerrufen der letzten wird abgelehnt – löschen Sie stattdessen die Freigabe.

Namen, die ein Protokoll transportieren kann. Ein Freigabename hat höchstens 80 Zeichen, beginnt und endet nicht mit einem Leerzeichen, ist nicht . oder .. und enthält kein nicht druckbares Zeichen und keines von :*?"<>|{}%. Subjekte enthalten ebenfalls kein nicht druckbares Zeichen, und eine Freigabe nimmt höchstens 1000 Berechtigungen auf – lange vorher eine Aufgabe für eine Gruppe. Jedes davon wurde vor v0.14.0 akzeptiert, in state_file geschrieben und war dann bei jedem Start fatal.

Keine Freigabe darf enthalten: die Konfiguration, die Zustandsdatei oder die Geheimnisse, die sie nennen: Wer hineinschreibt, würde umschreiben, wer was darf. Seit v0.17.0 gehören dazu authorized_keys_file, dsn_file und bind_password_file eines users-Blocks, die ca_file des ssf-Blocks und eine SQLite-Datenbank, die ein DSN nennt: Wer schreiben darf und seinen Schlüssel zu den authorized_keys eines anderen hinzufügen könnte, würde sich als dieser anmelden. Ebenso wenig darf eine Freigabe eine andere enthalten (siehe Verzeichnisse).

Eine Änderung wird wie eine Konfiguration geprüft, geöffnet, festgeschrieben und dann bereitgestellt. Eine Änderung, die der Server nicht einhalten kann – ein Image, das sich nicht öffnen lässt, ein Name, den niemand im Verzeichnis hat –, wird abgelehnt, wobei das Bereitgestellte und das Geschriebene unverändert bleiben.

Eine Freigabe deaktivieren

Deaktivieren entspricht Sambas available = no: Die Freigabe bleibt definiert, und jeder Verbindungsversuch schlägt fehl; ihre offenen Verbindungen werden geschlossen, und ihr Image oder Verzeichnis wird losgelassen, sodass die Datei ersetzt werden kann, während sie offline ist. EnableShare stellt sie wieder bereit und wird abgelehnt, wenn ihre Quelle sich nicht mehr öffnen lässt. CreateShare kann eine Freigabe auch bereits deaktiviert anlegen.

Anders als jede andere Änderung gilt sie auch für eine Freigabe der Konfiguration – eine Freigabe offline zu nehmen ist ein Vorgang, keine Definition –, und sie übersteht einen Neustart: Sie wird in der Zustandsdatei gespeichert. fileshare check listet auf, was offline ist.

Eine Änderung ist eine neue Generation

Eine Änderung startet die Protokollserver neu und schließt ihre Verbindungen

SMB prüft einmal pro Tree Connect, wer sich verbinden darf, und SFTP baut den Baum einer Person einmal pro Anmeldung auf, sodass eine Sitzung, die einen Widerruf überdauert, den gerade entzogenen Zugriff behielte. „Widerrufen, außer für alle, die schon verbunden waren" ist kein Widerruf.

Eine Änderung wird also nicht auf die laufenden Server angewendet: Sie werden durch neue ersetzt, die aus der neuen Liste gebaut werden – eine Generation –, so wie es ein Neustart täte, ohne die Ports aufzugeben. Die Bibliotheken könnten es nicht anders: go-filesystems/smb hat Share und kein Unshare, nfs hat Export und kein Unexport.

Die Ports bleiben gebunden, sodass ein Client, der sich während einer Änderung verbindet, wartet, statt abgelehnt zu werden. Images, deren Freigabe sich nicht geändert hat, behalten ihren Treiber und werden nie ein zweites Mal geöffnet. Clients verbinden sich neu; das ist der Preis, gezahlt bei jeder Änderung – deshalb wendet die API eine Änderung pro Aufruf an statt einer pro Feld.

Grenzen

  • --isolate passt noch nicht zu einem admin-Block – ebenso wenig zu einem metrics-Block –: Es gibt keinen einzelnen Prozess, auf den eine Änderung angewendet werden könnte. Die Kombination wird abgelehnt.
  • Ein mit -tags nogrpc gebautes Binary hat keine Admin-API, und eine Konfiguration mit einem admin-Block wird abgelehnt, statt ohne sie bereitgestellt zu werden. Dieses Tag spart 11,7 MB; siehe nur bauen, was Sie brauchen.

Die .proto ändern

Der Go-Code unter proto/ wird generiert und committet, sodass go install kein protoc braucht. Nach einer Änderung an einer .proto – der der Admin-API oder der des Provisioners, proto/fileshare/provision/v1/provision.proto – generieren Sie beide mit den Versionen neu, die die CI von fileshare pinnt (protoc 34.1, protoc-gen-go v1.36.12, protoc-gen-go-grpc v1.6.2). Die CI generiert sie neu und schlägt fehl, wenn der committete Code abweicht oder wenn generierter Code nicht committet wurde:

protoc -I proto --go_out=. --go_opt=module=github.com/go-fileshare/fileshare \
  --go-grpc_out=. --go-grpc_opt=module=github.com/go-fileshare/fileshare \
  proto/fileshare/admin/v1/admin.proto proto/fileshare/provision/v1/provision.proto

Erstellt am von tannevaled

Geändert am von tannevaled