Serveur REST : utilisateurs, multi-location et métriques | Blog eSeGeCe

Serveur REST : utilisateurs, multi-location et métriques

· Composants
Utilisateurs, multi-location et métriques du serveur REST sgcWebSockets

Le premier article présentait TsgcHTTPRESTServer et son traitement des requêtes. Celui-ci couvre les trois composants compagnons qui transforment un endpoint fonctionnel en quelque chose que vous pouvez réellement mettre devant vos clients : un stockage d'utilisateurs, la multi-location, et les métriques que l'équipe d'exploitation réclamera dès le premier jour.

Les trois sont des composants distincts. Vous créez ceux dont vous avez besoin et vous les affectez au serveur, et tout ce que vous laissez non affecté coûte un unique test de pointeur par requête.

Un stockage d'utilisateurs plutôt qu'une TStringList

TsgcHTTPServer_Users conserve les comptes avec des empreintes de mot de passe salées et itérées. Le point important est le peu de câblage nécessaire : affectez-le aux options d'authentification du serveur et la barrière HTTP Basic résout les identifiants contre lui toute seule.

uses
  sgcHTTP_REST_Server, sgcHTTP_REST_Server_Users;

FUsers := TsgcHTTPServer_Users.Create(self);
FServer := TsgcHTTPRESTServer.Create(self);
FServer.Authentication.Users := FUsers;

C'est tout. Vous n'avez pas besoin d'un gestionnaire OnAuthentication pour que l'authentification Basic fonctionne, la barrière effectue la recherche et la comparaison des empreintes.

Les comptes sont ajoutés avec AddUser, qui renvoie l'Id d'utilisateur généré, ou une chaîne vide lorsque le nom d'utilisateur est vide ou déjà pris :

FUsers.AddUser('alice', 'secret123', 'admin,reader');
FUsers.AddUser('bob', 'secret456', 'reader');

Le reste de la surface est celui auquel on s'attend, et chaque recherche est thread safe :

if FUsers.ValidateCredentials('alice', 'secret123') then
  ...

FUsers.SetPassword('bob', 'newsecret');
FUsers.EnableUser('bob', False);
FUsers.DeleteUser('bob');

Rôles

Les rôles forment une liste séparée par des virgules sur le compte, avec des utilitaires pour les lire et les tester. Une barrière de rôle dans un gestionnaire tient en un seul appel :

FUsers.AddRole('bob', 'admin');
if not FUsers.UserHasRole(vUser, 'admin') then
begin
  AResponseInfo.ResponseNo := 403;
  AResponseInfo.ContentType := 'application/json';
  AResponseInfo.ContentText := '{"error":"forbidden"}';
  Exit;
end;

Hachage

Les mots de passe sont hachés avec SHA-512 sur 10000 itérations par défaut, chaque compte portant son propre sel aléatoire. C'est ce sel par compte qui fait déjà que deux mots de passe identiques produisent des empreintes différentes, vous avez donc rarement besoin d'y toucher :

FUsers.Hashing.Algorithm := uhaSHA512;
FUsers.Hashing.Iterations := 10000;

La propriété Salt est autre chose et mérite une lecture attentive. Il s'agit d'un poivre optionnel mélangé à chaque empreinte en plus du sel propre à chaque compte, et il est vide par défaut à dessein : une valeur inventée à la construction différerait à l'exécution suivante, et chaque mot de passe correct serait alors rejeté sans aucune erreur pour l'expliquer. Ne la définissez que lorsqu'elle peut provenir d'un endroit qui n'est pas le stockage d'utilisateurs lui-même, une variable d'environnement ou un coffre-fort de clés par exemple. Un poivre conservé à côté des empreintes qu'il protège n'apporte rien. Le modifier invalide tous les mots de passe déjà stockés.

Persistance

Le stockage est en mémoire par défaut. Pointez-le vers un fichier et il survit à un redémarrage :

FUsers.Storage.StorageType := ustFile;
FUsers.Storage.FileName := 'sgcRESTUsers.dat';
FUsers.Storage.EncryptAtRest := True;
FUsers.Storage.EncryptionKey := GetKeyFromEnvironment;
FUsers.Storage.AutoSaveSeconds := 30;
FUsers.LoadUsers;

Avec AutoSaveSeconds défini, le stockage se vide périodiquement sur disque ; sinon appelez SaveUsers ou SaveToFile quand cela vous convient. Pour un stockage qui réside déjà dans votre propre base de données, mettez StorageType à ustCustom et répondez plutôt aux événements, où OnValidateCredentials devient l'autorité :

procedure TForm1.UsersValidateCredentials(Sender: TObject;
  const aUsername, aPassword: string; var Valid: Boolean);
begin
  Valid := MyDatabase.CheckLogin(aUsername, aPassword);
end;

Un détail qui compte si vous construisez un écran d'administration : GetUserByIndex vide PasswordHash et Salt avant de rendre l'enregistrement, ainsi une route d'énumération ne peut pas divulguer un identifiant, même par accident. FindUser ne le fait pas. Il retourne l'enregistrement exactement tel qu'il est stocké, champs d'identification inclus, car c'est la recherche qu'utilise le contrôle d'authentification lui-même. Lisez-en les champs dont vous avez besoin, et ne sérialisez jamais l'enregistrement complet dans un corps de réponse.

Multi-location

TsgcHTTPServer_Tenancy répond à une question par requête : pour quel client est-ce ? Il résout une chaîne de locataire avant l'exécution de votre gestionnaire, et le serveur l'expose sur la propriété en lecture seule Tenant.

