J’étais en train de bricoler du FastAPI pour m’amuser un peu sur mon temps libre, particulièrement avec l’authentification JWT. (https://en.wikipedia.org/wiki/JSON_Web_Token)

C’est sympa et formateur, mais bien que je fasse beaucoup de scripts, je doute un peu de mes capacités à construire un système d’authentification et de gestion de permission auquel je puisse avoir 100% confiance.

Et en vrai, tu ne devrais pas faire toi-même ton système de sécurité et d’authentification.

la cité de la peur : sécurité, je passe en premier

Si ton système est unique, il n’est pas autant éprouvé qu’une solution utilisée par des milliers de personnes :

  • Tu vas passer beaucoup de temps à dev et maintenir un système critique et réinventer la roue
  • Tu auras toujours un doute sur la sécurité de ton système
  • Tu auras toujours un temps de retard sur ce qui se fait de mieux dans le domaine

Si tu délègues ta sécurité :

  • Ce sera le problème de ton Identity Provider qui travaillera à la maintenance et l’évolution de ta sécurité
  • Tu dégageras du temps pour ce qui importe vraiment pour ton business, les fonctionnalités métiers de ton produit
  • Tu auras des moyens d’audit et d’authentification forte, sans effort

“L’homme et sa sécurité doivent constituer la première préoccupation de toute aventure technologique.” (Albert Einstein)

Une fois n’est pas coutume, on va regarder ce que nous propose GCP en service managé. Tu vas me dire gnagnagna vendors lock-in, bouh c’est pas bien. Pour les gens moins Tech qui ne connaissent pas, l’effet « lock-in » est la dépendance d’un client à un produit ou à une technologie.

Alors Monsieur ou Madame “lock-off”, si tu veux trainer un Keycloak, un ORY Hydra un auth0 ou un autre produit open source, grand bien t’en fasses, ça marche aussi, mais ce n’est pas le sujet d’aujourd’hui :)

Car aujourd’hui on essaye Firebase dans un contexte d’Identity Provider. https://en.wikipedia.org/wiki/Identity_provider

Red skull, Marvel agent du shield, jeu de mot, hail hydrate

Va donc faire ton VMware ou ton OpenStack quelque part ailleurs et arrête de me casser les pieds. Tu peux aussi payer un bras pour Okta ou une autre solution en SaaS, je ne juge pas, si tu as de gros sous à protéger et de gros sous à dépenser :)

Je pars sur l’idée que je n’ai pas du tout envie de trainer une brique à maintenir quelque part dans un cluster Kube ou un compute, que je n’ai pas de sous pour payer un produit en SaaS super cher.

Je veux de la auth dispo et de la auth performance (les fautes là, c’est exprès) et surtout pas maintenir du code custom quelque part ou un produit open source en Java qui me casse les pieds.

Une application prétexte

On va faire une API toute simple qui va nous servir de prétexte pour faire joujou avec de l’authentification.

Je suis radin comme tu le sais bien, donc on va faire ce qui coûte le moins cher possible, un CloudRun avec une API Gateway, Identity Platform/Firebase et un GCS pour persister mes données comme un gros porc en mode JSON chaussettes sales. En gros je prends un bucket GCS et je stocke mes données dedans au format JSON.

Tu peux juger, ce n’est probablement pas une bonne idée :)

C’est pas cher et je n’ai pas envie d’une base de données, je pourrai mettre un BigQuery, ou un CloudSQL, mais je suis curieux des performances que je peux obtenir avec cette idée. Et si vraiment j’étais dans un cas réel, il ne manquerait pas grand-chose pour passer sur une base NoSQL des familles ou migrer ailleurs.

Mais bon, on s’en balance, c’est juste un jouet, on fait du sale :)

émission YouTube, balek

Tous ces produits ont un free tier d’utilisation nous permettant de faire un POC gratuit ou presque, on ne va pas s’en priver.

