Inzicht in Erlang & Lua / Luerl voor VerneMQ MongoDB auth_on_register haak

Mijn doel / TLDR

Mijn doel met deze blog post is om uit te leggen hoe u aangepaste mountpoints voor VerneMQ door het wijzigen van de meegeleverde MongoDB auth Lua script (lua/auth/mongodb.lua) in te stellen.

Het instellen van een aangepast mountpoint is mogelijk met VerneMQ, niet alleen door het handmatig instellen van mountpoints voor specifieke listeners (b.v. poorten), maar ook programmatisch tijdens autorisatie in uw scripts.

Ik had moeite om het te begrijpen, omdat er geen voorbeelden zijn, en ik nog niet eerder in een functionele taal (zoals Erlang) had geprogrammeerd. Bovendien had ik Lua nog niet eerder aangeraakt - maar Lua is gemakkelijker te begrijpen dan Erlang IMHO.

waarom aangepaste mountpoints?

Het idee is om verschillende gebruikers tegen elkaar te isoleren (multi-tenancy). Elke gebruiker zal zijn eigen topic tree hebben, het zal niet nodig zijn om te controleren op botsingen of om van gebruikers te eisen dat ze een extra prefix toevoegen.

De totale isolatie verhoogt ook de beveiliging tegen het per ongeluk verkeerd instellen van de ACL's.

Vanuit mijn perspectief dus een must!

noodzakelijke script aanpassingen

De noodzakelijke wijziging van het script is als volgt (geplakt als code & nogmaals als screenshot, zodat je kunt zien waar WordPress knoeit met de opmaak, etc.):

functie auth_on_register(reg)
     als reg.username ~= nil en reg.password ~= nil dan
         doc = mongodb.find_one(pool, "vmq_acl_auth",
                                 {client_id = reg.client_id,
                                  gebruikersnaam = reg.gebruikersnaam})
         als doc ~= false dan
             als doc.active dan
                 als doc.passhash == bcrypt.hashpw(reg.wachtwoord, doc.passhash) dan
                     cache_insert(
                         doc.mountpoint,
                         reg.client_id,
                         reg.gebruikersnaam,
                         doc.publish_acl,
                         doc.subscribe_acl
                         )
                     reg.mountpoint = doc.mountpoint
                     - als alternatief alleen true teruggeven, maar dan kunnen geen modifiers worden ingesteld
                     Terugkeren {
                         abonnee_id = {
                                 mountpoint = doc.mountpoint,
                                 client_id = reg.client_id
                             }
                         }

                 einde
             einde
         einde
     einde
     terugzenden
einde

afbeelding

Natuurlijk kunt u ook andere modifiers teruggeven. Hier is een meer uitputtende lijst voor auth_on_register van de VerneMQ documentatie:

afbeelding

Opmerking: het is belangrijk dat u de juiste types opgeeft:

afbeelding

de abonnee_id is een complexer type dat bestaat uit een Tuple (vanuit Erlang gezien) van mountpoint en client_id.