FTenancy := TsgcHTTPServer_Tenancy.Create(self);
FTenancy.Resolution := trHeader;
FTenancy.HeaderName := 'X-Tenant-Id';
FTenancy.DefaultTenant := 'public';
FServer.Tenancy := FTenancy;

Il existe cinq modes de résolution :

ResolutionSourceConfiguré avec
trNonedésactivé, Tenant est toujours vide
trHostle nom d'hôteHostSuffix
trPathun segment du chemin de la requêtePathSegmentIndex
trHeaderun en-tête de requêteHeaderName
trJWTClaimune revendication du jeton bearerClaimName

Avec trHost et HostSuffix défini à .example.com, une requête vers acme.example.com se résout en acme. Avec trPath et un PathSegmentIndex de 0, /acme/api/orders se résout de la même manière. Lorsque la source configurée ne donne rien, DefaultTenant est utilisé.

Le lire dans un gestionnaire est une simple propriété :

procedure TForm1.ServerCommandGet(AContext: TIdContext;
  ARequestInfo: TIdHTTPRequestInfo; AResponseInfo: TIdHTTPResponseInfo);
begin
  AResponseInfo.ResponseNo := 200;
  AResponseInfo.ContentType := 'application/json';
  AResponseInfo.ContentText := '{"tenant":"' + FServer.Tenant + '"}';
end;

Tenant est valide dans OnBeforeCommand, OnCommandGet et OnCommandOther, et il est local au thread, donc un serveur chargé qui sert plusieurs locataires à la fois ne les mélange jamais.

À propos de trJWTClaim, un point doit être clair. La revendication est lue dans la charge utile du jeton sans valider la signature, car le locataire n'est qu'une indication de routage. La signature est toujours vérifiée par l'authentification JWT lorsque vous l'activez. Ne considérez pas le locataire comme une preuve d'identité à lui seul.

Si aucun des cinq modes ne convient, OnResolveTenant vous remet toutes les sources d'un coup et vous laisse décider :

procedure TForm1.TenancyResolveTenant(Sender: TObject;
  const aHost, aPath, aHeaderValue, aJWTPayload: string; var aTenant: string);
begin
  aTenant := LookupTenantForHost(aHost);
end;

Métriques et santé

TsgcHTTPServerStats compte ce que le serveur a fait et peut le publier sur deux endpoints. Les deux sont désactivés tant que vous ne les activez pas individuellement, ainsi rien ne devient accessible du simple fait d'avoir déposé le composant sur la fiche.

FStats := TsgcHTTPServerStats.Create(self);
FStats.Endpoints.Metrics.Enabled := True;
FStats.Endpoints.Health.Enabled := True;
FServer.ServerStats := FStats;

Les compteurs sont également lisibles depuis le code, ce qui est pratique pour un écran d'état interne :

lblRequests.Caption := IntToStr(FStats.TotalRequests);
lblErrors.Caption := IntToStr(FStats.Status5xx);
lblLatency.Caption := IntToStr(FStats.LatencyAvgMs) + ' ms';
lblUptime.Caption := IntToStr(FStats.UptimeSeconds) + ' s';

/metrics répond directement au format d'exposition texte de Prometheus, sans exportateur intermédiaire. Les compteurs par endpoint sont inclus, avec la cardinalité plafonnée à 256 chemins distincts pour qu'une route contenant un identifiant ne puisse pas faire exploser le nombre de séries. Tout ce qui dépasse le plafond est regroupé sous other.

# HELP sgc_server_endpoint_requests_total Requests per endpoint
# TYPE sgc_server_endpoint_requests_total counter
sgc_server_endpoint_requests_total{endpoint="/api/orders"} 3120
sgc_server_endpoint_requests_total{endpoint="/api/users"} 845

Si les composants pare-feu, limiteur de débit, disjoncteur ou gestionnaire de clés API sont attachés, leurs propres métriques sont ajoutées à la même sortie :

FStats.RateLimiter := FRateLimiter;
FStats.Firewall := FFirewall;

/health répond par un document JSON compact. Son status indique ok, ou degraded lorsqu'un disjoncteur est attaché et présente des circuits ouverts, ce qui le rend directement utilisable comme sonde de répartiteur de charge.

Enfin, OnStats se déclenche avec l'objet de statistiques complet si vous préférez pousser les chiffres ailleurs vous-même :

procedure TForm1.StatsEvent(Sender: TObject;
  const aStats: TsgcHTTPServerStats);
begin
  MyTelemetry.Send(aStats.TotalRequests, aStats.LatencyAvgMs);
end;

Tout assembler

Un serveur avec les trois composants attachés fait environ une douzaine de lignes, et chaque composant reste indépendant des autres :

FStats := TsgcHTTPServerStats.Create(self);
FStats.Endpoints.Health.Enabled := True;
FStats.Endpoints.Metrics.Enabled := True;

FTenancy := TsgcHTTPServer_Tenancy.Create(self);
FTenancy.Resolution := trHeader;

FUsers := TsgcHTTPServer_Users.Create(self);
FUsers.Storage.StorageType := ustFile;
FUsers.Storage.FileName := 'users.dat';
FUsers.LoadUsers;

FServer := TsgcHTTPRESTServer.Create(self);
FServer.ServerStats := FStats;
FServer.Tenancy := FTenancy;
FServer.Authentication.Users := FUsers;
FServer.Authentication.Enabled := True;
FServer.Port := 5876;
FServer.Active := True;

Le prochain article place un contrat OpenAPI devant ce serveur, pour que le routage, la validation et la documentation proviennent tous de la spécification.

Téléchargez la dernière version depuis la page de téléchargement de sgcWebSockets.