On simule de cette façon un embryon d’application Serverless, comment ça il est moche mon bébé :)

Exploration des outils

On part sur une authentification basique de chez basique en mode email et mot de passe. Je garde la configuration avec les templates d’email par défaut et le serveur de mail de Google par défaut. C’est quand même bien cool de voir que sans rien faire on aura des emails templates pour la récupération et la vérification de mots de passe. On pourrait le faire nous-même, mais finalement est-ce que j’ai vraiment envie de le faire ?

Je suis tout seul, sur mon temps personnel, avec une vélocité de dev moyenne et j’ai jamais dev ce genre de système avant.

Identity Platform, email authentification

Hop encore une fois, je n’ai pas envie de lire la doc, ça tombe bien j’ai trouvé un snippet de code qui fait de l’auth et qui pourrait correspondre à mon besoin :)

https://www.padok.fr/en/blog/setup-application-gcp

Pour le moment, le fonctionnement est un peu Nebula pour moi.

Mavel Nebula, les gardiens de la galaxie

Ce que j’aimerai faire :

  • Avoir un point d’API pour enregistrer des utilisateurs dans Firebase
  • Fournir des token JWT à mes utilisateurs pour leur permettre d’utiliser l’API
  • Avoir des permissions customisées pour donner des permissions fines à mes utilisateurs
  • Utiliser les permissions pour lire et écrire des données propres à mes utilisateurs

Et surtout TUER THANOS, pardon je m’égare, hihi

Voyons voir où le script nous emmène

C’est parti pour essayer de faire fonctionner le bout de script glané sur le net. La seule petite difficulté que j’ai trouvée c’est où trouver les informations d’authentification du script.

On trouve ces informations dans la configuration de l’application dans le projet de la console Firebase. https://console.firebase.google.com/

Directement dans le snippet JavaScript, mais nous on va pas utiliser de Js parce que le front, ça ne nous intéresse pas cette fois-ci :)

firebase credentials snapshot

Il faudra veiller à exporter la clef du Service Account dans une variable d’environnement pour faire fonctionner le script. Et remplacer les infos de connexions à Firebase par celle de votre application.

1
2
3
4
5
6
7
Are you a new user? [y/n]n
Log in...
Enter email: user@coucou.com
Enter password: coucou
User authenticated
User ID :unuserid
JWT Token : eyregardecestuntokenjwt
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
curl --header "Authorization: Bearer eyregardecestuntokenjwt" "https://{api_gateway_url}/v1/hello" 
<!doctype html>
<html lang=en>
<head>
<meta charset=utf-8>
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Congratulations | Cloud Run</title>
<link href="https://fonts.googleapis.com/css?family=Roboto" rel="preload" as="font">
<link
[...]

Et donc là, on a une authentification qui fonctionne au strict minimum avec API Gateway, Firestore, Cloudrun. Sur un point d’api hello avec 1 utilisateur.

C’est un bon début :)

FastAPI Cloudrun

Bon maintenant qu’on a un bout de script pour enregistrer les utilisateurs et filer un token, c’est peut-être bien de faire rentrer ça dans un cloudrun, pour manager les utilisateurs via l’api directement.

De cette façon on donne un moyen d’enregistrer et d’authentifier les utilisateurs directement via FastAPI. Et on peut s’authentifier avec JWT sur tous les points d’accès API de la Gateway.

parce que c’est notre projet

code minimum fastAPI

À partir de maintenant il faut mettre en place de quoi créer des containers fastAPI cloudrun les pousser sur la Registry google et les déployer si possible avec une CI/CD.

Comme d’hab, radin malin, git hook, tequila, heineken pas le temps de niaser !

meme, tequila heineken, pas le temps de niaser

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
#!/bin/bash
set +x

