#2 Faire un vrai User.valid_name

Closed
opened 5 months ago by Darks · 18 comments
Darks commented 5 months ago

Et accepter les caractères classiques plus certains unicodes, mais pas tous.

On ne séparera sûrement pas le nom d’utilisateur du nom affiché, donc par sécurité on dégage la plupart des caractères.

Se baser sur une whitelist est beaucoup plus safe, quitte à l’agrandir au fur et à mesure. Type [A-Z0-9.-_+] + caractères accentués avec ´`^, non sensible à la casse, bien-sûr.

La méthode à modifier est dans /app/models/users.py (User.valid_name()).

Et accepter les caractères classiques plus certains unicodes, mais pas tous. On ne séparera sûrement pas le nom d'utilisateur du nom affiché, donc par sécurité on dégage la plupart des caractères. Se baser sur une whitelist est beaucoup plus safe, quitte à l'agrandir au fur et à mesure. Type `[A-Z0-9.-_+]` + caractères accentués avec ``´`^``, non sensible à la casse, bien-sûr. La méthode à modifier est dans `/app/models/users.py` (`User.valid_name()`).
Darks commented 5 months ago
Owner

Au passage, prendre en compte le fait que le LDAP risque d’avoir du mal avec l’unicode, donc l’identifiant membre sera le pseudo, forcé lowercase sans accents. Bien entendu on affichera le bon pseudo sur les pages :)

Au passage, prendre en compte le fait que le LDAP risque d'avoir du mal avec l'unicode, donc l'identifiant membre sera le pseudo, forcé lowercase sans accents. Bien entendu on affichera le bon pseudo sur les pages :)
Darks added the
enhancement
label 5 months ago
Darks added the
easy
label 5 months ago
Eragon commented 4 months ago
Collaborator

Donc, on doit avoir un pseudo à afficher et un identifiant pour le LDAP.
Le premier peut avoir des accents, des majuscules/minuscules, mais n’est pas sensible à la casse, car c’est chiant d’avoir Eragon et eragon qui sont deux utilisateurs différents avec le même pseudo… (en plus d’avoir des conflits dans le LDAP)
Et l’identifiant LDAP lui est en minuscule, sans accents. Il est généré à partir du pseudo affiché

Ce qui doit donner, en gros pour un ajout d’utilisateur :

1. Entré utilisateur du pseudo visible
2. Vérification de sa validité, nombre de caractères, caractères et conflits de pseudo.
3. Traduction en minuscule sans accents.
4. Vérification dans le LDAP des conflits.
5. Ajout en bdd de l'utilisateur

PS : Pour les difficultés du LDAP avec l’Unicode, j’aimerais avoir l’avis de Breizh

Donc, on doit avoir un pseudo à afficher et un identifiant pour le LDAP. Le premier peut avoir des accents, des majuscules/minuscules, mais n'est pas sensible à la casse, car c'est chiant d'avoir `Eragon` et `eragon` qui sont deux utilisateurs différents avec le même pseudo... (en plus d'avoir des conflits dans le LDAP) Et l'identifiant LDAP lui est en minuscule, sans accents. Il est généré à partir du pseudo affiché Ce qui doit donner, en gros pour un ajout d'utilisateur : ``` 1. Entré utilisateur du pseudo visible 2. Vérification de sa validité, nombre de caractères, caractères et conflits de pseudo. 3. Traduction en minuscule sans accents. 4. Vérification dans le LDAP des conflits. 5. Ajout en bdd de l'utilisateur ``` PS : Pour les difficultés du LDAP avec l’Unicode, j'aimerais avoir l'avis de Breizh
Lephenixnoir commented 4 months ago
Owner

Oui, tu mentionnes un truc assez fourbe, c’est la possibilité d’avoir des noms d’utilisateur très proches sans raison. On peut citer majuscules/miniscules mais également des arnaques avec des zero-width spaces ou des homoglyphes en abusant d’unicode.

Je pense qu’une étape importante est de limiter les caractères Unicode autorisés à un ensemble choisi de blocs. Et de vérifier ensuite à la main qu’il n’y a pas d’homoglyphes (attention : potentiellement lourd en complexité algorithmique).

Pour le LDAP, tu fais la transformation, et si le pseudo est déjà utilisé tu rajoutes un nombre pour désambiguer (ie. eragon2 par exemple). Mais ça veut dire qu’il faut avoir vérifié avant que c’était pas un pseudo déjà utilisé ou très proche d’un pseudo déjà utilisé.