Dat is, waarom ik een tabel van een tabel doorgeef (in Lua's terminologie):

                    Terugkeren {
                         abonnee_id = {
                                 mountpoint = doc.mountpoint,
                                 client_id = reg.client_id
                             }
                         }

Opmerking: de opmaak is onbelangrijk voor Lua, ik heb het alleen zo opgezet voor een betere leesbaarheid.

Verschillende parameters kunnen in één keer worden gewijzigd. U kunt bijvoorbeeld de berichtsnelheid afremmen, de clean_session vlag veranderen, enz.

Uitwerking van de Lua code

Merk op dat ik de oorspronkelijke controles heb bijgewerkt:

doc = mongodb.find_one(pool, "vmq_acl_auth",
                         {client_id = reg.client_id,
                          gebruikersnaam = reg.gebruikersnaam})

Dus zonder de controle van het mountpoint. Aangezien we het mountpoint vanuit de database gaan instellen, geven we niet om het initiële mountpoint van de client (dat hoogstwaarschijnlijk "" een lege string zal zijn).

Ik geef het mountpoint terug zoals gelezen uit de database, en stel de client_id in als de originele die aan ons is doorgegeven tijdens het authenticatieverzoek. Op dit punt is de gebruiker al geauthenticeerd tegen de database.

Herladen van het script

U kunt het script gewoon opnieuw laden nadat u het run-time hebt bijgewerkt, met de volgende opdrachtregel:

vmq-admin script herladen path=./share/lua/auth/mongodb.lua

VerneMQ hoeft hiervoor niet te worden afgesloten en opnieuw opgestart. Dit is goed, want het versnelt ontwikkeling en testen enorm!

Debuggen (sessies tonen)

Gebruik gewoon

vmq-admin sessie tonen

afbeelding

Zoals u kunt zien, worden de mountpoints ook weergegeven.

Dit komt overeen met de verwachte database informatie die ik heb:

afbeelding

De client_id shahrukh wordt verondersteld een leeg mountpoint te hebben, en de client_id goat wordt verondersteld het mountpoint beardedgoat te hebben.

Merk op dat de ACL's op dit punt erg permissief zijn om gemakkelijker debuggen mogelijk te maken:

afbeelding

Bonus: mosquitto_sub commando's om te testen

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

afbeelding

Over Erlang

Erlang is een taal die bij Ericsson speciaal werd ontwikkeld voor fouttolerante telecommunicatiesystemen met hoge beschikbaarheid.

Het heeft een aantal interessante functies, als je geïnteresseerd bent, lees er meer over op Wikipedia.

De grootste uitdaging bij het begrijpen van Erlang-code is dat het heel anders is dan alles wat ik tot nu toe ben tegengekomen.

Erlang is een functionele taal. Dit betekent, dat je geen code schrijft die zegt "doe dit, doe dat, kijk naar de variabele, doe dan dit, doe dat", maar dat alles wordt aangeroepen als een functie met een return waarde.

b.v. in plaats van lussen zul je functies hebben die elkaar recursief aanroepen.

Ook zal de Erlang runtime de juiste functie aanroepen, afhankelijk van de parameters.

Bv. voor een recursieve functie / lus, blijf je de functie aanroepen tot een bepaald punt is bereikt. Bijvoorbeeld, je hebt het laatste item van de lijst verwerkt, en de lijst is leeg - hier kun je anders reageren, in plaats van te blijven recurseren, geef je het eindresultaat terug.

VerneMQ Erlang-code begrijpen

Opmerking: Ik heb de code alleen gereproduceerd om uit te leggen wat het doet, alle code is copyright van Octavo Labs AG.

Als ik de erl-code een beetje begrijp, voor vmq_diversity_plugin.erl:

-module(vmq_diversity_plugin). 

de naam van de module moet hier overeenkomen met de bestandsnaam van de module

-export([auth_on_register/5, ... etc]).

welke functies in deze module kunnen worden aangeroepen, en het aantal parameters dat ze verwachten.

%%%===================================================================

%%% Haakfuncties

%%%===================================================================

%% opgeroepen als een all_till_ok haak

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},

{ Poort, poort},

{\a6},

{client_id, ClientId},

{gebruikersnaam, nilify(gebruikersnaam)},

{wachtwoord, nilify(Wachtwoord)},

{clean_session, CleanSession}]),

conv_res(auth_on_reg, Res).

all_till_ok zal alle beschikbare authenticatie "backends" (haken) aanroepen totdat één op zijn beurt ok teruggeeft, om elk een kans te geven de gebruiker te authenticeren.

auth_on_publish(UserName, AbonneeId, QoS, Topic, Payload, IsRetain) ->

{MP, ClientId} = subscriber_id(SubscriberId),

case vmq_diversity_cache:match_publish_acl(MP, ClientId, QoS, Topic, Payload, IsRetain) van

true ->

%% Een geldig cache item gevonden dat deze publicatie toestaat

ok;

Modifiers wanneer is_list(Modifiers) ->

%% Een geldig cache item gevonden met modifiers

{\a6};

vals ->

%% Een geldig cache item gevonden dat deze publicatie weigert

{\a6};

no_cache ->

Res = all_till_ok(auth_on_publish, [{gebruikersnaam, nilify(gebruikersnaam)},

{\a6},

{client_id, ClientId},

{qos, QoS},

{topic, unword(Onderwerp)},

{Payload, Payload},

{retain, IsRetain}]),

conv_res(auth_on_pub, Res)

einde.

noot:

SubscriberId bevat zowel mountpoint als client_id:

clip_image002

het wordt uitgepakt in mountpoint en client_id in het eerste statement:

{MP, ClientId} = subscriber_id(SubscriberId),

Merk op dat variabelen in Erlang met een hoofdletter beginnen. MP en ClientId zijn dus variabelen.