function shameful_cicd_function(){
    git_short_sha="$(git rev-parse --short HEAD)"
    docker_image="europe-west1-docker.pkg.dev/onionland/blog/fastapi:$git_short_sha"
    pipenv install
    pipenv lock -r > requirements.txt
    gcloud builds submit --tag "$docker_image"
    export TF_VAR_cloudrun_fastapi_docker_image="$docker_image"
    cd terraform/cloudrun_fastapi/
    terraform init
    terraform apply --auto-approve
}

if [ -z "$(git status --porcelain)" ]; then
  # Working directory clean
  shameful_cicd_function  & disown
  echo ""
fi

exit 0

Rapidement, j’utilise gcloud builder pour builder mon image docker et pusher sur le registry de mon projet. Et ensuite je passe une variable Terraform pour déployer mon image Cloudrun avec Terraform.

En tentant de fusionner le code FastAPI et le script Firebase, je me rends compte que la dépendance pyrebase est vraiment relou et provoque des incompatibilités de dépendance avec la version de requests utilisé par FastAPI aussi.

elle: il doit penser à une autre femme, lui: pourquoi j’ai une boucle de dépendance infinie ?

Tout ça pour un pauvre wrapper d’API qui m’économise 4 lignes de code, mais qui m’importe quantité de code inutile et dépendances.

On fouille de code de pyrebase et on trouve ça.

https://github.com/thisbejim/Pyrebase/blob/master/pyrebase/pyrebase.py

1
2
3
4
5
6
request_ref = "https://www.googleapis.com/identitytoolkit/v3/relyingparty/verifyPassword?key={0}".format(
    self.api_key
)
headers = {"content-type": "application/json; charset=UTF-8"}
data = json.dumps({"email": email, "password": password, "returnSecureToken": True})
request_object = requests.post(request_ref, headers=headers, data=data)

On dégage le p’tit wrap, et on prend un big mac

détournement Starwars, it’s a wrap

FastAPI authentification + persistance GCS

Au final ça nous fait un truc comme ça

modèle : fichier app/model.py

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
from pydantic import BaseModel, Field, EmailStr
from typing import Optional


class UserSchema(BaseModel):
    fullname: str = Field(...)
    email: EmailStr = Field(...)
    password: str = Field(...)
    disabled: Optional[bool] = False
    hashed_password: Optional[str] = None

    class Config:
        schema_extra = {
            "example": {
                "fullname": "John Doe",
                "email": "[email protected]",
                "password": "password",
            }
        }


class UserLoginSchema(BaseModel):
    email: EmailStr = Field(...)
    password: str = Field(...)

    class Config:
        schema_extra = {
            "example": {"email": "[email protected]", "password": "weakpassword"}
        }


class UserChangePasswordSchema(BaseModel):
    email: EmailStr = Field(...)
    password: str = Field(...)
    new_password: str = Field(...)

    class Config:
        schema_extra = {
            "example": {
                "email": "[email protected]",
                "password": "password",
                "new_password": "new_password",
            }
        }


class BlogPostSchema(BaseModel):
    post: str = Field(...)
    title: str = Field(...)

    class Config:
        schema_extra = {
            "example": {
                "post": "hello there and welcome to my blog !",
                "title": "first post",
            }
        }

Rien de surprenant ou de magique, c’est juste des modèles pydantic tout simples pour gérer mes formulaires utilisateur et Blog. J’ai gardé juste le strict minimum au niveau des champs dans un but de simplicité.

fichier app/main.py

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
import time
import os
import sys
import json
import requests
import firebase_admin
from firebase_admin import auth

from fastapi import FastAPI, Body, Depends
from fastapi.exceptions import HTTPException
from fastapi.security.http import HTTPBearer, HTTPBasicCredentials

from app.model import (
    UserSchema,
    UserLoginSchema,
    UserChangePasswordSchema,
    BlogPostSchema,
)

from google.cloud import storage

client = storage.Client()

FIREBASE_API_KEY = os.getenv("FIREBASE_API_KEY")
if not FIREBASE_API_KEY:
    print("env var FIREBASE_API_KEY is missing.")
    sys.exit(-1)