Oui, tu mentionnes un truc assez fourbe, c'est la possibilité d'avoir des noms d'utilisateur très proches sans raison. On peut citer majuscules/miniscules mais également des arnaques avec des zero-width spaces ou des homoglyphes en abusant d'unicode. Je pense qu'une étape importante est de **limiter les caractères Unicode autorisés à un ensemble choisi de blocs**. Et de vérifier ensuite à la main qu'il n'y a pas d'homoglyphes (attention : potentiellement lourd en complexité algorithmique). Pour le LDAP, tu fais la transformation, et si le pseudo est déjà utilisé tu rajoutes un nombre pour désambiguer (ie. `eragon2` par exemple). Mais ça veut dire qu'il faut avoir vérifié avant que c'était pas un pseudo déjà utilisé ou très proche d'un pseudo déjà utilisé.
Eragon commented 4 months ago
Collaborator

Justement, pour le LDAP, eragon2 ne devrait jamais arriver si le pseudo est proche(sauf si qqun à vraiment créé un compte eragon2) puisque Eragon(en bdd) existe déjà il va bloquer eragon,ERAGON,erag0n... donc les seuls username autorisés semblables seraient Eragon_fr,Eragondu56... mais pas un pseudo contenant uniquement eragon(ou un pseudo utilisant des homoglyphes pour y ressembler)

Justement, pour le LDAP, eragon2 ne devrait jamais arriver si le pseudo est proche(sauf si qqun à vraiment créé un compte eragon2) puisque `Eragon`(en bdd) existe déjà il va bloquer `eragon,ERAGON,erag0n...` donc les seuls username autorisés semblables seraient `Eragon_fr,Eragondu56...` mais pas un pseudo contenant uniquement eragon(ou un pseudo utilisant des homoglyphes pour y ressembler)
Darks commented 3 months ago
Owner

Pour le coup, je pensais plus à un système de ce type :

def normalize(username):
    return username.tolowercase() # en ajoutant le retrait des accents, etc.

# Pour savoir si le compte existe déjà
if normalize(new_user.username) is not available:
    print("Le pseudo est indisponible")

# Pour interroger un truc qui gère pas les pseudos étendus
ldap.getinfo(normalize(username))

Bon c’est du (pseudo)code d’exemple, mais ça a l’avantage d’être à priori simple à implémenter. Faut juste voir où qu’on fout la méthode normalize (dans User ?)

Concernant les blocs UTF-8, je suis d’accord, mais faut bien limiter.

Pour les homoglyphes basiques, j’ai une idée en tête, à base de dictionnaire d’homographes. À voir.

Pour le coup, je pensais plus à un système de ce type : ``` def normalize(username): return username.tolowercase() # en ajoutant le retrait des accents, etc. # Pour savoir si le compte existe déjà if normalize(new_user.username) is not available: print("Le pseudo est indisponible") # Pour interroger un truc qui gère pas les pseudos étendus ldap.getinfo(normalize(username)) ``` Bon c'est du (pseudo)code d'exemple, mais ça a l'avantage d'être à priori simple à implémenter. Faut juste voir où qu'on fout la méthode `normalize` (dans User ?) Concernant les blocs UTF-8, je suis d'accord, mais faut bien limiter. Pour les homoglyphes basiques, j'ai une idée en tête, à base de dictionnaire d'homographes. À voir.
Lephenixnoir commented 3 months ago
Owner

Ça me paraît une très bonne façon de faire, notamment si on peut prouver ce qu’il faut sur la fonction de normalisation alors le système est sécurisé. Ce serait une sorte de slug excepté que ça doit préserver plus de caractères et pas mettre des - partout, je suppose.

Ce qu’on peut faire aussi c’est refuser un pseudo qui mélange des caractères pris dans des blocs Unicodes (trop) différents. On n’a peut-être pas envie de le faire pour ne pas trop limiter les possibilités, mais c’est une option.

Comme toutes les choses touchant à Unicode, on a sans doute intérêt à se tourner vers des outils existants et éprouvés, si on le fait trop à la main on est presque certains de se planter.