case vmq_diversity_cache:match_publish_acl(MP, ClientId, QoS, Topic, Payload, IsRetain) van

true ->

%% Een geldig cache item gevonden dat deze publicatie toestaat

ok;

de vmq_diversity_cache module wordt aangeroepen met de functie match_publish_acl.

het mountpoint (MP), ClientId, Quality of Service (QoS), Topic, Payload en IsRetain worden er in doorgegeven.

Als deze functie true, de retourwaarde van de Erlang functie auth_on_publish is ok.

Merk op dat in Erlang, omdat "ok" begint met een kleine letter, het gewoon een naam is - geen variabele (het is, meer bepaald, een instantie van het datatype Atom). Het equivalent in Crystal Lang zou waarschijnlijk Symbols zijn.

ok;

Merk op dat de ";" geen beëindiging van de verklaring is, maar moet worden gelezen als een "else".

Modifiers wanneer is_list(Modifiers) ->

%% Een geldig cache item gevonden met modifiers

{\a6};

wanneer de teruggegeven waarde een lijst is, wordt deze doorgegeven als een teruggegeven waarde met Modifiers - in dit geval als een Erlang tuple {ok, Modifiers} (het groeperen van de Atom "ok" en de variabele Modifiers samen en ze teruggeven).

Merk op dat is_list een ingebouwde functie (BIF) van Erlang is, en niet iets specifieks voor Lua /Luerl.

vals ->

%% Een geldig cache item gevonden dat deze publicatie weigert

{\a6};

hier wordt in plaats van "ok" "error" doorgegeven, samen met "not_authorized". Dit zijn allemaal Atomen, geen variabelen - net als false.

no_cache ->

Res = all_till_ok(auth_on_publish, [{gebruikersnaam, nilify(gebruikersnaam)},

{\a6},

{client_id, ClientId},

{qos, QoS},

{topic, unword(Onderwerp)},

{Payload, Payload},

{retain, IsRetain}]),

conv_res(auth_on_pub, Res)

tenslotte, indien de cache "no_cache" teruggeeft, roepen we de all_till_ok functie op, met "auth_on_publish", waarbij een array van tuples wordt doorgegeven, die kijkt of een hook de publicatie van dit bericht kan authenticeren.

all_till_ok([Pid|Rest], HookName, Args) ->

geval vmq_diversity_script:call_function(Pid, HookName, Args) van

true ->

ok;

Mods0 wanneer is_list(Mods0) ->

Mods1 = convert_modifiers(HookName, Mods0),

geval vmq_plugin_util:check_modifiers(HookName, Mods1) of

fout ->

{\a6}};

CheckedModifiers ->

{ok, CheckedModifiers}

einde;

vals ->

{error, lua_script_returned_false};

fout ->

{error, lua_script_error};

{fout, reden} ->

{\a6};

_ ->

all_till_ok(Rest, HookName, Args)

einde;

all_till_ok([], _, _) -> next.

hier roept de functie all_till_ok de functie vmq_diversity_script:call_function aan, waarbij ook de HookName wordt doorgegeven (die is ingesteld op bijv. auth_on_publish of auth_on_register), en de argumenten voor de haak.

Als de haak "true" teruggeeft, dan is de waarde die teruggegeven moet worden "ok".

Anders als de haak een lijst van modifiers retourneert,

de modifiers worden doorgevoerd convert_modifiers

Aangezien variabelen onveranderlijk zijn in Erlang - dat wil zeggen, eens je iets toewijst aan een variabele, kan je het niet meer opnieuw toewijzen aan de variabele, gebruiken we een nieuwe variabele voor de geconverteerde modifiers, Mods1.

convert_modifiers(Hook, Mods0) ->

vmq_diversity_utils:convert_modifiers(Hook, Mods0).

dit omwikkelt gewoon de vmq_diversity_utils:convert_modifiers functie.

Het is hier gedefinieerd:

https://github.com/vernemq/vernemq/blob/c8b92f398e76d6ce4b8cca5e438e8ae1e717d71c/apps/vmq_diversity/src/vmq_diversity_utils.erl

convert_modifiers(auth_on_subscribe, Mods0) ->

normalize_subscribe_topics(convert(Mods0));

convert_modifiers(on_unsubscribe, Mods0) ->

converteren(Mods0);

convert_modifiers(Hook, Mods0) ->

Mods1 = atomize_keys(Mods0),

Geconverteerd = lijsten:map(

fun(Mod) ->

convert_modifier(Haak, Mod)

einde,

Mods1),