GCS_DATA_BUCKET = os.getenv("GCS_DATA_BUCKET")
if not GCS_DATA_BUCKET:
    print("env var GCS_DATA_BUCKET is missing.")
    sys.exit(-1)

firebase_admin_app = firebase_admin.initialize_app()
USER_CLAIMS = {"profile": "user"}

bearer_auth = HTTPBearer()

app = FastAPI()


bucket = client.get_bucket(GCS_DATA_BUCKET)

@app.get("/")
def root():
    return {""}


@app.post("/user/signup", tags=["user"])
async def create_user(user: UserSchema = Body(...)):
    try:
        user = auth.create_user(email=user.email, password=user.password)
        custom_token = auth.create_custom_token(user.uid, USER_CLAIMS)
        return {"token": custom_token}
    except firebase_admin._auth_utils.EmailAlreadyExistsError as msg_error:
        raise HTTPException(status_code=409, detail=str(msg_error))
        return {"error": str(msg_error)}


@app.post("/user/polite_if_you_are_admin", tags=["user"])
async def polite_if_you_are_admin(token: HTTPBasicCredentials = Depends(bearer_auth)):
    response = await sign_in_with_custom_token(token)
    if response.get("error"):
        return response

    user = auth.verify_id_token(response["idToken"])
    if not user:
        return {"error": "JWT token is not valid"}

    if "admin" not in user["profile"]:
        return {"hey ! get the fuck out of here ! i'am gonna call the police !"}
    else:
        return {"welcome home, dear admin"}


async def get_user_info_from_jwt(token):
    response = await sign_in_with_custom_token(token)
    if response.get("error"):
        return response

    user = auth.verify_id_token(response["idToken"])
    if not user:
        return {"error": "JWT token is not valid"}
    return user


async def check_user_identity(email, password):
    request_ref = "https://www.googleapis.com/identitytoolkit/v3/relyingparty/verifyPassword?key={0}".format(
        FIREBASE_API_KEY
    )
    headers = {"content-type": "application/json; charset=UTF-8"}
    data = json.dumps({"email": email, "password": password, "returnSecureToken": True})
    request_object = requests.post(request_ref, headers=headers, data=data)
    if "20" not in str(request_object.status_code):
        time.sleep(3)
        msg_error = "wrong login or password."
        raise HTTPException(status_code=403, detail=msg_error)
        return {"error": msg_error}

    return request_object.json()


async def sign_in_with_custom_token(token):
    request_ref = "https://identitytoolkit.googleapis.com/v1/accounts:signInWithCustomToken?key={0}".format(
        FIREBASE_API_KEY
    )
    headers = {"content-type": "application/json; charset=UTF-8"}

    data = json.dumps({"token": token.credentials, "returnSecureToken": True})

    request_object = requests.post(request_ref, headers=headers, data=data)

    if "20" not in str(request_object.status_code):
        time.sleep(3)
        msg_error = "error during sign custom token, token expired ?"
        raise HTTPException(status_code=403, detail=msg_error)
        return {"error": msg_error}
    return request_object.json()


@app.post("/user/signin", tags=["user"])
async def login_user(user: UserLoginSchema = Body(...)):
    response = await check_user_identity(email=user.email, password=user.password)
    if response.get("error"):
        return response

    user = response
    custom_token = auth.create_custom_token(user["localId"], USER_CLAIMS)
    return {"token": custom_token}


@app.post("/user/changepassword", tags=["user"])
async def change_user_password(user: UserChangePasswordSchema = Body(...)):
    response = await check_user_identity(email=user.email, password=user.password)
    if response.get("error"):
        return response
    current_user = response

    auth.update_user(
        uid=current_user["localId"], email=user.email, password=user.new_password
    )

    response = await check_user_token(email=user.email, password=user.new_password)
    if response.get("error"):
        return {"error": "error during password change."}
    current_user = response

    custom_token = auth.create_custom_token(current_user["localId"], USER_CLAIMS)
    return {"token": custom_token}


