Comprendre Erlang et Lua / Luerl pour VerneMQ MongoDB auth_on_register hook
Mon objectif / TLDR
L'objectif de cet article est d'expliquer comment définir des points de montage personnalisés pour VerneMQ en modifiant le script Lua d'authentification de MongoDB (lua/auth/mongodb.lua).
La définition d'un point de montage personnalisé est possible avec VerneMQ non seulement en définissant manuellement des points de montage pour des listeners spécifiques (par exemple des ports), mais aussi de manière programmatique pendant l'autorisation dans vos scripts.
J'ai eu du mal à m'y retrouver, car il n'y a pas d'exemples, et je n'avais jamais programmé dans un langage fonctionnel (comme Erlang) auparavant. De plus, je n'avais jamais touché à Lua auparavant - mais Lua est plus facile à comprendre qu'Erlang, à mon avis.
pourquoi des points de montage personnalisés ?
L'idée est d'isoler les différents utilisateurs les uns des autres (multi-tenant). Chaque utilisateur aura sa propre arborescence de sujets, il ne sera pas nécessaire de vérifier les collisions ou de demander aux utilisateurs d'ajouter un préfixe supplémentaire.
L'isolation totale permet également d'accroître la sécurité en cas d'erreur de paramétrage des ACL.
De mon point de vue, c'est donc un must !
modifications nécessaires du script
La modification nécessaire du script est la suivante (collée en tant que code et à nouveau en tant que capture d'écran, afin que vous puissiez voir où WordPress se trompe dans le formatage, etc :)
fonction auth_on_register(reg)
si reg.username ~= nil et reg.password ~= nil alors
doc = mongodb.find_one(pool, "vmq_acl_auth",
{client_id = reg.client_id,
username = reg.username})
si doc ~= false then
si doc.active alors
si doc.passhash == bcrypt.hashpw(reg.password, doc.passhash) alors
cache_insert(
doc.mountpoint,
reg.client_id,
reg.username,
doc.publish_acl,
doc.subscribe_acl
)
reg.mountpoint = doc.mountpoint
- alternativement retourner juste true, mais alors aucun modificateur ne peut être mis en place
retour {
subscriber_id = {
mountpoint = doc.mountpoint,
client_id = reg.client_id
}
}
fin
fin
fin
fin
retournez à la case départ
fin
Bien entendu, vous pouvez également renvoyer d'autres modificateurs. Voici une liste plus exhaustive pour auth_on_register, tirée de la page d'accueil du site Web de la Commission européenne. Documentation de VerneMQ:
Remarque : il est important de fournir les bons types :
le subscriber_id est un type plus complexe consistant en un Tuple (du point de vue d'Erlang) de mountpoint et de client_id.
C'est pourquoi je passe dans une table d'une table (dans la terminologie de Lua):
retour {
subscriber_id = {
mountpoint = doc.mountpoint,
client_id = reg.client_id
}
}
Note : le formatage n'a aucune importance pour Lua, je l'ai simplement configuré de cette façon pour une meilleure lisibilité.
Plusieurs paramètres peuvent être modifiés en même temps. Par exemple, vous pouvez réduire le débit des messages, changer l'indicateur clean_session, etc.
Élaboration du code Lua
Notez que j'ai mis à jour les contrôles initiaux pour lire :
doc = mongodb.find_one(pool, "vmq_acl_auth",
{client_id = reg.client_id,
username = reg.username})
Ainsi, la vérification du point de montage est omise. Comme nous allons définir le point de montage à partir de la base de données, nous ne nous soucions pas du point de montage initial du client (qui sera très probablement "" une chaîne vide).
Je renvoie le point de montage tel qu'il a été lu dans la base de données, et je définis le client_id comme celui qui nous a été transmis lors de la demande d'authentification. À ce stade, l'utilisateur est déjà authentifié auprès de la base de données.
Recharger le script
Vous pouvez simplement recharger le script après l'avoir mis à jour en cours d'exécution, en utilisant la ligne de commande suivante :
vmq-admin script reload path=./share/lua/auth/mongodb.lua
VerneMQ n'a pas besoin d'être arrêté et redémarré pour cela. C'est une bonne chose, car cela accélère considérablement le développement et les tests !
Débogage (affichage des sessions)
Il suffit d'utiliser
vmq-admin session show
Comme vous pouvez le voir, les points de montage sont également affichés.
Cela correspond aux informations attendues de la base de données dont je dispose :
Le client_id shahrukh est censé avoir un point de montage vide, et le client_id goat est censé avoir le point de montage beardedgoat.
Notez que les ACLs à ce stade sont très permissives pour permettre un débogage plus facile :
Bonus : commandes mosquitto_sub pour les tests
mosquitto_sub -t "#" -u "o2PTwBb" -P "Dis8gJ2yhdSYmkQBH1mCosxgJmAxCQm3698tg7Mh8mNFAHXDf4" -host 192.168.1.2 -port 1883 -id "shahrukh" -q 2 -verbose
mosquitto_sub -t "#" -u "o2PTwBb" -P "Dis8gJ2yhdSYmkQBH1mCosxgJmAxCQm3698tg7Mh8mNFAHXDf4" -host 192.168.1.2 -port 1883 -id "goat" -q 2 -verbose
A propos d'Erlang
Erlang est un langage qui a été développé chez Ericsson spécifiquement pour les systèmes de télécommunication à haute disponibilité et à tolérance de pannes.
Il présente quelques caractéristiques intéressantes, si vous êtes intéressé, lire plus à ce sujet sur Wikipedia.
Le principal défi pour comprendre le code Erlang est qu'il est très différent de tout ce que j'ai rencontré jusqu'à présent.
Erlang est un langage fonctionnel. Cela signifie que vous n'écrivez pas de code qui dit "fais ceci, fais cela, regarde la variable, puis fais ceci, fais cela", mais que tout est appelé comme une fonction avec une valeur de retour.
Par exemple, au lieu de boucles, vous aurez des fonctions s'appelant les unes les autres de manière récursive.
De plus, le run-time Erlang fait correspondre la fonction correcte qu'il doit appeler en fonction des paramètres.
Par exemple, pour une fonction / boucle récursive, vous continuerez à appeler la fonction jusqu'à ce qu'un certain point soit atteint. Par exemple, vous avez traité le dernier élément de la liste, et la liste est vide - ici, vous pouvez répondre différemment, au lieu de continuer à récurer, donner le résultat final.
Comprendre le code VerneMQ Erlang
Note : J'ai reproduit le code dans le seul but d'expliquer ce qu'il fait, tout le code est protégé par des droits d'auteur. Octavo Labs AG.
En comprenant un peu le code erl, pour vmq_diversity_plugin.erl:
-module(vmq_diversity_plugin).
le nom du module ici doit correspondre au nom de fichier du module
-export([auth_on_register/5, ... etc]).
quelles fonctions peuvent être appelées dans ce module, et le nombre de paramètres qu'ils attendent.
%%%===================================================================
%%% Fonctions de crochet
%%%===================================================================
%% appelé comme un crochet all_till_ok
auth_on_register(Peer, SubscriberId, UserName, Password, CleanSession) ->
{PPeer, Port} = peer(Peer),
{MP, ClientId} = subscriber_id(SubscriberId),
Res = all_till_ok(auth_on_register, [{addr, PPeer},
{port, Port},
{mountpoint, MP},
{client_id, ClientId},
{nom d'utilisateur, nilify(Nom d'utilisateur)},
{mot de passe, nilify(Password)},
{clean_session, CleanSession}]),
conv_res(auth_on_reg, Res).
tout_jusqu'à_ok appellera tous les "backends" d'authentification disponibles (hooks) jusqu'à ce qu'ils retournent tous ok, pour donner à chacun une chance d'authentifier l'utilisateur.
auth_on_publish(Nom d'utilisateur, SubscriberId, QoS, Topic, Payload, IsRetain) ->
{MP, ClientId} = subscriber_id(SubscriberId),
case vmq_diversity_cache:match_publish_acl(MP, ClientId, QoS, Topic, Payload, IsRetain) de
vrai ->
%% A trouvé une entrée de cache valide qui accorde cette publication.
ok ;
Modificateurs quand is_list(Modificateurs) ->
%% A trouvé une entrée de cache valide contenant des modificateurs
{ok, Modificateurs} ;
faux ->
%% A trouvé une entrée de cache valide qui rejette cette publication.
{error, not_authorized} ;
no_cache ->
Res = all_till_ok(auth_on_publish, [{nom d'utilisateur, nilify(UserName)},
{mountpoint, MP},
{client_id, ClientId},
{qos, QoS},
{topic, unword(Topic)},
{payload, Payload},
{retain, IsRetain}]),
conv_res(auth_on_pub, Res)
fin.
note :
SubscriberId contient à la fois mountpoint et client_id :
il est décomposé en mountpoint et client_id dans la première déclaration :
{MP, ClientId} = subscriber_id(SubscriberId),
Notez que les variables commencent par une lettre majuscule en Erlang. MP et ClientId sont donc des variables.
case vmq_diversity_cache:match_publish_acl(MP, ClientId, QoS, Topic, Payload, IsRetain) de
vrai ->
%% A trouvé une entrée de cache valide qui accorde cette publication.
ok ;
le module vmq_diversity_cache est appelé avec la fonction match_publish_acl.
le point de montage (MP), ClientId, Quality of Service (QoS), Topic, Payload et IsRetain y sont passés.
Si cette fonction renvoie vrai, la valeur de retour de la fonction Erlang auth_on_publish est ok.
Notez qu'en Erlang, puisque "ok" commence par une petite lettre, il s'agit juste d'un nom - pas d'une variable (c'est, spécifiquement, une instance du type de données Atom). L'équivalent dans Crystal Lang serait probablement les symboles.
ok ;
Notez que le " ;" n'est pas une terminaison de la déclaration, mais doit être lu comme un "else".
Modificateurs quand is_list(Modificateurs) ->
%% A trouvé une entrée de cache valide contenant des modificateurs
{ok, Modificateurs} ;
lorsque la valeur renvoyée est une liste, elle est transmise en tant que valeur de retour avec des modificateurs - dans ce cas, il s'agit d'un tuple Erlang {ok, Modifiers} (qui regroupe l'atome "ok" et la variable Modifiers et les renvoie).
Notez que is_list est une fonction intégrée (BIF) d'Erlang, et non quelque chose de spécifique à Lua/Luerl.
faux ->
%% A trouvé une entrée de cache valide qui rejette cette publication.
{error, not_authorized} ;
ici, au lieu de "ok", "error" est transmis, ainsi que "not_authorized". Ce sont tous des Atomes, pas des variables - tout comme false.
no_cache ->
Res = all_till_ok(auth_on_publish, [{nom d'utilisateur, nilify(UserName)},
{mountpoint, MP},
{client_id, ClientId},
{qos, QoS},
{topic, unword(Topic)},
{payload, Payload},
{retain, IsRetain}]),
conv_res(auth_on_pub, Res)
enfin, si le cache renvoie "no_cache", on appelle la fonction all_till_ok, avec "auth_on_publish", en passant un tableau de tuples, cherchant si un hook peut authentifier la publication de ce message.
all_till_ok([Pid|Rest], HookName, Args) ->
case vmq_diversity_script:call_function(Pid, HookName, Args) of
vrai ->
ok ;
Mods0 quand is_list(Mods0) ->
Mods1 = convert_modifiers(HookName, Mods0),
case vmq_plugin_util:check_modifiers(HookName, Mods1) of
erreur ->
{erreur, {modificateurs_invalides, Mods1}} ;
CheckedModifiers ->
{ok, CheckedModifiers}
fin ;
faux ->
{erreur, lua_script_returned_false} ;
erreur ->
{erreur, lua_script_error} ;
{erreur, Raison} ->
{erreur, raison} ;
_ ->
all_till_ok(Rest, HookName, Args)
fin ;
all_till_ok([], _, _) -> suivant.
ici, la fonction all_till_ok appelle la fonction vmq_diversity_script:call_function, en passant également le HookName (qui est défini comme étant par exemple auth_on_publish ou auth_on_register), et les arguments en faveur du Crochet.
Si le crochet renvoie "true", alors la valeur à renvoyer est "ok".
Sinon, si le Hook renvoie une liste de modificateurs,
les modificateurs sont exécutés par convertisseur_modificateurs
Comme les variables sont immuables en Erlang - c'est-à-dire qu'une fois que vous avez assigné quelque chose à une variable, vous ne pouvez pas le réassigner à la variable, nous utilisons une nouvelle variable pour les modificateurs convertis, Mods1.
convert_modifiers(Hook, Mods0) ->
vmq_diversity_utils:convert_modifiers(Hook, Mods0).
ceci ne fait qu'envelopper la fonction vmq_diversity_utils:convert_modifiers.
Il est défini ici :
convert_modifiers(auth_on_subscribe, Mods0) ->
normalize_subscribe_topics(convert(Mods0)) ;
convert_modifiers(on_unsubscribe, Mods0) ->
convertir(Mods0) ;
convert_modifiers(Hook, Mods0) ->
Mods1 = atomiser_clés(Mods0),
Converties = lists:map(
fun(Mod) ->
convert_modifier(Hook, Mod)
fin,
Mods1),
cas lists:member(Hook, [auth_on_register_m5,
auth_on_subscribe_m5,
auth_on_unsubscribe_m5,
on_unsubscribe_m5,
auth_on_publish_m5,
on_deliver_m5,
on_auth_m5]) de
vrai ->
maps:from_list(Converted) ;
_ ->
Convertie
fin.
Cela montre les capacités de correspondance d'Erlang. Selon l'Atome avec lequel la première partie de la fonction est appelée, une fonction différente est exécutée.
Si elle est appelée avec auth_on_subscribeil appellera normalize_subscribe_topics, en passant dans une version convertie de Mods0.
normalize_subscribe_topics(convert(Mods0)) ;
convert est défini et expliqué plus bas dans le même fichier :
%% @doc convertit récursivement une valeur renvoyée par lua en une valeur erlang.
Structure de données %%.
convert(Val) when is_list(Val) ->
convert_list(Val, []) ;
convert(Val) when is_number(Val) ->
cas round(Val) de
RVal quand RVal == Val -> RVal ;
_ -> Val
fin ;
convert(Val) when is_binary(Val) -> Val ;
convert(Val) when is_boolean(Val) -> Val ;
convert(nil) -> indéfini.
si Val (Mods0 dans notre cas) est une liste, convert_list est appelé :
convert_list([ListItem|Rest], Acc) ->
convert_list(Rest, [convert_list_item(ListItem)|Acc]) ;
convert_list([], Acc) -> lists:reverse(Acc).
convert_list_item({Idx, Val}) when is_integer(Idx) ->
%% tableau lua
convert(Val) ;
convert_list_item({BinKey, Val}) when is_binary(BinKey) ->
essayez list_to_existing_atom(binary_to_list(BinKey)) de
Clé -> {Clé, convertir(Val)}
attraper
_:_ ->
{BinKey, convert(Val)}
fin.
convert_list([ListItem|Rest], Acc) ->
convert_list(Rest, [convert_list_item(ListItem)|Acc]) ;
convert_list([], Acc) -> lists:reverse(Acc).
Ceci utilise la récursion d'Erlang. La liste est convertie un élément à la fois, en s'appelant récursivement (et en traitant chaque élément de la liste à son tour, en utilisant convert_list_item). L'élément de la liste est transféré de la gauche vers la droite lorsqu'il a été traité, afin que la variable de gauche finisse par être une liste vide. Une fois que c'est le cas, la deuxième partie correspondra :
convert_list([], Acc) -> lists:reverse(Acc).
et le résultat de la fonction sera lists:reverse(Acc) (le côté droit).
convert_list_item utilise certaines fonctions Erlang, actuellement je ne comprends pas complètement cette partie. Je comprends cependant la première partie :
convert_list_item({Idx, Val}) when is_integer(Idx) ->
%% tableau lua
convert(Val) ;
Pour un tableau Lua (table), le tableau est dépaqueté et l'index du tableau de l'élément correspondant est abandonné.
Notez qu'en Lua, l'option tableau est le seul type de tableau / hachage / ... associatif. Il n'y a pas de type de tableau spécifique.
Retour à la fonction all_till_ok :
case vmq_plugin_util:check_modifiers(HookName, Mods1) of
erreur ->
{erreur, {modificateurs_invalides, Mods1}} ;
CheckedModifiers ->
{ok, CheckedModifiers}
fin ;
Les Modificateurs convertis sont passés dans vmq_plugin_util:check_modifiers (c'est-à-dire la fonction check_modifiers du module vmq_plugin_util).
Si cette fonction renvoie une erreur, la valeur de retour est un tuple de {error, {invalid_modifiers, Mods1}} ;
Les modificateurs error et invalid_modifiers ne sont, rappelons-le, que des noms. De plus, les modificateurs sont transmis pour notre inspection. (Encore une fois, notez le point-virgule à la fin de la première partie de la déclaration, indiquant un "else")
si la fonction retourne une variable à la place, nous retournons un tuple de {ok, CheckedModifiers}.
Cette fonction check_modifiers est implémentée ici :
-spec check_modifiers(atom(), list() | map()) -> list() | map() | error.
Cette ligne nous indique que la fonction check_modifiers attend un atome (i.e. auth_on_subscribe) comme premier paramètre, et une liste() ou une map() comme second paramètre. Elle retourne une liste(), ou une map(), ou une erreur (un atome()).
Une fonction intimidante, je l'admets honnêtement. Passons-la en revue :
AllowedModifiers = modifiers(Hook),
AllowedModifiers est une variable. Elle appelle la fonction modifiers, avec la variable Hook passée.
Par exemple, pour auth_on_registeril correspondra à la fonction suivante :
modifiers(auth_on_register) ->
[{allow_register, fun val_bool/1},
{allow_publish, fun val_bool/1},
{allow_subscribe, fun val_bool/1},
{allow_unsubscribe, fun val_bool/1},
{max_message_size, fun val_int/1},
{subscriber_id, fun val_subscriber_id/1},
{clean_session, fun val_bool/1},
{max_message_rate, fun val_int/1},
{max_inflight_messages, fun val_int/1},
{politique_d'abonnement_partagé, fun val_atom/1},
{intervalle d'essai, fun val_int/1},
{upgrade_qos, fun val_bool/1},
{allow_multiple_sessions, fun val_bool/1},
{max_online_messages, fun val_int/1},
{max_offline_messages, fun val_int/1},
{queue_deliver_mode, fun val_atom/1},
{type_queue, fun val_atom/1},
{max_drain_time, fun val_int/1},
{max_msgs_par_drain_step, fun val_int/1}] ;
de sorte qu'il retournera un tableau de tuples. Dans ces tuples, les noms autorisés (qui peuvent être modifiés) sont définis, par exemple le subscriber_id. (rappelez-vous, l'identifiant de l'abonné contient à la fois le point de montage et l'identifiant du client !) , et la fonction pour vérifier la valeur. Par exemple, fun val_subscriber_id/1 signifie que la fonction val_subscriber_id doit être vérifiée, avec 1 paramètre à lui passer.
Pour comprendre les déclarations suivantes, nous devons regarder un peu la documentation d'Erlang :
lists:foldl(fun (_, error) -> error ;
http://erlang.org/doc/man/lists.html
foldl appellera une fonction sur les éléments successifs d'une liste
C'est ainsi qu'il est défini.
foldl(Fun, Acc0, Liste) -> Acc1
donc nous passons dans une fonction, une liste vide, et nos modificateurs.
explication pour la liste vide : "Acc0 est retourné si la liste est vide" - c'est-à-dire que si notre liste initiale de modificateurs est vide, nous retournons une liste vide.
Le "_" signifie une variable anonyme. Cela signifie que la variable est requise, mais que sa valeur peut être ignorée..
Voir http://erlang.org/doc/reference_manual/expressions.html pour les détails.
Ainsi, si la fonction est appelée avec quelque chose, et error comme deuxième variable passée, le résultat est erreur.
sinon, si elle est appelée avec un tuple {ModKey, ModVal} et Acc, la valeur du résultat dépend de la présence ou non de la clé dans la liste des Modificateurs autorisés. Si elle n'est pas trouvée (false), alors le résultat est erreur.
keyfind retournera un tuple si la clé est trouvée (y compris la clé), sinon false.
Puisque nous avons déjà déterminé que nous connaissons la clé, qu'elle est dans la liste, nous pouvons l'ignorer en utilisant la variable anonyme "_", et nous concentrer sur le ValidatorFun (fonction du validateur).
Ici, ModVal est ensuite exécuté par la fonction validateur (qui est définie dans la fonction modificateur appropriée que nous avons appariée).
Si la fonction retourne true, alors le tuple de ModKey et ModVal est retourné (il est correct et a été vérifié) avec le reste de Acc.
Si elle est fausse, une erreur sera enregistrée (ne peut pas valider le modificateur), et une erreur sera retournée.
Si c'est un tuple avec ok et NewModVal, alors ModKey et NewModVal seront utilisés.
Jetons un coup d'oeil à val_subscriber_id, qui nous permet de modifier l'abonné, et donc de changer le point de montage :
si nous passons dans une liste, alors d'autres vérifications sont effectuées. Sinon, false est retourné.
La liste doit contenir à la fois "client_id" et "mountpoint". Le reste du code est mal compris par moi pour le moment.
Si cette première déclaration ne correspond pas, nous retournons également false.
Le résultat
Voir le code Lua dans l'introduction, ce que nous voulions réaliser a été atteint, le point de montage est maintenant personnalisé pour chaque client :
Références :
https://github.com/vernemq/vernemq/issues/533
https://docs.vernemq.com/configuring-vernemq/db-auth
http://erlang.org/doc/getting_started/users_guide.html
https://docs.vernemq.com/plugin-development/luaplugins
https://docs.vernemq.com/plugin-development/sessionlifecycle
http://erlang.org/doc/man/erlang.html
https://en.wikipedia.org/wiki/Erlang_(programming_language)
https://github.com/vernemq/vernemq/issues/312 (ce numéro m'a orienté dans la bonne direction, merci beaucoup !)