case lists:member(Haak, [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]) van

true ->

maps:from_list(Geconverteerd);

_ ->

Omgebouwd

einde.

Dit toont Erlang's mogelijkheden van matching. Afhankelijk van met welke Atom als eerste deel de functie wordt aangeroepen, wordt een andere functie uitgevoerd.

Indien opgeroepen met auth_on_subscribezal het normalize_subscribe_topics aanroepen, waarbij het een geconverteerde versie van Mods0 doorgeeft.

normalize_subscribe_topics(convert(Mods0));

converteren wordt gedefinieerd en uitgelegd verderop in hetzelfde bestand:

%% @doc converteert recursief een waarde teruggestuurd van lua naar een erlang

%% gegevensstructuur.

convert(Val) when is_list(Val) ->

convert_list(Val, []);

convert(Val) when is_number(Val) ->

geval ronde(Val) van

RVal wanneer RVal == Val -> RVal;

_ -> Val

einde;

convert(Val) wanneer is_binary(Val) -> Val;

convert(Val) wanneer is_boolean(Val) -> Val;

convert(nil) -> ongedefinieerd.

Als Val (Mods0 in ons geval) een lijst is, wordt convert_list aangeroepen:

converteer_lijst([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) ->

%% lua matrix

convert(Val);

convert_list_item({BinKey, Val}) when is_binary(BinKey) ->

probeer list_to_existing_atom(binary_to_list(BinKey)) of

Key -> {Key, convert(Val)}

vangen

_:_ ->

{BinKey, convert(Val)}

einde.

converteer_lijst([ListItem|Rest], Acc) ->

convert_list(Rest, [convert_list_item(ListItem)|Acc]);

convert_list([], Acc) -> lists:reverse(Acc).

Dit maakt gebruik van Erlang's recursie. De lijst wordt één item per keer geconverteerd, door zichzelf recursief aan te roepen (en elk lijstitem op zijn beurt te verwerken, met behulp van convert_list_item). Het lijst item wordt van links naar rechts verplaatst als het verwerkt is, zodat de linker variabele eindigt als een lege lijst. Zodra dat het geval is, komt het tweede deel overeen:

convert_list([], Acc) -> lists:reverse(Acc).

en het resultaat van de functie zal zijn lists:reverse(Acc) (het rechterlid).

convert_list_item gebruikt enkele Erlang functies, momenteel begrijp ik dat deel niet helemaal. Het eerste deel begrijp ik echter wel:

convert_list_item({Idx, Val}) when is_integer(Idx) ->

%% lua matrix

convert(Val);

Voor een Lua array (tabel), wordt de array uitgepakt en de Array index van het corresponderende item wordt weggelaten.

Merk op, dat in Lua de tabel is het enige associatieve array / hash / ... type. Er is geen specifiek array type.

Terug naar de all_till_ok functie:

geval vmq_plugin_util:check_modifiers(HookName, Mods1) of

fout ->

{\a6}};

CheckedModifiers ->

{ok, CheckedModifiers}

einde;

De geconverteerde Modifiers worden doorgegeven aan vmq_plugin_util:check_modifiers (dat wil zeggen, de check_modifiers functie in de vmq_plugin_util module).

Als deze functie een fout teruggeeft, is de terugkeerwaarde een tupel van {error, {invalid_modifiers, Mods1}};

error en invalid_modifiers zijn, vergeet niet, slechts namen. Bovendien worden de modifiers doorgegeven voor onze inspectie. (Nogmaals, let op de puntkomma aan het eind van het eerste deel van de verklaring, wat duidt op een "else")

als de functie in plaats daarvan een variabele teruggeeft, geven we een tupel van {ok, GecontroleerdeModifiers} terug.

De functie check_modifiers is hier geïmplementeerd:

https://github.com/vernemq/vernemq/blob/cd6666a2a57e16eb04011d0628359ad6a4883b34/apps/vmq_plugin/src/vmq_plugin_util.erl

-spec check_modifiers(atom(), list() | map()) -> list() | map() | error.

Deze regel vertelt ons, dat de check_modifiers functie een atom verwacht (i.e. auth_on_subscribe) als eerste parameter, en een list() of een map() als tweede parameter. Hij retourneert een lijst(), of een map(), of een fout (een atoom()).

clip_image004

Een intimiderende functie, ik zal het eerlijk toegeven. Laten we er even doorheen lopen:

ToegestaneModifiers = modifiers(Hook),