@app.post("/blog/send_post", tags=["blog"])
async def blog_send_post(
    token: HTTPBasicCredentials = Depends(bearer_auth),
    blog_post: BlogPostSchema = Body(...),
):
    user = await get_user_info_from_jwt(token)
    if user.get("error", ""):
        return user

    post = {
        "user_id": user["user_id"],
        "email": user["email"],
        "post": blog_post.post,
        "title": blog_post.title,
    }
    await read_append_blob(post, "posts")
    return post


@app.post("/blog/get_user_posts", tags=["blog"])
async def blog_get_user_posts(
    token: HTTPBasicCredentials = Depends(bearer_auth),
    blog_post: BlogPostSchema = Body(...),
):
    user = await get_user_info_from_jwt(token)
    if user.get("error", ""):
        return user

    blog_posts = await read_blob("posts")
    return [post for post in blog_posts[0] if post['user_id'] in user["user_id"]]
    
async def read_append_blob(data, file_path):
    content, blob = await read_blob(file_path)
    if not content:
        blob = storage.Blob(file_path, bucket)

    content.append(data)
    blob.upload_from_string(json.dumps(content))


async def read_blob(file_path):
    content = []
    blob = bucket.get_blob(file_path)
    if blob:
        content = json.loads(blob.download_as_bytes())
    return content, blob

Donc on a un système minimum viable qui sait enregistrer des utilisateurs, enregistrer des articles de blog de façon rudimentaire, récupérer les articles de l’utilisateur authentifié. On peut aussi changer de mot de passe et retourner des tokens JWT utilisables sur l’API Gateway de GCP et sur notre FastAPI.

On peut gérer les permissions en ajoutant l’information dans le token comme fait dans le code, GCP ne le recommande pas, je dirais que par principe moi non plus (il vaut mieux stocker l’info en base) :)

Pour récupérer les infos utilisateurs depuis le token on doit faire appel à l’API sign_in_with_custom_token. On peut ensuite identifier l’utilisateur par son id et sa permission (ajouté dans le custom token).

J’ai aussi ajouté la persistance dans GCS dans un bucket zonal pour la latence, et je suis surpris par la vitesse de mon bricolage. C’est presque bien, à condition d’organiser les fichiers dans le bucket et de ne pas charger des blobs énormes. https://en.wikipedia.org/wiki/Binary_large_object

Je vous recommande quand même une base de données qui répond bien mieux au besoin, mais si vous voulez vous en passer c’est aussi possible comme ça :)

logo monsieur bricolage

Finalement on peut dégager la Gateway et la Cloud Function hello.

On a juste besoin du cloudrun, d’une image Docker et des variables d’env.

Le dockerFile

1
2
3
4
5
6
7
8
FROM python:3.9
EXPOSE 8080
WORKDIR /code
COPY Pipfile /code/Pipfile
COPY requirements.txt /code/requirements.txt
RUN pip install --no-cache-dir --upgrade -r /code/requirements.txt
COPY ./app /code/app
CMD ["uvicorn", "app.main:app", "--proxy-headers", "--host", "0.0.0.0", "--port", "8080"]

Le Terraform qui va bien, avec les permissions pour permettre d’écrire des token, utiliser l’API admin de Firebase et lire et écrire dans un bucket GCS.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
resource "google_cloud_run_service" "fastapi_cloudrun" {
  name     = "fastapi"
  location = "europe-west1"
  project =  var.project
  template {
    spec {
      containers {
        image = var.cloudrun_fastapi_docker_image
        env {
          name = "GCS_DATA_BUCKET"
          value = var.gcs_bucket
        }
        env {
          name = "FIREBASE_API_KEY"
          value = var.firebase_api_key
        }
      }
        service_account_name = google_service_account.service_account_fastapi.email
    }
  }
}

data "google_iam_policy" "noauth" {
  binding {
    role = "roles/run.invoker"
    members = [
      "allUsers",
    ]
  }
}