Ça me paraît une très bonne façon de faire, notamment si on peut prouver ce qu'il faut sur la fonction de normalisation alors le système est sécurisé. Ce serait une sorte de slug excepté que ça doit préserver plus de caractères et pas mettre des `-` partout, je suppose. Ce qu'on peut faire aussi c'est refuser un pseudo qui mélange des caractères pris dans des blocs Unicodes (trop) différents. On n'a peut-être pas envie de le faire pour ne pas trop limiter les possibilités, mais c'est une option. Comme toutes les choses touchant à Unicode, on a sans doute intérêt à se tourner vers des outils existants et éprouvés, si on le fait trop à la main on est presque certains de se planter.
Eragon commented 3 months ago
Collaborator

J’abandonne cette issue, je ne me sent pas capable de faire un truc comme ça, lier un ldap avec le site avec une vérification poussé, j’ai même du mal à savoir ce qui est acceptable et ce qui doit être bloqué, je vous laisse faire ça, ça me semble trop critique de faire une bourde ici et comme j’y connais rien(dans ldap et le système d’auth) je vais éviter de faire cette bourde

J'abandonne cette issue, je ne me sent pas capable de faire un truc comme ça, lier un ldap avec le site avec une vérification poussé, j'ai même du mal à savoir ce qui est acceptable et ce qui doit être bloqué, je vous laisse faire ça, ça me semble trop critique de faire une bourde ici et comme j'y connais rien(dans ldap et le système d'auth) je vais éviter de faire cette bourde
Lephenixnoir commented 3 months ago
Owner

Là il n’est pas question du gérer le LDAP (on ne l’a pas encore lié à Flask), juste de spécifier les choses suivantes :

  1. Quels sont les blocs Unicode autorisés (si on restreint) ?
  2. Quels outils a-t-on pour mettre en forme normale ?
  3. Quel dictionnaire préétabli d’homoglyphes peut-on utiliser ?

Ensuite, coder ; mais pour l’instant je pense qu’il manque encore des détails pour pouvoir bien attaquer le code. :smiley:

Là il n'est pas question du gérer le LDAP (on ne l'a pas encore lié à Flask), juste de spécifier les choses suivantes : 1. Quels sont les blocs Unicode autorisés (si on restreint) ? 2. Quels outils a-t-on pour mettre en forme normale ? 3. Quel dictionnaire préétabli d'homoglyphes peut-on utiliser ? Ensuite, coder ; mais pour l'instant je pense qu'il manque encore des détails pour pouvoir bien attaquer le code. :smiley:
Darks commented 3 months ago
Owner

On commence par définir un ensemble réduit, abc…xyz012…79-_* grossomerdo, qui sera l’ensemble d’arrivée du pseudo normalisé. Pour la suite on regardera Bob-l-Épong3

