Automatisons la création des comptes Gitlab avec une CI Gitlab !
dernière mise à jour : 18/07/2024
Actuellement, notre gitlab est un gitlab de taille moyenne, avec authentification LDAP. Il est possible de créer des utilisateurs en dehors du LDAP, mais c'est une action manuelle sur laquelle seul un administrateur a la main.
Comment créer des comptes sans être admin ? Actuellement, ce n'est pas possible, sauf ...
Et si nous essayons de réfléchir à une CI basée sur un token admin qui utiliserait l'API gitlab !?
La documentation de l'API gitlab à ce sujet est ici.
Les utilisateurs déjà existants feraient un commit pour rajouter leurs collaborateurs, avec pour chacun, une ligne contenant les champs requis : email, name et username.
On rajoutera les champs external à true
et reset_password
, ainsi l'utilisateur recevra un lien pour définir son mot de passe directement avec le mail fourni.
Premières étapes
- on crée un dépôt "create user" sur lequel seuls les utilisateurs internes ont accès,
- on crée un token en tant qu'administrateur avec les droits
api
etadmin_mode
(1), - on copie le token et on le met dans un fichier
admin_token.txt
, - on crée un gitlab-runner associé à ce dépôt (2),
- on crée une variable gitlab contenant le token admin attaché au projet
(
Paramètres
>CI/CD
>Variables
>Ajouter une variable
(cocherMasquer la variable
).
Commençons maintenant à faire quelques requêtes curl pour tester l'API.
On commence par la simple requête pour lister les projets visibles depuis l'extérieur, sans token :
curl -X GET "https://<GITLAB_URL>/api/v4/projects" | jq .
Maintenant, listons les utilsateurs actifs :
curl -s --header "Authorization: Bearer $(cat admin_token.txt)" -X GET "https://<GITLAB_URL>/api/v4/users?active=true" | jq .
Créons un utilisateur de test avec un mail créé pour l'occasion :
curl -s -H "Content-type: application/json" -H "Authorization: Bearer $(cat admin_token.txt)" -X POST --data '{"name": "toto", "username": "toto", "email": "toto@foobar.test.fr", "external": "true", "reset_password": "true"}' "https://<GITLAB_URL>/api/v4/users"
Ok; ça fonctionne. Je supprime l'utilisateur de test, puis on va pouvoir commencer à faire les choses de manière un peu plus propre.
Création du script python associé
Le fonctionnement du dépôt sera le suivant : je rajoute des utilisateurs développeurs qui pourront faire des commits sur la branche main
; ces derniers éditent le fichier users_to_create.csv
et rajoutent une ligne avec le nom de la personne, son nom d'utilisateur et enfin son adresse email (les 3 champs obligatoires pour la création de compte).
users_to_create.csv
:
# utilisateurs à créer, au format:
# Mon nom, username, email@tld.com
John Doe, johntest, john.doe@test.fr
Ici, le script rajoutera donc John Doe, avec le username johntest
et lui enverra un mail à john.doe@test.fr
pour intialiser son compte et son mot de passe.
Mon script python lira le fichier et récupèrera uniquement la dernière ligne (si ce n'est pas un commentaire), validera les champs, puis enverra la requête à l'API Gitlab.
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
import requests
import argparse
import re
import json
def main():
parser = argparse.ArgumentParser()
parser.add_argument("token", type=str,
help="Admin token value")
args = parser.parse_args()
token = args.token
API_URL = "https://<GITLAB_URL>/api/v4/users"
user = read_lastline("users_to_create.csv")
user = sanitized_user(user)
response = send_notif(API_URL, user, token)
print(response)
def read_lastline(file):
"""
:param a csv file to read
:return user array from last line of the file
"""
with open(file, "r", encoding="utf-8", errors="ignore") as scraped:
final_line = scraped.readlines()[-1]
if final_line.startswith("#"):
raise ValueError("Last line seems to be a comment")
user = final_line.split(",")
return user
def sanitized_user(user):
"""
:param user to clean
:return user sanitized value
"""
suser = []
for key,val in enumerate(user):
if key < 3:
val = val.strip()
if key == 2:
val = validate_email(val)
val = strip_dquotes(val)
suser.append(val)
if len(suser) == 3:
return suser
raise ValueError('Something went wrong while trying to read your CSV last line...')
def send_notif(API_URL, user, token):
"""
:params str message: body of the message (there is no subject in messages)
"""
name = user[0]
username = user[1]
email = user[2]
token_str = "Bearer {}".format(token)
headers = {"Authorization": token_str}
values = {"name": name, "username": username, "email": email, "external": "true", "reset_password": "true"}
response = requests.post(API_URL, headers=headers, data=values)
return response
def strip_dquotes(s):
"""
If a string has single or double quotes around it, remove them.
Make sure the pair of quotes match.
If a matching pair of quotes is not found,
or there are less than 2 characters, return the string unchanged.
"""
if (len(s) >= 2 and s[0] == s[-1]) and s.startswith(("'", '"')):
return s[1:-1]
return s
def validate_email(email):
"""
Basic email check with regexp
param emails
return valid emails
"""
regex = re.compile(r"([-!#-'*+/-9=?A-Z^-~]+(\.[-!#-'*+/-9=?A-Z^-~]+)*|\"([]!#-[^-~ \t]|(\\[\t -~]))+\")@([-!#-'*+/-9=?A-Z^-~]+(\.[-!#-'*+/-9=?A-Z^-~]+)*|\[[\t -Z^-~]*])")
if not re.fullmatch(regex, email):
raise ValueError("Email does not match a valid email format")
return email
if __name__ == "__main__":
main()
Le script se décompose ainsi :
- on récupère le token admin en variable,
- on lit la dernière ligne du fichier
users_to_create.csv
(read_lastline()
), - on vérifie un peu les champs (
sanitized_user()
+validate_email()
) + on les nettoie (strip_dquotes()
), - on envoie la requête à l'API Gitlab (
send_notif()
)
Contrairement à mes 1ers tests, il faut penser à enlever "Content": "application/json"
des Headers. La méthode requests.post()
s'en chargera pour nous.
On teste :
python3 create_user.py
Ok, ça fonctionne ! On supprime johntest
, puis ne reste plus qu'à mettre en place la CI.
Pour la CI, on lancera nos essais dans un conteneur docker python3.12
.
La CI se déclenchera sur le mot useradd
ou adduser
(toute allusion à la création des comptes Linux étant purement fortuite !) :
.gitlab-ci.yml
image: python:3.12-bookworm
services:
- docker:20.10.16-dind
variables:
TOKEN: $ADMIN_TOKEN
workflow:
rules:
- if: ( $CI_COMMIT_TITLE =~ /adduser/ || $CI_COMMIT_TITLE =~ /useradd/ ) && $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH
stages:
- test
- deploy
before_script:
- python3 -m venv createusers_venv
- source createusers_venv/bin/activate
- python3 -m pip install requests
check_env:
stage: test
timeout: 60 seconds
script:
- source createusers_venv/bin/activate
- python3 -c "import requests"
create_user:
stage: deploy
timeout: 5 minutes
script:
- source createusers_venv/bin/activate
- python3 create_user.py "${TOKEN}"
On constate que la CI se fait en 2 phases + celle de création de l'environnement qui s'exécutera dans chacune des phases :
- on crée l'environnement dans un venv,
- on teste l'import d'une librairie non présente par défaut dans l'image docker,
- on exécute le script.
On teste en faisant un commit, et miracle, tout fonctionne !
Pour plus de supervision sur ces actions, on rajoute une intégration sur ce dépôt. Ainsi, les commits seront visibles sur l'outil interne utilisé.
Personnellement, j'attends avec impatience l'intégration Matrix (3).
NB : il faut également pener à déprotéger la branche main, sinon, les développeurs ne seront pas à même de pousser vers la branche
main
, et des profils de nniveau supérieurs à développeurs sont en capacité de lire le token d'API utilisé (maintainer
,owner
). A noter toutefois qu'il faut qu'il y ait tout de même une certaine confiance envers ces développeurs, cf. : https://docs.gitlab.com/ee/user/project/repository/branches/default.html#protect-initial-default-branches . En effet, le.gitlab-ci.yml
reste assez sensible; sinon, il faudrait valider manuellement les merge requests et lancer l'action au merge.
Edit 18/07/2024 : j'ai codé l'intégration Matrix, ce qui me permet d'être alerté par Matrix lors des rajouts de compte sur le gitlab. Le code est également plus propre et plus complet, bien que pas optimal. Si certains sont intéressés, qu'il n'hésitent pas à me contacter.
Notes
A noter que si vous n'êtes pas en community edition, un service account serait certainement plus approprié...
2 On l'enregistre ainsi :
gitlab-runner register \
--non-interactive \
--url "https://<GITLAB_URL>/" \
--token "$RUNNER_TOKEN" \
--executor "docker" \
--docker-image python:3.12-bookworm \
--description "docker-runner CreateGitlabExtUsers"
3 Mais ça peut aussi se coder ! ... Et se rajouter dans la CI.