resource "google_cloud_run_service_iam_policy" "noauth" {
  location    = google_cloud_run_service.fastapi_cloudrun.location
  project     = google_cloud_run_service.fastapi_cloudrun.project
  service     = google_cloud_run_service.fastapi_cloudrun.name
  policy_data = data.google_iam_policy.noauth.policy_data
}

resource "google_service_account" "service_account_fastapi" {
  project = var.project
  account_id   = "sa-cloudrun-fastapi-firebase"
  display_name = "SA Cloudrun FastAPI Firebase"
}

resource "google_project_iam_binding" "firebase_sdk_admin" {
  project = var.project
  role    = "roles/firebase.sdkAdminServiceAgent"
  members = ["serviceAccount:${google_service_account.service_account_fastapi.email}"]
}

resource "google_project_iam_binding" "firebase_storage_service_agent" {
  project = var.project
  role    = "roles/firebasestorage.serviceAgent"
  members = ["serviceAccount:${google_service_account.service_account_fastapi.email}"]
}
resource "google_project_iam_binding" "token_creator" {
  project = var.project
  role    = "roles/iam.serviceAccountTokenCreator"
  members = ["serviceAccount:${google_service_account.service_account_fastapi.email}"]
}

Rien de compliqué ou de magique, j’ai mis des variables pour l’image Docker, le tokens Firebase et le bucket GCS. On pourrait utiliser des secrets Cloudrun pour faire plus propre, mais comme on fait du rapidos crados j’ai mis de côté cette idée :)

Backup et Réversibilité

Bon maintenant qu’on à un système qui fonctionne, On va faire en sorte de dumper la base d’utilisateurs Firebase vers un bucket tous les soirs.

En cas de destruction accidentelle et/ou de besoin de réversibilité. J’ai dit qu’on s’en foutait, mais c’est quand même important d’y penser si vous l’utilisez sur un vrai produit. Au moins, pouvoir se garder l’option si Firebase venait à disparaitre ou que vous voulez bouger de GCP.

un Cronjob dans Cloudrun avec Cloud Scheduler devrait faire le taf :)

On tente un dump local pour commencer https://firebase.google.com/docs/cli/auth

installation du cli

curl -sL https://firebase.tools | bash

J’avais pour idée de prendre le même Service Account que le Cloudrun FastAPI. Comme il faut une permission Editor pour avoir le droit de backup des passwords hash, on va refaire un Service Account juste pour les backups avec la permission super pétée, comme captain Marvel.

Il est un peu dangereux d’exposer un service web avec un Service Account avec un des droits les plus forts sur GCP :) Il faudra aussi veiller à restreindre au maximum l’accès au bucket GCS qui contiendra les backups pour éviter une fuite de données ou une utilisation malveillante du Service Account.

meme école indienne, cours d’anglais, dangerous

Petite subtilité un peu relou, il faut créer le Service Account à la main, comme on utilise des permissions pétées qui normalement s’appliquent uniquement aux utilisateurs humains admin.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10

 Error: Request `Set IAM Binding for role "roles/owner" on "project \"onionland\""` returned error: Error applying IAM policy for project "onionland": Error setting IAM policy for project "onionland": googleapi: Error 400: Request contains an invalid argument.
 Details:
 [
   {
     "@type": "type.googleapis.com/google.cloudresourcemanager.v1.ProjectIamPolicyError",
     "role": "roles/owner",
     "type": "SOLO_REQUIRE_TOS_ACCEPTOR"
   }
 ]

Ici le script qu’on met dans un cloudrun, qui fera les backups Firebase vers un bucket sécurisé. Il faudra brancher un cloud scheduler pour déclencher les backups à un interval régulier.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
import os
import sys
from flask import Flask

from google.cloud import storage
from datetime import datetime

client = storage.Client()