Pour trouver la forme normalisée, on commence par faire un unicodedata.normalize qui permet de virer tout les accents (Bob-l-Epong3). On passe le résultat au lowercase (bob-l-epong3. Ensuite, on vérifie qu’il n’y a pas de caractères restant qui n’est pas contenu dans notre ensemble de départ. Si il y en a, on refuse l’entrée. Sinon on compare avec les homoglyphes.

Pour les homoglyphes, j’associe à chacun des caractères du set réduit une “origine” (provenant du même ensemble). Exemple : {'a': 'a', '4': 'a', 'i': 'i', 'l': 'i', '1': 'i', '3': 'e', …}

À partir de ça, on peut créer une fonction qui retourne une forme ultra-normalisée. (bob-i-eponge). On peut derrière comparer ces formes ultra normalisées entre elles.

Par contre ça veut dire soit récupérer l’ensemble des pseudos inscrits, les passer à la moulinette et regarder si ça match ; soit générer tout les homographes et regarder si y’en a pas un en bdd qui est déjà là ; soit stocker la forme ultra-normalisée à coté du pseudo normalisé ; soit un mix de tout ça à la fois, soit une dernière solution que je n’ai pas trouvée.

On commence par définir un ensemble réduit, `abc…xyz012…79-_*` grossomerdo, qui sera l'ensemble d'arrivée du pseudo normalisé. Pour la suite on regardera `Bob-l-Épong3` Pour trouver la forme normalisée, on commence par faire un [`unicodedata.normalize`](https://docs.python.org/3/library/unicodedata.html#unicodedata.normalize) qui permet de virer tout les accents (`Bob-l-Epong3`). On passe le résultat au lowercase (`bob-l-epong3`. Ensuite, on vérifie qu'il n'y a pas de caractères restant qui n'est pas contenu dans notre ensemble de départ. Si il y en a, on refuse l'entrée. Sinon on compare avec les homoglyphes. Pour les homoglyphes, j'associe à chacun des caractères du set réduit une "origine" (provenant du même ensemble). Exemple : `{'a': 'a', '4': 'a', 'i': 'i', 'l': 'i', '1': 'i', '3': 'e', …}` À partir de ça, on peut créer une fonction qui retourne une forme ultra-normalisée. (`bob-i-eponge`). On peut derrière comparer ces formes ultra normalisées entre elles. Par contre ça veut dire soit récupérer l'ensemble des pseudos inscrits, les passer à la moulinette et regarder si ça match ; soit générer tout les homographes et regarder si y'en a pas un en bdd qui est déjà là ; soit stocker la forme ultra-normalisée à coté du pseudo normalisé ; soit un mix de tout ça à la fois, soit une dernière solution que je n'ai pas trouvée.
Lephenixnoir commented 3 months ago
Owner

Hmm, c’est pas mal déjà.

Donc tu retires tous les scripts étrangers du l’ensemble d’arrivée ?

unicodedata.normalize ne supprime malheureusement pas les accents, il décompose une partie des caractères accentués en un diacritique et un caractère de base.

Je pense qu’une bonne façon de faire serait d’avoir le pseudo normalisé et le pseudo affiché (les deux) dans la base. On peut se servir du pseudo normalisé pour mentionner des gens dont le pseudo ne s’écrit pas en Latin, ou des choses comme ça.

Hmm, c'est pas mal déjà. Donc tu retires tous les scripts étrangers du l'ensemble d'arrivée ? `unicodedata.normalize` ne supprime malheureusement pas les accents, il décompose une partie des caractères accentués en un diacritique et un caractère de base. Je pense qu'une bonne façon de faire serait d'avoir le pseudo normalisé et le pseudo affiché (les deux) dans la base. On peut se servir du pseudo normalisé pour mentionner des gens dont le pseudo ne s'écrit pas en Latin, ou des choses comme ça.
Darks commented 3 months ago
Owner

Pour le coup, je serais du genre à autoriser assez peu de caractères. Genre l’alphabet latin (accentué), les chiffres et quelques caractères spéciaux. Disons qu’après ça peut vite devenir un trop gros bordel à gérer.

Pour les accents, en effet y’a d’autres astuces.

Et oui, je comptais enregistrer au moins le pseudo normalisé (qui sert d’identifiant unique facilement mémorisable, à la manière d’un slug) et l’autre, qui lui est affiché de manière fancy.

Par contre, ça donnerai J'ai vu avec :upseudonormalise … en Lightscript

Le gros problème que ça ne résout pas, c’est la détection des homographes (pour peu qu’on souhaite interdire autre chose que des pseudos qui ont la même normalisation).

Pour le coup, je serais du genre à autoriser assez peu de caractères. Genre l'alphabet latin (accentué), les chiffres et quelques caractères spéciaux. Disons qu'après ça peut vite devenir un trop gros bordel à gérer. Pour les accents, en effet y'a [d'autres astuces](https://stackoverflow.com/questions/517923/what-is-the-best-way-to-remove-accents-in-a-python-unicode-string). Et oui, je comptais enregistrer au moins le pseudo normalisé (qui sert d'identifiant unique facilement mémorisable, à la manière d'un slug) et l'autre, qui lui est affiché de manière fancy. Par contre, ça donnerai `J'ai vu avec :upseudonormalise …` en Lightscript Le gros problème que ça ne résout pas, c'est la détection des homographes (pour peu qu'on souhaite interdire autre chose que des pseudos qui ont la même normalisation).
Darks removed the
easy
label 3 months ago
Lephenixnoir commented 3 months ago
Owner

J’ai regardé le code d’Unidecode, ça fonctionne avec un ensemble de tables plus ou moins bien fait. Je vois beaucoup d’associations qui ne correspondent pas vraiment à ce qu’on voudrait (par exemple £PS, ou ¢C/, ou ¤$?).

Au risque de retourner ma veste à la vue de cet exemple et à l’idée que finalement assez peu de caractères seront autorisés, je propose de le faire à la main. Le protocole serait comme ceci :

  1. Sélectionner les régions autorisées. Ce seront probablement des sous-ensembles des blocs parce que dans Latin-1 Supplement, le premier bloc non trivial que l’on veut autoriser, il y a plein de caractères non textuels dont on ne veut pas vraiment comme des code points non assignés ou la cédille ¸.

  2. Indiquer à la main le caractère associé pour chaque caractère supporté.

Entre nous, je pense que ça peut être plus facile qu’on ne l’anticipe. J’ai dessiné 1000 caractères Unicode en 5x7 en trois soirées, je pense qu’on peut (que je peux) facilement faire ça pour tous les caractères accentués Latin communs et le reste de leurs blocs.

J'ai regardé le code d'Unidecode, ça fonctionne avec un ensemble de tables plus ou moins bien fait. Je vois beaucoup d'associations qui ne correspondent pas vraiment à ce qu'on voudrait (par exemple `£` → `PS`, ou `¢` → `C/`, ou `¤` → `$?`). Au risque de retourner ma veste à la vue de cet exemple et à l'idée que finalement assez peu de caractères seront autorisés, je propose de le faire à la main. Le protocole serait comme ceci : 1. Sélectionner les régions autorisées. Ce seront probablement des sous-ensembles des blocs parce que dans Latin-1 Supplement, le premier bloc non trivial que l'on veut autoriser, il y a plein de caractères non textuels dont on ne veut pas vraiment comme des code points non assignés ou la cédille `¸`. 2. Indiquer à la main le caractère associé pour chaque caractère supporté. Entre nous, je pense que ça peut être plus facile qu'on ne l'anticipe. J'ai dessiné 1000 caractères Unicode en 5x7 en trois soirées, je pense qu'on peut (que je peux) facilement faire ça pour tous les caractères accentués Latin communs et le reste de leurs blocs.
Lephenixnoir commented 3 months ago
Owner

Voilà une petite idée de ce qu’on peut faire en 18 minutes.

pseudo_map = [
    (0x0020, 0x007f,
        "             -. "
        "0123456789      "
        " abcdefghijklmno"
        "pqrstuvwxyz    _"
        " abcdefghijklmno"
        "pqrstuvwxyz   - "),
    (0x00c0, 0x00ff,
        "aaaaaaaceeeeiiii"
        "dnooooo ouuuuy b"
        "aaaaaaaceeeeiiii"
        "dnooooo ouuuuy y"),
    (0x0100, 0x17f,
        "aaaaaaccccccccdd"
        "ddeeeeeeeeeegggg"
        "gggghhhhiiiiiiii"
        "iiiijjkkklllllll"
        "lllnnnnnnnnnoooo"
        "oooorrrrrrssssss"
        "ssttttttuuuuuuuu"
        "uuuuwwyyyzzzzzz "),
]

def transliterate_char(c):
    n = ord(c)

    for (start, end, codes) in pseudo_map:
        if not (start <= n <= end): continue

        r = codes[n - start]
        if r == ' ':
            raise ValueError(c)
        return r

def normalize(string):
    return ''.join(transliterate_char(c) for c in string)
>>> import unicode_names
>>> unicode_names.normalize("Lephénixnoir")
'lephenixnoir'
>>> unicode_names.normalize("D@rks")
Traceback (most recent call last):
  (...)
ValueError: @
Voilà une petite idée de ce qu'on peut faire en 18 minutes. ```python pseudo_map = [ (0x0020, 0x007f, " -. " "0123456789 " " abcdefghijklmno" "pqrstuvwxyz _" " abcdefghijklmno" "pqrstuvwxyz - "), (0x00c0, 0x00ff, "aaaaaaaceeeeiiii" "dnooooo ouuuuy b" "aaaaaaaceeeeiiii" "dnooooo ouuuuy y"), (0x0100, 0x17f, "aaaaaaccccccccdd" "ddeeeeeeeeeegggg" "gggghhhhiiiiiiii" "iiiijjkkklllllll" "lllnnnnnnnnnoooo" "oooorrrrrrssssss" "ssttttttuuuuuuuu" "uuuuwwyyyzzzzzz "), ] def transliterate_char(c): n = ord(c) for (start, end, codes) in pseudo_map: if not (start <= n <= end): continue r = codes[n - start] if r == ' ': raise ValueError(c) return r def normalize(string): return ''.join(transliterate_char(c) for c in string) ``` ```python >>> import unicode_names >>> unicode_names.normalize("Lephénixnoir") 'lephenixnoir' >>> unicode_names.normalize("D@rks") Traceback (most recent call last): (...) ValueError: @ ```
Eragon commented 3 months ago
Collaborator

:sob: Je ne suis plus dans la discussion, j’ai du mal à suivre ça, il faudrait que je me prenne un moment pour lire tout ce que vous avez dit et aller lire de la doc sur toutes les fonctions possibles pour faire ça

:sob: Je ne suis plus dans la discussion, j'ai du mal à suivre ça, il faudrait que je me prenne un moment pour lire tout ce que vous avez dit et aller lire de la doc sur toutes les fonctions possibles pour faire ça
Darks commented 3 months ago
Owner

Propre. Y’a plus qu’à faire passer ça dans une moulinette de tests unitaires et ça part en prod (dans /app/utils/ ?).

Reste la question des homographes, voir si on veut vraiment les empêcher ou non.

Propre. Y'a plus qu'à faire passer ça dans une moulinette de tests unitaires et ça part en prod (dans `/app/utils/` ?). Reste la question des homographes, voir si on veut vraiment les empêcher ou non.
Lephenixnoir commented 3 months ago
Owner

Reste la question des homographes, voir si on veut vraiment les empêcher ou non.

Eh bien justement, l’intérêt de sélectionner les blocs à la main c’est qu’on peut lister les homographes à la main. :grinning:

Ici, il n’y a aucun caractère susceptible de ressembler fortement à un autre mais qui n’ait pas la même normalization. Donc, s’il y a des homographes ou quasi-homographes alors il y aura un conflit de pseudo normalisé.

J’ai écrit ça dans app/utils, je regarde ce qu’il peut y avoir comme autre blocs pertinents et je pousse ça ce soir.

> Reste la question des homographes, voir si on veut vraiment les empêcher ou non. Eh bien justement, l'intérêt de sélectionner les blocs à la main c'est qu'on peut lister les homographes à la main. :grinning: Ici, il n'y a aucun caractère susceptible de ressembler fortement à un autre mais qui n'ait pas la même normalization. Donc, s'il y a des homographes ou quasi-homographes alors il y aura un conflit de pseudo normalisé. J'ai écrit ça dans `app/utils`, je regarde ce qu'il peut y avoir comme autre blocs pertinents et je pousse ça ce soir.
Lephenixnoir commented 3 months ago
Owner

Après être tombés d’accord que les blocs de Latin mentionnés plus haut suffisaient, on a décidé que le script actuel va bien. Je l’ai poussé dans e5ff934c4a (app/utils/unicode_names.py).

Il reste maintenant à modifier la procédure d’inscription et à stocker le pseudo normalisé en bdd. Ça servira à pouvoir désigner un utilisateur de façon large, à accents/majuscules près.

Après être tombés d'accord que les blocs de Latin mentionnés plus haut suffisaient, on a décidé que le script actuel va bien. Je l'ai poussé dans e5ff934c4a (`app/utils/unicode_names.py`). Il reste maintenant à modifier la procédure d'inscription et à stocker le pseudo normalisé en bdd. Ça servira à pouvoir désigner un utilisateur de façon large, à accents/majuscules près.
Lephenixnoir commented 3 months ago
Owner

Implémenté dans 8570b8660f. Le pseudo normalisé est désormais calculé automatiquement, inséré en bdd, et doit pouvoir être unique.

On ne peut donc pas avoir à la fois lephe et Lephé dans les utilisateurs, car les deux ont le même pseudo normalisé lephe.

Le nom normalisé sera également utilisé comme clé dans le LDAP.

Implémenté dans 8570b8660f. Le pseudo normalisé est désormais calculé automatiquement, inséré en bdd, et doit pouvoir être unique. On ne peut donc pas avoir à la fois `lephe` et `Lephé` dans les utilisateurs, car les deux ont le même pseudo normalisé `lephe`. Le nom normalisé sera également utilisé comme clé dans le LDAP.
Sign in to join this conversation.
No Milestone
No Assignees
3 Participants
Due Date

No due date set.

Dependencies

This issue currently doesn't have any dependencies.

Loading…
Cancel
Save
There is no content yet.