AllowedModifiers is een variabele. Het roept de functie modifiers aan, met de variabele Hook die wordt doorgegeven.

Bijvoorbeeld, voor auth_on_registerzal het overeenkomen met de volgende functie:

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},

{\a6_session, fun val_bool/1},

{max_message_rate, fun val_int/1},

{max_inflight_messages, fun val_int/1},

{shared_subscription_policy, fun val_atom/1},

{retry_interval, 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},

{queue_type, fun val_atom/1},

{max_drain_time, fun val_int/1},

{max_msgs_per_drain_step, fun val_int/1}];

dus zal het een array van tupels teruggeven. In deze tupels worden de toegestane namen (die gewijzigd kunnen worden) gedefinieerd, b.v. de subscriber_id. (denk eraan, de abonnee_id bevat zowel het mountpoint als de client_id!) , en de functie om de waarde tegen te controleren. bv. fun val_subscriber_id/1 betekent dat de functie val_subscriber_id moet worden gecontroleerd, met 1 parameter die eraan moet worden doorgegeven.

Om de volgende verklaringen te begrijpen, moeten we een beetje Erlang documentatie bekijken:

lists:foldl(fun (_, error) -> error;

http://erlang.org/doc/man/lists.html

foldl zal een functie aanroepen op opeenvolgende elementen van een lijst

Dit is hoe het gedefinieerd is.

foldl(Fun, Acc0, Lijst) -> Acc1

dus geven we een functie, een lege Lijst, en onze modifiers.

uitleg voor de lege lijst: "Acc0 wordt teruggegeven als de lijst leeg is" - dat wil zeggen als onze initiële lijst van modifiers leeg is, geven we een lege lijst terug.

De "_" betekent een anonieme variabele. Dat betekent dat de variabele vereist is, maar dat de waarde ervan kan worden genegeerd.

Zie http://erlang.org/doc/reference_manual/expressions.html voor details.

dus, als de functie wordt aangeroepen met iets, en error als de tweede variabele die wordt doorgegeven, is het resultaat fout.

anders, indien aangeroepen met een tupel {ModKey, ModVal} en Acc, hangt de resultaatwaarde af van het feit of de sleutel wordt gevonden in de lijst van AllowedModifiers. Als ze niet gevonden wordt (false), dan is het resultaat fout.

keyfind zal een tupel teruggeven als de sleutel is gevonden (inclusief de sleutel), anders false.

Aangezien we al hebben vastgesteld dat we de sleutel kennen, hij staat in de lijst, we kunnen hem negeren met behulp van de anonieme variabele "_", en ons richten op de ValidatorFun (validator functie).

Hier wordt dan ModVal door de validator functie gehaald (die gedefinieerd is in de juiste modifiers functie die we gematched hebben).

Als de functie waar teruggeeft, dan wordt de tupel van ModKey en ModVal teruggegeven (het is ok en is gecontroleerd) samen met de rest van Acc.

Als het onwaar is, zal een fout worden gelogd (kan modifier niet valideren), en fout zal worden geretourneerd.

Als het een tupel is met ok en NewModVal, dan zullen ModKey en NewModVal gebruikt worden.

Laten we eens kijken naar val_subscriber_id, waarmee we de subscriber kunnen wijzigen, en dus ook het mountpoint:

clip_image006

als we een lijst invoeren, dan worden verdere controles gedaan. Anders wordt false geretourneerd.

De lijst moet zowel "client_id" als "mountpoint" bevatten. De rest van de code begrijp ik op dit moment niet goed.

Als deze eerste verklaring niet overeenkomt, geven we ook false terug.

Het resultaat

Zie Lua code in de inleiding, wat we wilden bereiken is bereikt, het mountpoint is nu een aangepast mountpoint voor elke client:

clip_image007

Referenties:

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

https://www.erlang.org/docs

http://erlang.org/doc/man/erlang.html

https://en.wikipedia.org/wiki/Erlang_(programming_language)

https://github.com/vernemq/vernemq/issues/312 (dit nummer wees me in de juiste richting, hartelijk dank!)

https://github.com/vernemq/vernemq/blob/cd6666a2a57e16eb04011d0628359ad6a4883b34/apps/vmq_plugin/src/vmq_plugin_util.erl

https://github.com/vernemq/vernemq/blob/c8b92f398e76d6ce4b8cca5e438e8ae1e717d71c/apps/vmq_diversity/src/vmq_diversity_plugin.erl