app = Flask(__name__)
GCS_BACKUP_BUCKET=os.getenv("GCS_BACKUP_BUCKET")
PROJECT_NAME=os.getenv("PROJECT_NAME")

if not GCS_BACKUP_BUCKET:
    print("ERROR: env GCS_BACKUP_BUCKET is missing")
    sys.exit(-1)

if not PROJECT_NAME:
    print("ERROR: env PROJECT_NAME is missing")
    sys.exit(-1)

bucket = client.get_bucket(GCS_BACKUP_BUCKET)

@app.route("/")
def backup_firebase():
    filename="export_firebase-{}.json".format(datetime.now().strftime("%Y_%m_%d-%I_%M_%S_%p"))
    tmp_file="/tmp/" + filename
    os.system("firebase auth:export {} --project={} --format=json".format(tmp_file, PROJECT_NAME))
    blob = storage.Blob(filename, bucket)
    blob.upload_from_filename(tmp_file)
    return ''

if __name__ == "__main__":
    app.run(debug=True, host="0.0.0.0", port=int(os.environ.get("PORT", 8080)))

Le Dockerfile ici, pas vraiment de trucs magiques non plus :)

Rien dans les poches, rien dans le git, prestidigitation !

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
FROM python:3.10-slim
ENV PYTHONUNBUFFERED True
ENV APP_HOME /app
WORKDIR $APP_HOME
COPY . ./
RUN apt update
RUN apt upgrade -y
RUN apt install -y curl sudo
RUN curl -sL https://firebase.tools|bash
RUN pip install --no-cache-dir -r requirements.txt
CMD exec gunicorn --bind :$PORT --workers 1 --threads 8 --timeout 0 main:app

Et du coup pour importer dans l’autre sens il suffit de récupérer le fichier de backup et lancer la commande d’import comme dans la doc.

Conclusion

Globalement Firebase fonctionne plutôt bien pour mon petit bricolage, mais je pense que ça ne suffit pas si on a des besoins beaucoup plus avancés. Néanmoins c’est très pratique si on part sur une architecture Serverless et qu’on ne veut pas gérer seul l’authentification. C’est plutôt rapide, mais pas complètement sans douleur, je ne peux pas dire que je n’ai pas un peu galéré ! Par contre j’ai grandement réduit le code lié à l’authentification et la gestion de mes utilisateurs, en comparaison avec la solution incluse dans FastAPI.

C’est vraiment bien, parce que je m’inquiète moins niveau sécurité et bugs, et le stockage des identifiants de mes utilisateurs.

Quelques possibilités d’amélioration de cette solution :

  • Meilleure gestion et révocation de la validité des tokens.
  • Meilleure gestion des permissions
  • Mettre en place un frontend sympa
  • Mettre en place un backend admin
  • Distribuer le monolithe FastAPI en microservice

Bien sûr, le code est loin d’être impeccable et prod ready, mais vous avez l’idée générale de comment intégrer Firebase auth dans FastAPI. Avec un peu plus de soins et des tests, je pense que c’est une solution sérieuse et envisageable en production. Le backup et l’import des données sont plutôt simples, mais je déplore le besoin d’être Editor pour avoir le backup complet des Utilisateurs. Je pense que Google pourrait faire un effort pour restreindre les droits nécessaires pour cette action.

J’imagine que comme Firebase n’est pas un produit GCP natif, Google doit faire un effort d’assimilation du produit avant de pouvoir envisager de faire des changements profonds. On peut s’en rendre compte parfois, quand on a des erreurs un peu vagues et moins standard que les produits Google natifs. https://en.wikipedia.org/wiki/Firebase

Je vous laisse donc matière à réflexion et je vous donne rendez-vous bientôt pour une prochaine connerie :)

ici le code fastAPI, Firebase, GCS , déposé comme une machine à laver en fin de vie dans une décharge publique.

https://github.com/helldrum/fastapi-firebase

roi loth Kaamelott, et là normalement il faut une citation latine, mais pff, j’en ai marre