diff --git a/README.md b/README.md
index 162166cd..edeedbaf 100644
--- a/README.md
+++ b/README.md
@@ -6,7 +6,7 @@
Pour commencer :
-## [Docker](docker/DOCKER.md)
+## [Docker](ops/cours/OPS_COURS.md)
Pour l'ensemble des parties du cours
diff --git a/api/.env b/api/.env
index 220539aa..6e5bfc7b 100644
--- a/api/.env
+++ b/api/.env
@@ -1,3 +1,7 @@
POSTGRES_USER=userpd
POSTGRES_PASSWORD=postgrespassword
POSTGRES_DB=dbesiee
+
+PGADMIN_DEFAULT_EMAIL=morgan.courivaud@esiee.fr
+PGADMIN_DEFAULT_PASSWORD=pgadminpassword
+PGADMIN_LISTEN_PORT=9001
diff --git a/api/Dockerfile b/api/Dockerfile
index 38ae8716..7f95badc 100644
--- a/api/Dockerfile
+++ b/api/Dockerfile
@@ -3,5 +3,6 @@ FROM tiangolo/uvicorn-gunicorn-fastapi:python3.7
ADD requirements.txt .
RUN pip install -r requirements.txt
+WORKDIR /app
COPY ./app /app/app
diff --git a/api/app/main.py b/api/app/main.py
index 25ab717d..1a2149b9 100644
--- a/api/app/main.py
+++ b/api/app/main.py
@@ -1,12 +1,15 @@
+import base64
from typing import Optional
-from fastapi import FastAPI
+from fastapi import FastAPI, Header
from fastapi.middleware.cors import CORSMiddleware
import json
import asyncio
import os
+
+from starlette.requests import Request
from starlette_exporter import PrometheusMiddleware, handle_metrics
-from .models import BaseSQL, engine
-from . import routers
+from models import BaseSQL, engine
+import routers
app = FastAPI(
title="My title",
@@ -28,7 +31,7 @@
)
app.include_router(routers.PostRouter)
-app.include_router(routers.HealthRouter)
+#app.include_router(routers.HealthRouter)
app.add_middleware(PrometheusMiddleware)
app.add_route("/metrics", handle_metrics)
@@ -38,10 +41,16 @@
async def startup_event():
BaseSQL.metadata.create_all(bind=engine)
+
@app.get("/api/headers")
-def read_hello(request: Request, x_userinfo: Optional[str] = Header(None, convert_underscores=True), ):
+def read_hello(
+ request: Request,
+ x_userinfo: Optional[str] = Header(None, convert_underscores=True),
+):
print(request["headers"])
- return {"Headers": json.loads(base64.b64decode(x_userinfo))}
+ b64 = base64.b64decode(x_userinfo.encode("utf-8"))
+ return {"Headers": request["headers"]}
+
@app.get("/")
def read_root():
diff --git a/api/app/models/__init__.py b/api/app/models/__init__.py
index 1c268fd3..5e154a90 100644
--- a/api/app/models/__init__.py
+++ b/api/app/models/__init__.py
@@ -1,3 +1,3 @@
from .post import Post
from .database import BaseSQL
-from .db import get_db, engine
\ No newline at end of file
+from .db import get_db, engine
diff --git a/api/app/models/database.py b/api/app/models/database.py
index 7d39e2dd..690701c7 100644
--- a/api/app/models/database.py
+++ b/api/app/models/database.py
@@ -1,7 +1,7 @@
from sqlalchemy import create_engine
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.orm import sessionmaker
-import os
+import os
POSTGRES_USER = os.environ.get("POSTGRES_USER")
@@ -9,12 +9,12 @@
POSTGRES_DB = os.environ.get("POSTGRES_DB")
-SQLALCHEMY_DATABASE_URL = f"postgresql://{POSTGRES_USER}:{POSTGRES_PASSWORD}@postgres/{POSTGRES_DB}"
+SQLALCHEMY_DATABASE_URL = (
+ f"postgresql://{POSTGRES_USER}:{POSTGRES_PASSWORD}@db/{POSTGRES_DB}"
+)
print(SQLALCHEMY_DATABASE_URL)
-engine = create_engine(
- SQLALCHEMY_DATABASE_URL
-)
+engine = create_engine(SQLALCHEMY_DATABASE_URL)
SessionLocal = sessionmaker(autocommit=False, autoflush=True, bind=engine)
BaseSQL = declarative_base()
diff --git a/api/app/models/db.py b/api/app/models/db.py
index 7d907548..7b6d13d2 100644
--- a/api/app/models/db.py
+++ b/api/app/models/db.py
@@ -6,4 +6,4 @@ def get_db():
db = SessionLocal()
yield db
finally:
- db.close()
\ No newline at end of file
+ db.close()
diff --git a/api/app/routers/__init__.py b/api/app/routers/__init__.py
index 5dab8b48..93ea2d4d 100644
--- a/api/app/routers/__init__.py
+++ b/api/app/routers/__init__.py
@@ -1,2 +1,2 @@
from .posts import router as PostRouter
-from .health import router as HealthRouter
\ No newline at end of file
+from .health import router as HealthRouter
diff --git a/api/app/routers/health.py b/api/app/routers/health.py
index 3b042fdf..6cfd7c96 100644
--- a/api/app/routers/health.py
+++ b/api/app/routers/health.py
@@ -22,6 +22,8 @@ def read_root():
@router.get("/api/headers")
-def read_hello(request: Request, x_userinfo: Optional[str] = Header(None, convert_underscores=True)):
+def read_hello(
+ request: Request, x_userinfo: Optional[str] = Header(None, convert_underscores=True)
+):
print(request["headers"])
return {"Headers": json.loads(base64.b64decode(x_userinfo))}
diff --git a/api/app/routers/posts.py b/api/app/routers/posts.py
index 668b2437..66d4210b 100644
--- a/api/app/routers/posts.py
+++ b/api/app/routers/posts.py
@@ -1,6 +1,6 @@
from fastapi import APIRouter, Depends
-from ..services import posts as posts_service
-from .. import schemas, models
+from services import posts as posts_service
+import schemas, models
from sqlalchemy.orm import Session
router = APIRouter(prefix="/posts")
@@ -25,8 +25,9 @@ async def get_posts_by_title(title: str = None, db: Session = Depends(models.get
@router.put("/{post_id}", tags=["posts"])
-async def update_post_by_id(post_id: str, post: schemas.Post,
- db: Session = Depends(models.get_db)):
+async def update_post_by_id(
+ post_id: str, post: schemas.Post, db: Session = Depends(models.get_db)
+):
return posts_service.update_post(post_id=post_id, db=db, post=post)
diff --git a/api/app/services/posts.py b/api/app/services/posts.py
index 76524f40..02965e88 100644
--- a/api/app/services/posts.py
+++ b/api/app/services/posts.py
@@ -3,7 +3,7 @@
from sqlalchemy.orm import Session
from fastapi import HTTPException
from datetime import datetime
-from .. import models, schemas
+import models, schemas
def get_all_posts(db: Session, skip: int = 0, limit: int = 10) -> List[models.Post]:
@@ -16,7 +16,7 @@ def get_all_posts(db: Session, skip: int = 0, limit: int = 10) -> List[models.Po
def get_post_by_id(post_id: str, db: Session) -> models.Post:
record = db.query(models.Post).filter(models.Post.id == post_id).first()
if not record:
- raise HTTPException(status_code=404, detail="Not Found")
+ raise HTTPException(status_code=404, detail="Not Found")
record.id = str(record.id)
return record
diff --git a/api/tp/predict/app/main.py b/api/tp/predict/app/main.py
index 99245b45..97e30910 100644
--- a/api/tp/predict/app/main.py
+++ b/api/tp/predict/app/main.py
@@ -1,6 +1,6 @@
from fastapi import FastAPI, BackgroundTasks
import pathlib
-import glob
+import glob
from joblib import load, dump
from schema import IrisPredict, IrisTrain
import numpy as np
@@ -14,16 +14,17 @@
version="0.0.1",
)
-global clf
+global clf
+
+CLASSES = ["setosa", "versicolor", "virginica"]
-CLASSES = ['setosa', 'versicolor', 'virginica']
@app.get("/")
def read_root():
return {"Hello": "World"}
-@app.on_event('startup')
+@app.on_event("startup")
async def load_model():
global clf
all_model_paths = glob.glob("./data/iris_*")
@@ -34,11 +35,12 @@ async def load_model():
@app.post("/iris/predict")
async def predict_iris(iris: IrisPredict):
return {
- "predicted_classes" : clf.predict(np.asarray([iris.data])).tolist(),
- "predicted_probas" : clf.predict_proba(np.asarray([iris.data])).tolist(),
- "classes" : CLASSES
+ "predicted_classes": clf.predict(np.asarray([iris.data])).tolist(),
+ "predicted_probas": clf.predict_proba(np.asarray([iris.data])).tolist(),
+ "classes": CLASSES,
}
+
def retrain_model(X, y):
logreg = LogisticRegression()
logreg.fit(X, y)
@@ -52,7 +54,7 @@ async def train_iris_model(iris: IrisTrain, background_tasks: BackgroundTasks):
background_tasks.add_task(retrain_model, X=X, y=y)
return {"message": "Notification sent in the background"}
+
@app.get("/iris/classes")
async def create_post(iris: IrisPredict):
return CLASSES
-
diff --git a/api/tp/predict/app/schema.py b/api/tp/predict/app/schema.py
index 26f82c09..3851c42c 100644
--- a/api/tp/predict/app/schema.py
+++ b/api/tp/predict/app/schema.py
@@ -5,6 +5,7 @@
class IrisPredict(BaseModel):
data: List[float]
+
class IrisTrain(BaseModel):
data: List[List[float]]
- targets: List[float]
\ No newline at end of file
+ targets: List[float]
diff --git a/authentication/tp/api.py b/authentication/tp/api.py
index 91bbd2d7..e8dee562 100644
--- a/authentication/tp/api.py
+++ b/authentication/tp/api.py
@@ -7,15 +7,14 @@
oauth2_scheme = OAuth2PasswordBearer(tokenUrl="token")
# Configure client
-keycloak_openid = KeycloakOpenID(server_url="http://localhost:8080/auth/",
- client_id="fastapi",
- realm_name="master",
- client_secret_key="97e13e2d-90e6-447f-9e3b-914b27653821")
+keycloak_openid = KeycloakOpenID(
+ server_url="http://localhost:8080/auth/",
+ client_id="fastapi",
+ realm_name="master",
+ client_secret_key="97e13e2d-90e6-447f-9e3b-914b27653821",
+)
@app.get("/protected")
def protected(token: str = Depends(oauth2_scheme)):
- return {
- "Hello": "World",
- "user_infos": token
- }
+ return {"Hello": "World", "user_infos": token}
diff --git a/authentication/tp/front.py b/authentication/tp/front.py
old mode 100755
new mode 100644
index 3cad317e..89660300
--- a/authentication/tp/front.py
+++ b/authentication/tp/front.py
@@ -7,27 +7,30 @@
from os.path import join, dirname, realpath
import requests
-UPLOADS_PATH = join(dirname(realpath(__file__)), 'client_secrets.json')
+UPLOADS_PATH = join(dirname(realpath(__file__)), "client_secrets.json")
logging.basicConfig(level=logging.DEBUG)
app = Flask(__name__)
print(UPLOADS_PATH)
-app.config.update({
- 'SECRET_KEY': 'SomethingNotEntirelySecret',
- 'TESTING': True,
- 'DEBUG': True,
- 'OIDC_CLIENT_SECRETS': UPLOADS_PATH,
- 'OIDC_ID_TOKEN_COOKIE_SECURE': False,
- 'OIDC_REQUIRE_VERIFIED_EMAIL': False,
- 'OIDC_USER_INFO_ENABLED': True,
- 'OIDC_OPENID_REALM': 'master',
- 'OIDC_SCOPES': ['openid', 'email', 'profile'],
- 'OIDC_INTROSPECTION_AUTH_METHOD': 'client_secret_post'
-})
+app.config.update(
+ {
+ "SECRET_KEY": "SomethingNotEntirelySecret",
+ "TESTING": True,
+ "DEBUG": True,
+ "OIDC_CLIENT_SECRETS": UPLOADS_PATH,
+ "OIDC_ID_TOKEN_COOKIE_SECURE": False,
+ "OIDC_REQUIRE_VERIFIED_EMAIL": False,
+ "OIDC_USER_INFO_ENABLED": True,
+ "OIDC_OPENID_REALM": "master",
+ "OIDC_SCOPES": ["openid", "email", "profile"],
+ "OIDC_INTROSPECTION_AUTH_METHOD": "client_secret_post",
+ }
+)
oidc = OpenIDConnect(app)
+
@app.before_request
def before_request():
if oidc.user_loggedin:
@@ -36,58 +39,68 @@ def before_request():
g.user = None
-@app.route('/')
+@app.route("/")
def hello_world():
if oidc.user_loggedin:
- return ('Hello, %s, See private '
- 'Log out') % \
- oidc.user_getfield('preferred_username')
+ return (
+ 'Hello, %s, See private '
+ 'Log out'
+ ) % oidc.user_getfield("preferred_username")
else:
return 'Welcome anonymous, Log in'
-@app.route('/private')
+@app.route("/private")
@oidc.require_login
def hello_me():
"""Example for protected endpoint that extracts private information from the OpenID Connect id_token.
- Uses the accompanied access_token to access a backend service.
+ Uses the accompanied access_token to access a backend service.
"""
- info = oidc.user_getinfo(['preferred_username', 'email', 'sub'])
+ info = oidc.user_getinfo(["preferred_username", "email", "sub"])
- username = info.get('preferred_username')
- email = info.get('email')
- user_id = info.get('sub')
+ username = info.get("preferred_username")
+ email = info.get("email")
+ user_id = info.get("sub")
if user_id in oidc.credentials_store:
try:
from oauth2client.client import OAuth2Credentials
- access_token = OAuth2Credentials.from_json(oidc.credentials_store[user_id]).access_token
- headers = {'Authorization': 'Bearer %s' % (access_token)}
+
+ access_token = OAuth2Credentials.from_json(
+ oidc.credentials_store[user_id]
+ ).access_token
+ headers = {"Authorization": "Bearer %s" % (access_token)}
# YOLO
- greeting = requests.get('http://127.0.0.1:1235/protected', headers=headers).text
+ greeting = requests.get(
+ "http://127.0.0.1:1235/protected", headers=headers
+ ).text
except:
greeting = "Hello %s" % username
-
- return ("""%s your email is %s and your user_id is %s!
+ return """%s your email is %s and your user_id is %s!
""" %
- (greeting, email, user_id))
+ """ % (
+ greeting,
+ email,
+ user_id,
+ )
-@app.route('/logout')
+@app.route("/logout")
def logout():
"""Performs local logout by removing the session cookie."""
res = make_response('Hi, you have been logged out! Return')
session.clear()
oidc.logout()
- requests.get("http://localhost:8080/auth/realms/master/protocol/openid-connect/logout")
+ requests.get(
+ "http://localhost:8080/auth/realms/master/protocol/openid-connect/logout"
+ )
return res
-if __name__ == '__main__':
- app.run(host='0.0.0.0', port=5001, debug=True)
\ No newline at end of file
+if __name__ == "__main__":
+ app.run(host="0.0.0.0", port=5001, debug=True)
diff --git a/base_authentication/.env b/base_authentication/.env
new file mode 100644
index 00000000..e7aa090c
--- /dev/null
+++ b/base_authentication/.env
@@ -0,0 +1,6 @@
+POSTGRES_USER=userpd
+POSTGRES_PASSWORD=postgrespassword
+POSTGRES_DB=dbesiee
+
+JWT_SECRET_KEY=mysecretkey
+JWT_SECRET_ALGORITHM=HS256
diff --git a/base_authentication/Dockerfile b/base_authentication/Dockerfile
new file mode 100644
index 00000000..a046c181
--- /dev/null
+++ b/base_authentication/Dockerfile
@@ -0,0 +1,7 @@
+FROM tiangolo/uvicorn-gunicorn-fastapi:python3.7
+
+COPY requirements.txt .
+
+RUN pip install -r requirements.txt
+
+COPY ./app /app/app
diff --git a/base_authentication/README.md b/base_authentication/README.md
new file mode 100644
index 00000000..2f4e8e17
--- /dev/null
+++ b/base_authentication/README.md
@@ -0,0 +1,232 @@
+# Notions abordées dans ce chapitre
+
+- Notion de clé étrangère (foreign key) dans une base de données ainsi qui sont implémentation en FastAPI
+- Notion d'encodage et de décodage de token JWT (JSON WebToken)
+- Systême d'authentification de base avec FastAPI
+
+# Foreign Key
+
+## Concept
+Dans les bases de données relationnelles, la notion de clé étrangère est extrêmement importante.
+Elle permet d'établir un lien entre deux tables et d'effectuer des opérations entre elles.
+Le concept est très simple, dans la base de données, il faut simplement définir une colonne qui va indiquer une colonne dans une autre table.
+
+Ici, nous étudions le cas de la clé étrangère (ForeignKey) qui permet de représenter une relation de "un à plusieurs" entre deux tables.
+Il va nous permettre de lier plusieurs objets d'une table à un seul objet d'une autre table.
+
+Parmi les autres types de relation que l'on peut retrouver dans les bases de données relationnelles, on peut citer :
+- La relation "un à un" : un objet d'une table est lié à un seul objet d'une autre table. Ce n'est qu'un cas particulier de la relation "un à plusieurs". Il existe alors une contrainte d'unicité sur la clé étrangère.
+- La relation "plusieurs à plusieurs" : un objet d'une table est lié à plusieurs objets d'une autre table et vice versa. Pour cela, on utilise une table de jointure appelée Many To Many.
+
+## Exemple
+
+Reprenons notre application avec le model Post créé précedemment.
+Voici la représentation écrite en python pour la table Post:
+```
+class Post(BaseSQL):
+ __tablename__ = "posts"
+
+ id = Column(UUID(as_uuid=True), primary_key=True, index=True)
+ title = Column(String)
+ description = Column(String)
+ created_at = Column(DateTime())
+ updated_at = Column(DateTime())
+```
+
+
+Imaginons maintenant que l'on veuille savoir quel utilisateur de notre application a créé un post.
+Nous aurons donc besoin d'un nouvel objet (table) User qui va contenir les informations de l'utilisateur.
+
+```
+class User(BaseSQL):
+ __tablename__ = "users"
+
+ id = Column(UUID(as_uuid=True), primary_key=True, index=True)
+ username = Column(String)
+ password = Column(String)
+ created_at = Column(DateTime())
+ updated_at = Column(DateTime())
+```
+
+Ainsi, nous pouvons sauvegarder les informations liés a nos utilisateurs.
+Nous devons donc maintenant créer un lien entre ces deux tables.
+
+Pour cela, nous allons ajouter une colonne `user_id` dans la table Post qui va contenir l'id de l'utilisateur qui a créé le post.
+
+```
+class Post(BaseSQL):
+ __tablename__ = "posts"
+
+ id = Column(UUID(as_uuid=True), primary_key=True, index=True)
+ title = Column(String)
+ description = Column(String)
+ created_at = Column(DateTime())
+ updated_at = Column(DateTime())
+ user_id = Column(UUID(as_uuid=True), ForeignKey('users.id'))
+```
+
+Ainsi, nous avons créé une relation entre les tables Post et User.
+
+Pour récupérer les informations de l'utilisateur qui a créé un post, il suffit de filter les posts par l'id de l'utilisateur.
+
+```
+user_posts = session.query(Post).filter(Post.user_id == user_id).first()
+```
+
+Il existe un moyen plus directe de récupérer les informations de l'utilisateur qui a créé un post en utilisant la méthode `relationship` de SQLAlchemy.
+
+```
+class Post(BaseSQL):
+ __tablename__ = "posts"
+
+ id = Column(UUID(as_uuid=True), primary_key=True, index=True)
+ title = Column(String)
+ description = Column(String)
+ created_at = Column(DateTime())
+ updated_at = Column(DateTime())
+ user_id = Column(UUID(as_uuid=True), ForeignKey('users.id'))
+ user = relationship("User", back_populates="posts")
+```
+
+En premier argument, on retrouve le nom de la table User et en second argument, le nom de l'attribut `backpopulates` qui désigne le nom
+de l'attribut de l'objet User pour y retrouver tous ses posts.
+On peut donc maintenant récupérer l'utilisateur depuis un `Post`
+
+```
+post = session.query(Post).filter(Post.id == post_id).first()
+user = post.user
+all_user_posts = user.posts
+```
+
+Ou bien
+
+```
+user = session.query(User).filter(User.id == user_id).first()
+user_posts = user.posts
+```
+
+# Authentication
+
+Dans cette section, nous allons voir comment mettre en place un système d'authentification basique avec FastAPI.
+A partir d'un login classique avec username et mot de passe, nous allons générer un Token JWT (JSON Web Token)
+qui sera ensuite utilisé pour s'authentifier sur nos endpoints qui seront configurés en conséquence.
+Chaque endpoint qui attend une authentification acceptera un header `Authorization`. La valeur de ce header commencera par le mot `Bearer` suivi du token.
+
+```
+Authorization: Bearer
+```
+
+
+## JWT Token
+
+
+Un JWT (JSON Web Token) est un moyen sécurisé d'échanger des informations entre deux parties.
+
+Un tel token est constitué de trois parties : un header, un payload et une signature.
+
+Le header contient des informations sur le type de token et l'algorithme de cryptage utilisé pour le signer (HMAC, RSA...).
+Le payload contient les données que l'on veut transmettre (comme l'ID d'utilisateur, les rôles, etc.). Ces données peuvent être publiques ou privées.
+Signature : C'est une sorte de vérification qui permet de s'assurer que le token n'a pas été modifié. Elle est créée en combinant le header, le payload et une clé secrète.
+
+Voici un exemple de JWT Token
+
+```
+eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyX2lkIjoibW9uX2lkIn0.qIBDak2Hk554hDP0hgHfMeQmt-D74kV21qQiHQ7AIow
+```
+
+Les trois parties distinctes sont séparées par un `.`.
+
+## Encodage et décodage de JWT Token
+
+Pour encoder un JWT Token, on utilise une librairie qui va nous permettre de générer un token à partir d'un payload et d'une clé secrète.
+
+``` python
+import jwt
+JWT_SECRET_KEY = os.getenv("JWT_SECRET_KEY", "should-be-an-environment-variable")
+JWT_SECRET_ALGORITHM = os.getenv("JWT_SECRET_ALGORITHM", "HS256")
+
+def _encode_jwt(user: User) -> str:
+ return jwt.encode(
+ {
+ "user_id": "mon_id",
+ },
+ JWT_SECRET_KEY,
+ algorithm=JWT_SECRET_ALGORITHM,
+ )
+```
+
+Il est fortement recommandé de stocker la clé secrète dans une variable d'environnement.
+
+Pour décoder un JWT Token, on utilise la même librairie qui va nous permettre de vérifier la signature du token et de récupérer les données du payload.
+
+``` python
+def _decode_jwt(token: str) -> dict:
+ return jwt.decode(token, JWT_SECRET_KEY, algorithms=[JWT_SECRET_ALGORITHM])
+```
+
+Ainsi, un tier ne possédant pas la clé secrète ne pourra pas vérifier la signature d'un token, en créer un ou en décoder un.
+
+Par contre, un JWT Token en tant que tel est facilement décodable et lisible par n'importe qui.
+En effet, il s'agit simplement d'une concaténation d'information en base64.
+
+Par exemple, on peut retrouver les informations encodées dans le token précédent en utilisant un décodeur en ligne (https://jwt.io/).
+
+Lors de la création d'un token, on y associe généralement un temps d'expiration pour des raisons de sécurité.
+
+## API endpoint avec authentification
+
+Nous devons maintenant configurer nos endpoints pour recevoir un JWT Token, vérifier son intégrité et autoriser l'accès aux ressources.
+
+Pour cela, nous allons utiliser une dépendance FastAPI qui va nous permettre de vérifier la validité du token.
+
+```python
+from fastapi.security import HTTPBearer
+
+security=HTTPBearer()
+
+@router.get("/{post_id}", dependencies=[Depends(security)], tags=["posts"])
+async def get_post_by_id(post_id: str, request: Request, db: Session = Depends(models.get_db)):
+ auth_header = request.headers.get("Authorization")
+
+ token = verify_autorization_header(auth_header)
+
+ post = posts_service.get_post_by_id(post_id=post_id, db=db)
+
+ if str(post.user_id) != token.get("user_id"):
+ raise HTTPException(status_code=403, detail=f"Forbidden {post.user_id} {token.get('user_id')}")
+
+ return post
+```
+
+Pour une simple authentification, FastAPI nous fournit une dépendance `HTTPBearer` qui va nous permettre de vérifier la présence d'un token dans le header `Authorization`.
+Vous verrez apparaitre un petit cadenas à côté de l'endpoint pour indiquer qu'il est sécurisé.
+
+
+
+Pour s'authentifier, il suffit de générer un token et de l'envoyer dans le header `Authorization` de la requête HTTP.
+La documentation de l'API vous offre un bouton pour tester l'endpoint avec un token.
+En cliquant sur le cadenas, vous verrez une fenêtre s'ouvrir pour vous permettre de rentrer un token.
+
+
+
+Il nous faut désormais décoder ce token pour vérifier l'identité de l'utilisateur.
+Voici un exemple de fonction qui va nous permettre de vérifier le token.
+
+```python
+def verify_autorization_header(access_token: str):
+ if not access_token or not access_token.startswith("Bearer "):
+ raise HTTPException(status_code=401, detail="No auth provided.")
+
+ try:
+ token = access_token.split("Bearer ")[1]
+ auth = jwt.decode(
+ token, JWT_SECRET_KEY, JWT_SECRET_ALGORITHM
+ )
+ except jwt.InvalidTokenError as err:
+ raise HTTPException(status_code=401, detail=f"Invalid token.")
+
+ return auth
+```
+
+Ainsi, nous avons mis en place un système d'authentification basique avec FastAPI.
+Toutes les requêtes vers les endpoints sécurisés devront être accompagnées d'un token valide. Sinon la requete sera rejetée
diff --git a/projet/app/__init__.py b/base_authentication/app/__init__.py
similarity index 100%
rename from projet/app/__init__.py
rename to base_authentication/app/__init__.py
diff --git a/base_authentication/app/main.py b/base_authentication/app/main.py
new file mode 100644
index 00000000..8a45060c
--- /dev/null
+++ b/base_authentication/app/main.py
@@ -0,0 +1,59 @@
+import base64
+from typing import Optional
+from fastapi import FastAPI, Header
+from fastapi.middleware.cors import CORSMiddleware
+import json
+
+from starlette.requests import Request
+from starlette_exporter import PrometheusMiddleware, handle_metrics
+from models import BaseSQL, engine
+import routers
+from routers.auth import auth_router
+from routers.users import user_router
+
+app = FastAPI(
+ title="Auth app",
+ description="My description",
+ version="0.0.1",
+)
+origins = [
+ "http://localhost:4200",
+ "http://localhost:3000",
+ "http://localhost:3001",
+]
+
+app.add_middleware(
+ CORSMiddleware,
+ allow_origins=origins,
+ allow_credentials=True,
+ allow_methods=["*"],
+ allow_headers=["*"],
+)
+
+app.include_router(routers.PostRouter)
+app.include_router(routers.HealthRouter)
+app.include_router(user_router)
+app.include_router(auth_router)
+
+app.add_middleware(PrometheusMiddleware)
+
+app.add_route("/metrics", handle_metrics)
+
+
+@app.on_event("startup")
+async def startup_event():
+ BaseSQL.metadata.create_all(bind=engine)
+
+
+@app.get("/api/headers")
+def read_hello(
+ request: Request,
+ x_userinfo: Optional[str] = Header(None, convert_underscores=True),
+):
+ print(request["headers"])
+ return {"Headers": json.loads(base64.b64decode(x_userinfo))}
+
+
+@app.get("/")
+def read_root():
+ return {"Hello": "World"}
diff --git a/base_authentication/app/models/__init__.py b/base_authentication/app/models/__init__.py
new file mode 100644
index 00000000..74f11bd6
--- /dev/null
+++ b/base_authentication/app/models/__init__.py
@@ -0,0 +1,4 @@
+from .post import Post
+from .user import User
+from .database import BaseSQL
+from .db import get_db, engine
diff --git a/projet/app/models/database.py b/base_authentication/app/models/database.py
similarity index 72%
rename from projet/app/models/database.py
rename to base_authentication/app/models/database.py
index 2b15ed94..690701c7 100644
--- a/projet/app/models/database.py
+++ b/base_authentication/app/models/database.py
@@ -1,7 +1,7 @@
from sqlalchemy import create_engine
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.orm import sessionmaker
-import os
+import os
POSTGRES_USER = os.environ.get("POSTGRES_USER")
@@ -9,12 +9,12 @@
POSTGRES_DB = os.environ.get("POSTGRES_DB")
-SQLALCHEMY_DATABASE_URL = f"postgresql://{POSTGRES_USER}:{POSTGRES_PASSWORD}@db/{POSTGRES_DB}"
+SQLALCHEMY_DATABASE_URL = (
+ f"postgresql://{POSTGRES_USER}:{POSTGRES_PASSWORD}@db/{POSTGRES_DB}"
+)
print(SQLALCHEMY_DATABASE_URL)
-engine = create_engine(
- SQLALCHEMY_DATABASE_URL
-)
+engine = create_engine(SQLALCHEMY_DATABASE_URL)
SessionLocal = sessionmaker(autocommit=False, autoflush=True, bind=engine)
BaseSQL = declarative_base()
diff --git a/base_authentication/app/models/db.py b/base_authentication/app/models/db.py
new file mode 100644
index 00000000..a25d8386
--- /dev/null
+++ b/base_authentication/app/models/db.py
@@ -0,0 +1,10 @@
+from .database import SessionLocal, engine
+
+
+# Dependency
+def get_db():
+ try:
+ db = SessionLocal()
+ yield db
+ finally:
+ db.close()
diff --git a/base_authentication/app/models/post.py b/base_authentication/app/models/post.py
new file mode 100644
index 00000000..14eb20d3
--- /dev/null
+++ b/base_authentication/app/models/post.py
@@ -0,0 +1,19 @@
+from sqlalchemy import Column, String, DateTime, Integer, ForeignKey
+from sqlalchemy.dialects.postgresql import UUID
+from sqlalchemy.orm import relationship
+
+from models.database import BaseSQL
+
+
+class Post(BaseSQL):
+ __tablename__ = "posts"
+
+ id = Column(UUID(as_uuid=True), primary_key=True, index=True)
+ title = Column(String)
+ description = Column(String)
+
+ user_id = Column(UUID, ForeignKey("users.id"))
+ user = relationship("User", back_populates="posts")
+
+ created_at = Column(DateTime())
+ updated_at = Column(DateTime())
diff --git a/base_authentication/app/models/user.py b/base_authentication/app/models/user.py
new file mode 100644
index 00000000..bc5f4088
--- /dev/null
+++ b/base_authentication/app/models/user.py
@@ -0,0 +1,20 @@
+from datetime import datetime
+from uuid import uuid4
+
+from sqlalchemy import Column, String, DateTime
+from sqlalchemy.dialects.postgresql import UUID
+from sqlalchemy.orm import relationship
+
+from .database import BaseSQL
+
+
+class User(BaseSQL):
+ __tablename__ = "users"
+
+ id = Column(UUID(as_uuid=True), primary_key=True, index=True, default=str(uuid4()))
+ username = Column(String, unique=True)
+ password = Column(String)
+ created_at = Column(DateTime(), default=datetime.now())
+ updated_at = Column(DateTime(), default=datetime.now())
+
+ posts = relationship("Post", back_populates="user")
diff --git a/base_authentication/app/routers/__init__.py b/base_authentication/app/routers/__init__.py
new file mode 100644
index 00000000..93ea2d4d
--- /dev/null
+++ b/base_authentication/app/routers/__init__.py
@@ -0,0 +1,2 @@
+from .posts import router as PostRouter
+from .health import router as HealthRouter
diff --git a/base_authentication/app/routers/auth.py b/base_authentication/app/routers/auth.py
new file mode 100644
index 00000000..d79f3eb1
--- /dev/null
+++ b/base_authentication/app/routers/auth.py
@@ -0,0 +1,20 @@
+from fastapi import APIRouter, Depends
+from sqlalchemy.orm import Session
+
+import models
+from schemas.auth_token import AuthToken
+from schemas.users import User
+from services.auth import generate_access_token
+
+auth_router = APIRouter(prefix="/auth")
+
+
+@auth_router.post("/token", tags=["auth"])
+async def get_access_token(
+ user_login: User,
+ db: Session = Depends(models.get_db),
+) -> AuthToken:
+ access_token = generate_access_token(db=db, user_login=user_login)
+ return AuthToken(
+ access_token=access_token,
+ )
diff --git a/projet/app/routers/health.py b/base_authentication/app/routers/health.py
similarity index 81%
rename from projet/app/routers/health.py
rename to base_authentication/app/routers/health.py
index 3b042fdf..6cfd7c96 100644
--- a/projet/app/routers/health.py
+++ b/base_authentication/app/routers/health.py
@@ -22,6 +22,8 @@ def read_root():
@router.get("/api/headers")
-def read_hello(request: Request, x_userinfo: Optional[str] = Header(None, convert_underscores=True)):
+def read_hello(
+ request: Request, x_userinfo: Optional[str] = Header(None, convert_underscores=True)
+):
print(request["headers"])
return {"Headers": json.loads(base64.b64decode(x_userinfo))}
diff --git a/base_authentication/app/routers/posts.py b/base_authentication/app/routers/posts.py
new file mode 100644
index 00000000..b26309e8
--- /dev/null
+++ b/base_authentication/app/routers/posts.py
@@ -0,0 +1,73 @@
+from typing import List, Any, Dict
+
+from fastapi import APIRouter, Depends, HTTPException
+from fastapi.security import HTTPBearer
+from sqlalchemy.orm import Session
+from starlette.requests import Request
+from starlette.routing import Router
+
+import models
+import schemas
+
+from services.auth import verify_authorization_header, get_user_id
+from services import posts as posts_service
+
+router = APIRouter(prefix="/posts")
+
+security = HTTPBearer()
+
+
+@router.post("/", dependencies=[Depends(security)], tags=["posts"])
+async def create_post(
+ post: schemas.Post,
+ user_id: str = Depends(get_user_id),
+ db: Session = Depends(models.get_db),
+):
+ return posts_service.create_post(user_id=user_id, post=post, db=db)
+
+
+@router.get("/users", dependencies=[Depends(security)], tags=["posts_per_user"])
+async def get_user_posts(
+ token: int = Depends(verify_authorization_header),
+ db: Session = Depends(models.get_db),
+) -> List[schemas.Post]:
+
+ user_id = token.get("user_id")
+
+ return posts_service.get_posts_for_user(db=db, user_id=user_id)
+
+
+@router.get("/{post_id}", dependencies=[Depends(security)], tags=["posts"])
+async def get_post_by_id(
+ post_id: str, token: int = Depends(verify_authorization_header), db: Session = Depends(models.get_db)
+):
+ post = posts_service.get_post_by_id(post_id=post_id, db=db)
+
+ if str(post.user_id) != token.get("user_id"):
+ raise HTTPException(
+ status_code=403, detail=f"Forbidden {post.user_id} {token.get('user_id')}"
+ )
+
+ return post
+
+
+@router.get("/", tags=["posts"])
+async def get_posts(db: Session = Depends(models.get_db)):
+ return posts_service.get_all_posts(db=db)
+
+
+@router.put("/{post_id}", dependencies=[Depends(security)], tags=["posts"])
+async def update_post_by_id(
+ post_id: str, post: schemas.Post, db: Session = Depends(models.get_db)
+):
+ return posts_service.update_post(post_id=post_id, db=db, post=post)
+
+
+@router.delete("/{post_id}", tags=["posts"])
+async def delete_post_by_id(post_id: str, db: Session = Depends(models.get_db)):
+ return posts_service.delete_post(post_id=post_id, db=db)
+
+
+@router.delete("/", tags=["posts"])
+async def delete_all_posts(db: Session = Depends(models.get_db)):
+ return posts_service.delete_all_posts(db=db)
diff --git a/base_authentication/app/routers/users.py b/base_authentication/app/routers/users.py
new file mode 100644
index 00000000..519e7ac9
--- /dev/null
+++ b/base_authentication/app/routers/users.py
@@ -0,0 +1,20 @@
+from typing import List
+
+from fastapi import APIRouter, Depends
+from sqlalchemy.orm import Session
+
+import models
+from schemas.users import User
+from services.users import create_user, get_all_users
+
+user_router = APIRouter(prefix="/users")
+
+
+@user_router.post("/", tags=["users"])
+async def post_user(user: User, db: Session = Depends(models.get_db)):
+ return create_user(user=user, db=db)
+
+
+@user_router.get("/", tags=["users"])
+async def retrieve_all_users(db: Session = Depends(models.get_db)) -> List[User]:
+ return get_all_users(db=db)
diff --git a/base_authentication/app/routers/utils.py b/base_authentication/app/routers/utils.py
new file mode 100644
index 00000000..5e5499d6
--- /dev/null
+++ b/base_authentication/app/routers/utils.py
@@ -0,0 +1,17 @@
+import jwt
+from fastapi import HTTPException
+
+from services.auth import JWT_SECRET_KEY, JWT_SECRET_ALGORITHM
+
+
+def verify_autorization_header(access_token: str):
+ if not access_token or not access_token.startswith("Bearer "):
+ raise HTTPException(status_code=401, detail="No auth provided.")
+
+ try:
+ token = access_token.split("Bearer ")[1]
+ auth = jwt.decode(token, JWT_SECRET_KEY, JWT_SECRET_ALGORITHM)
+ except jwt.InvalidTokenError as err:
+ raise HTTPException(status_code=401, detail=f"Invalid token.")
+
+ return auth
diff --git a/projet/app/schemas/__init__.py b/base_authentication/app/schemas/__init__.py
similarity index 100%
rename from projet/app/schemas/__init__.py
rename to base_authentication/app/schemas/__init__.py
diff --git a/base_authentication/app/schemas/auth_token.py b/base_authentication/app/schemas/auth_token.py
new file mode 100644
index 00000000..cafb3d5b
--- /dev/null
+++ b/base_authentication/app/schemas/auth_token.py
@@ -0,0 +1,5 @@
+from pydantic import BaseModel
+
+
+class AuthToken(BaseModel):
+ access_token: str
diff --git a/base_authentication/app/schemas/posts.py b/base_authentication/app/schemas/posts.py
new file mode 100644
index 00000000..5c50f6f0
--- /dev/null
+++ b/base_authentication/app/schemas/posts.py
@@ -0,0 +1,17 @@
+from typing import List, Optional
+from datetime import datetime
+from pydantic import BaseModel, Field
+from uuid import uuid4
+from typing_extensions import Annotated
+
+
+class Post(BaseModel):
+ id: Annotated[str, Field(default_factory=lambda: uuid4().hex)]
+ title: str
+ description: Optional[str]
+ user_id: Optional[str]
+ created_at: Annotated[datetime, Field(default_factory=lambda: datetime.now())]
+ updated_at: Annotated[datetime, Field(default_factory=lambda: datetime.now())]
+
+ class Config:
+ orm_mode = True
diff --git a/base_authentication/app/schemas/users.py b/base_authentication/app/schemas/users.py
new file mode 100644
index 00000000..81bc556c
--- /dev/null
+++ b/base_authentication/app/schemas/users.py
@@ -0,0 +1,10 @@
+from typing import List, Optional
+from datetime import datetime
+from pydantic import BaseModel, Field
+from uuid import uuid4
+from typing_extensions import Annotated
+
+
+class User(BaseModel):
+ username: str
+ password: str
diff --git a/projet/app/dependencies/__init__.py b/base_authentication/app/services/__init__.py
similarity index 100%
rename from projet/app/dependencies/__init__.py
rename to base_authentication/app/services/__init__.py
diff --git a/base_authentication/app/services/auth.py b/base_authentication/app/services/auth.py
new file mode 100644
index 00000000..96b90128
--- /dev/null
+++ b/base_authentication/app/services/auth.py
@@ -0,0 +1,70 @@
+import os
+from typing import Dict, Union, List
+
+import jwt
+from fastapi import Depends, HTTPException, Header
+from sqlalchemy.orm import Session
+
+import models
+from schemas.users import User
+
+JWT_SECRET_KEY = os.getenv("JWT_SECRET_KEY", "should-be-an-environment-variable")
+JWT_SECRET_ALGORITHM = os.getenv("JWT_SECRET_ALGORITHM", "HS256")
+
+
+def _encode_jwt(user: User) -> str:
+ return jwt.encode(
+ {
+ "user_id": str(user.id),
+ "role": user.role, # admin, customer, staff, manager...
+ },
+ JWT_SECRET_KEY,
+ algorithm=JWT_SECRET_ALGORITHM,
+ )
+
+
+async def verify_authorization_header(
+ authorization: str = Header(...),
+) -> Dict[str, Union[int, Dict[str, Union[List[str], int, str]]]]:
+ if not authorization or not authorization.startswith("Bearer "):
+ raise HTTPException(status_code=401, detail="No authorization header")
+ try:
+ auth = jwt.decode(
+ authorization[7:],
+ JWT_SECRET_KEY,
+ algorithms=[JWT_SECRET_ALGORITHM],
+ )
+ except jwt.ExpiredSignatureError:
+ raise HTTPException(status_code=401, detail="Token expired")
+ except jwt.InvalidTokenError as err:
+ raise HTTPException(status_code=401, detail=f"Invalid token: '{err}'")
+
+ return auth
+
+
+async def get_user_id(authorization: str = Header(...)) -> str:
+ auth = await verify_authorization_header(authorization)
+ try:
+ user_id = str(auth["user_id"])
+ except KeyError:
+ raise HTTPException(status_code=401, detail="Invalid token")
+ return user_id
+
+
+def generate_access_token(
+ db: Session,
+ user_login: User,
+):
+ user = (
+ db.query(models.User)
+ .filter(
+ models.User.username == user_login.username,
+ models.User.password == user_login.password,
+ )
+ .first()
+ )
+
+ if not user:
+ raise HTTPException(status_code=404, detail="Incorrect username or password")
+
+ return _encode_jwt(user)
diff --git a/base_authentication/app/services/posts.py b/base_authentication/app/services/posts.py
new file mode 100644
index 00000000..f76fe8ce
--- /dev/null
+++ b/base_authentication/app/services/posts.py
@@ -0,0 +1,68 @@
+from typing import List
+
+from sqlalchemy.orm import Session
+from fastapi import HTTPException
+from datetime import datetime
+import models, schemas
+
+
+def get_all_posts(db: Session, skip: int = 0, limit: int = 10) -> List[models.Post]:
+ records = db.query(models.Post).filter().all()
+ for record in records:
+ record.id = str(record.id)
+ return records
+
+
+def get_post_by_id(post_id: str, db: Session) -> models.Post:
+ record = db.query(models.Post).filter(models.Post.id == post_id).first()
+ if not record:
+ raise HTTPException(status_code=404, detail="Not Found")
+ record.id = str(record.id)
+ return record
+
+
+def get_posts_by_title(title: str, db: Session) -> List[models.Post]:
+ records = db.query(models.Post).filter(models.Post.title == title).all()
+ for record in records:
+ record.id = str(record.id)
+ return records
+
+
+def get_posts_for_user(db: Session, user_id: str) -> List[models.Post]:
+ return db.query(models.Post).filter(models.Post.user_id == user_id).all()
+
+
+def update_post(post_id: str, db: Session, post: schemas.Post) -> models.Post:
+ db_post = get_post_by_id(post_id=post_id, db=db)
+ for var, value in vars(post).items():
+ setattr(db_post, var, value) if value else None
+ db_post.updated_at = datetime.now()
+ db.add(db_post)
+ db.commit()
+ db.refresh(db_post)
+ return db_post
+
+
+def delete_post(post_id: str, db: Session) -> models.Post:
+ db_post = get_post_by_id(post_id=post_id, db=db)
+ db.delete(db_post)
+ db.commit()
+ return db_post
+
+
+def delete_all_posts(db: Session) -> List[models.Post]:
+ records = db.query(models.Post).filter()
+ for record in records:
+ db.delete(record)
+ db.commit()
+ return records
+
+
+def create_post(db: Session, user_id: str, post: schemas.Post) -> models.Post:
+ db_post = models.Post(**post.dict())
+ db_post.user_id = user_id
+ db.add(db_post)
+ db.commit()
+ db.refresh(db_post)
+ db_post.id = str(db_post.id)
+ return db_post
diff --git a/base_authentication/app/services/users.py b/base_authentication/app/services/users.py
new file mode 100644
index 00000000..3bdf675d
--- /dev/null
+++ b/base_authentication/app/services/users.py
@@ -0,0 +1,52 @@
+from typing import Optional, List
+from uuid import uuid4
+
+import jwt
+from fastapi import HTTPException
+from jwt import InvalidTokenError
+from sqlalchemy.orm import Session
+from starlette.status import HTTP_401_UNAUTHORIZED
+
+import models
+from models import get_db
+from schemas.users import User
+from services.auth import JWT_SECRET_KEY, JWT_SECRET_ALGORITHM
+
+
+def create_user(db: Session, user: User) -> models.User:
+ record = db.query(models.User).filter(models.User.username == user.username).first()
+ if record:
+ raise HTTPException(status_code=409, detail="Username already taken")
+
+ db_user = models.User(
+ id=str(uuid4()), username=user.username, password=user.password
+ )
+ db.add(db_user)
+ db.commit()
+
+ return db_user
+
+
+def get_all_users(db: Session) -> List[models.User]:
+ return db.query(models.User).filter().all()
+
+
+def get_user_by_id(db: Session, user_id: int) -> Optional[models.User]:
+ return db.query(models.User).filter(models.User.id == user_id).first()
+
+
+def get_current_user_id(token: str):
+ try:
+ payload = jwt.decode(token, JWT_SECRET_KEY, algorithms=[JWT_SECRET_ALGORITHM])
+ user_id: str = payload.get("user_id")
+ if user_id is None:
+ raise HTTPException(
+ status_code=HTTP_401_UNAUTHORIZED,
+ detail="Could not validate credentials",
+ )
+ return user_id
+ except InvalidTokenError:
+ raise HTTPException(
+ status_code=HTTP_401_UNAUTHORIZED,
+ detail="Could not validate credentials",
+ )
diff --git a/base_authentication/docker-compose.yml b/base_authentication/docker-compose.yml
new file mode 100644
index 00000000..f926d156
--- /dev/null
+++ b/base_authentication/docker-compose.yml
@@ -0,0 +1,33 @@
+version: '3.4'
+
+networks:
+ default:
+ driver: bridge
+services:
+ api:
+ build: .
+ networks:
+ - default
+ volumes:
+ - ./app/:/app
+ command: ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8080", "--reload"]
+ ports:
+ - "8080:8080"
+ env_file:
+ - .env
+ depends_on:
+ - db
+
+ db:
+ image: postgres
+ restart: always
+ env_file:
+ - .env
+ ports:
+ - "5431:5432"
+
+ volumes:
+ - postgres_data:/var/lib/postgresql/data
+
+volumes:
+ postgres_data:
\ No newline at end of file
diff --git a/base_authentication/img.png b/base_authentication/img.png
new file mode 100644
index 00000000..230d5e01
Binary files /dev/null and b/base_authentication/img.png differ
diff --git a/base_authentication/img_1.png b/base_authentication/img_1.png
new file mode 100644
index 00000000..7452d469
Binary files /dev/null and b/base_authentication/img_1.png differ
diff --git a/projet/requirements.txt b/base_authentication/requirements.txt
similarity index 62%
rename from projet/requirements.txt
rename to base_authentication/requirements.txt
index d298b3c4..5aaa1af3 100644
--- a/projet/requirements.txt
+++ b/base_authentication/requirements.txt
@@ -2,4 +2,6 @@ starlette_exporter
sqlalchemy
pydantic
psycopg2
-python-slugify
\ No newline at end of file
+python-slugify
+pyjwt
+fastapi
diff --git a/base_authentication/tp.md b/base_authentication/tp.md
new file mode 100644
index 00000000..d8eddddc
--- /dev/null
+++ b/base_authentication/tp.md
@@ -0,0 +1,22 @@
+# Exercice
+
+## Première partie
+A partir de l'app mise en place dans le dossier `app/`, ajouter un système d'authentification basique avec FastAPI sur les
+endpoints que vous jugerez bon de sécuriser.
+
+## Deuxième partie
+
+Vous remarquerez certainement que pour chaque endpoint sécurisé, vous devrez vérifier le token JWT à chaque fois.
+Ce qui implique une duplication de code.
+A l'aide de la documentation de FastAPI, mettez en place un système permettant de gérer
+l'authentification de manière automatique pour un endpoint.
+
+## Troisième partie
+
+Ecrivez une fonction qui récupère l'ID du user à partir du token JWT.
+En entrant dans la fonction de l'endpoint, le programme devra avoir valider le token et avoir l'ID de l'utilisateur à disposition
+
+## Quatrième partie
+
+A l'aide de la librairie `requests`, écrivez un script python pour effectuer une requête vers un de vos endpoint sécurisé.
+
diff --git a/kong/POSTGRES_PASSWORD b/kong/POSTGRES_PASSWORD
old mode 100755
new mode 100644
diff --git a/kong/docker/scripts/kong_cors_plugin.py b/kong/docker/scripts/kong_cors_plugin.py
index eee1bdb9..cb821177 100644
--- a/kong/docker/scripts/kong_cors_plugin.py
+++ b/kong/docker/scripts/kong_cors_plugin.py
@@ -1,5 +1,6 @@
import requests
import os
+
# from dotenv import load_dotenv
# load_dotenv("../../.env")
@@ -16,33 +17,38 @@
BACKEND_HOST_IP = os.environ.get("BACKEND_HOST_IP")
data = [
- ('name', 'cors'),
- ('config.origins', 'http://localhost:1337/*'),
- ('config.methods', 'GET'),
- ('config.methods', 'POST'),
- ('config.methods', 'OPTIONS'),
- ('config.methods', 'PUT'),
- ('config.methods', 'DELETE'),
- ('config.headers', 'Accept'),
- ('config.headers', 'Accept-Version'),
- ('config.headers', 'Content-Length'),
- ('config.headers', 'Content-MD5'),
- ('config.headers', 'Content-Type'),
- ('config.headers', 'Authorization'),
- ('config.headers', 'Date'),
- ('config.headers', 'X-Auth-Token'),
- ('config.exposed_headers', 'X-Auth-Token'),
- ('config.credentials', 'true'),
- ('config.max_age', '3600')
+ ("name", "cors"),
+ ("config.origins", "http://localhost:1337/*"),
+ ("config.methods", "GET"),
+ ("config.methods", "POST"),
+ ("config.methods", "OPTIONS"),
+ ("config.methods", "PUT"),
+ ("config.methods", "DELETE"),
+ ("config.headers", "Accept"),
+ ("config.headers", "Accept-Version"),
+ ("config.headers", "Content-Length"),
+ ("config.headers", "Content-MD5"),
+ ("config.headers", "Content-Type"),
+ ("config.headers", "Authorization"),
+ ("config.headers", "Date"),
+ ("config.headers", "X-Auth-Token"),
+ ("config.exposed_headers", "X-Auth-Token"),
+ ("config.credentials", "true"),
+ ("config.max_age", "3600"),
]
def get_enki_service_id():
- response = requests.get(f'http://{KONG_HOST_IP}:{KONG_PORT}/services')
- print(response.json()["data"])
- enki_api_id = [elt["id"] for elt in response.json()["data"] if elt["host"] == BACKEND_HOST_IP][0]
- return enki_api_id
+ response = requests.get(f"http://{KONG_HOST_IP}:{KONG_PORT}/services")
+ print(response.json()["data"])
+ enki_api_id = [
+ elt["id"] for elt in response.json()["data"] if elt["host"] == BACKEND_HOST_IP
+ ][0]
+ return enki_api_id
+
enki_service_id = get_enki_service_id()
-response = requests.post(f'http://{KONG_HOST_IP}:{KONG_PORT}/services/{enki_service_id}/plugins', data=data)
+response = requests.post(
+ f"http://{KONG_HOST_IP}:{KONG_PORT}/services/{enki_service_id}/plugins", data=data
+)
diff --git a/kong/docker/scripts/kong_oidc_import.py b/kong/docker/scripts/kong_oidc_import.py
index f75d0866..abf331a5 100644
--- a/kong/docker/scripts/kong_oidc_import.py
+++ b/kong/docker/scripts/kong_oidc_import.py
@@ -2,6 +2,7 @@
import os
import uuid
from keycloak.exceptions import KeycloakGetError
+
# from dotenv import load_dotenv
# load_dotenv("../../.env")
@@ -22,92 +23,95 @@
services = [
- {
- 'name': "front_service",
- 'url': f'http://nginx:7777/front',
- 'path': "front"
-
- },
- {
- 'name': "api_service",
- 'url': f'http://api:5000/api',
- 'path': "api"
-
- }
+ {"name": "front_service", "url": f"http://nginx:7777/front", "path": "front"},
+ {"name": "api_service", "url": f"http://api:5000/api", "path": "api"},
]
-#def clean():
-response = requests.get(f'http://{KONG_HOST_IP}:{KONG_PORT}/routes')
+# def clean():
+response = requests.get(f"http://{KONG_HOST_IP}:{KONG_PORT}/routes")
for _id in [e["id"] for e in response.json()["data"]]:
- requests.delete(f'http://{KONG_HOST_IP}:{KONG_PORT}/routes/{_id}')
-response = requests.get(f'http://{KONG_HOST_IP}:{KONG_PORT}/services')
+ requests.delete(f"http://{KONG_HOST_IP}:{KONG_PORT}/routes/{_id}")
+response = requests.get(f"http://{KONG_HOST_IP}:{KONG_PORT}/services")
for _id in [e["id"] for e in response.json()["data"]]:
- requests.delete(f'http://{KONG_HOST_IP}:{KONG_PORT}/services/{_id}')
+ requests.delete(f"http://{KONG_HOST_IP}:{KONG_PORT}/services/{_id}")
from keycloak import KeycloakAdmin
# Create kong client on Keycloak
-keycloak_admin = KeycloakAdmin(server_url=KEYCLOAK_URL,
- username=KEYCLOAK_ADMIN_USER,
- password=KEYCLOAK_ADMIN_PASSWORD,
- verify=True)
+keycloak_admin = KeycloakAdmin(
+ server_url=KEYCLOAK_URL,
+ username=KEYCLOAK_ADMIN_USER,
+ password=KEYCLOAK_ADMIN_PASSWORD,
+ verify=True,
+)
def create_client_and_get_client_secret():
# Create kong client on Keycloak
- keycloak_admin = KeycloakAdmin(server_url=KEYCLOAK_URL,
- username=KEYCLOAK_ADMIN_USER,
- password=KEYCLOAK_ADMIN_PASSWORD,
- verify=True)
+ keycloak_admin = KeycloakAdmin(
+ server_url=KEYCLOAK_URL,
+ username=KEYCLOAK_ADMIN_USER,
+ password=KEYCLOAK_ADMIN_PASSWORD,
+ verify=True,
+ )
try:
- keycloak_admin.create_client({
- "clientId":CLIENT_NAME,
- "name":CLIENT_NAME,
- "enabled": True,
- "redirectUris":[ "/front/*", "/api/*", "/*", "*" ],
- })
-
+ keycloak_admin.create_client(
+ {
+ "clientId": CLIENT_NAME,
+ "name": CLIENT_NAME,
+ "enabled": True,
+ "redirectUris": ["/front/*", "/api/*", "/*", "*"],
+ }
+ )
+
client_uuid = keycloak_admin.get_client_id(CLIENT_NAME)
keycloak_admin.generate_client_secrets(client_uuid)
except KeycloakGetError as e:
- if e.response_code == 409:
+ if e.response_code == 409:
print("Keycloak Kong client already exists")
-
+
client_uuid = keycloak_admin.get_client_id(CLIENT_NAME)
- return keycloak_admin.get_client_secrets(client_uuid)['value']
+ return keycloak_admin.get_client_secrets(client_uuid)["value"]
+
CLIENT_SECRET = create_client_and_get_client_secret()
-introspection_url = f'http://{KEYCLOAK_HOST_IP}:{KEYCLOAK_PORT}/auth/realms/{REALM_NAME}/protocol/openid-connect/token/introspect'
-discovery_url = f'http://{KEYCLOAK_HOST_IP}:{KEYCLOAK_PORT}/auth/realms/{REALM_NAME}/.well-known/openid-configuration'
+introspection_url = f"http://{KEYCLOAK_HOST_IP}:{KEYCLOAK_PORT}/auth/realms/{REALM_NAME}/protocol/openid-connect/token/introspect"
+discovery_url = f"http://{KEYCLOAK_HOST_IP}:{KEYCLOAK_PORT}/auth/realms/{REALM_NAME}/.well-known/openid-configuration"
for service in services:
data = service
# Create Service
- response = requests.post(f'http://{KONG_HOST_IP}:{KONG_PORT}/services', data=data)
+ response = requests.post(f"http://{KONG_HOST_IP}:{KONG_PORT}/services", data=data)
created_service_id = response.json()["id"]
# Create route
data = {
- 'service.id': f'{created_service_id}',
- 'paths[]': f'/{service["path"]}',
+ "service.id": f"{created_service_id}",
+ "paths[]": f'/{service["path"]}',
}
- response = requests.post(f'http://{KONG_HOST_IP}:{KONG_PORT}/services/{service["name"]}/routes', data=data)
+ response = requests.post(
+ f'http://{KONG_HOST_IP}:{KONG_PORT}/services/{service["name"]}/routes',
+ data=data,
+ )
# Configure OIDC
data = {
- 'name': 'oidc',
- 'config.client_id': f'{CLIENT_ID}',
- 'config.client_secret': f'{CLIENT_SECRET}',
- 'config.realm': f'{REALM_NAME}',
- 'config.bearer_only': 'true',
- 'config.introspection_endpoint': introspection_url,
- 'config.discovery': discovery_url
+ "name": "oidc",
+ "config.client_id": f"{CLIENT_ID}",
+ "config.client_secret": f"{CLIENT_SECRET}",
+ "config.realm": f"{REALM_NAME}",
+ "config.bearer_only": "true",
+ "config.introspection_endpoint": introspection_url,
+ "config.discovery": discovery_url,
}
- response = requests.post(f'http://{KONG_HOST_IP}:{KONG_PORT}/services/{created_service_id}/plugins', data=data)
\ No newline at end of file
+ response = requests.post(
+ f"http://{KONG_HOST_IP}:{KONG_PORT}/services/{created_service_id}/plugins",
+ data=data,
+ )
diff --git a/kong/docker/scripts/kong_oidc_import_test.py b/kong/docker/scripts/kong_oidc_import_test.py
index e7ffe8b1..ed83ea71 100644
--- a/kong/docker/scripts/kong_oidc_import_test.py
+++ b/kong/docker/scripts/kong_oidc_import_test.py
@@ -1,6 +1,7 @@
import requests
import os
import uuid
+
# from dotenv import load_dotenv
# load_dotenv("../../.env")
@@ -15,74 +16,74 @@
services = [
- {
- 'name': "front_service",
- 'url': f'http://nginx:7777/front',
- 'path': "front"
-
- },
- {
- 'name': "api_service",
- 'url': f'http://nginx:7777/api',
- 'path': "api"
-
- }
+ {"name": "front_service", "url": f"http://nginx:7777/front", "path": "front"},
+ {"name": "api_service", "url": f"http://nginx:7777/api", "path": "api"},
]
-#def clean():
-response = requests.get(f'http://{KONG_HOST_IP}:{KONG_PORT}/routes')
+# def clean():
+response = requests.get(f"http://{KONG_HOST_IP}:{KONG_PORT}/routes")
for _id in [e["id"] for e in response.json()["data"]]:
- requests.delete(f'http://{KONG_HOST_IP}:{KONG_PORT}/routes/{_id}')
-response = requests.get(f'http://{KONG_HOST_IP}:{KONG_PORT}/services')
+ requests.delete(f"http://{KONG_HOST_IP}:{KONG_PORT}/routes/{_id}")
+response = requests.get(f"http://{KONG_HOST_IP}:{KONG_PORT}/services")
for _id in [e["id"] for e in response.json()["data"]]:
- requests.delete(f'http://{KONG_HOST_IP}:{KONG_PORT}/services/{_id}')
+ requests.delete(f"http://{KONG_HOST_IP}:{KONG_PORT}/services/{_id}")
from keycloak import KeycloakAdmin
# Create kong client on Keycloak
-keycloak_admin = KeycloakAdmin(server_url=KEYCLOAK_URL,
- username=KEYCLOAK_ADMIN_USER,
- password=KEYCLOAK_ADMIN_PASSWORD,
- verify=True)
+keycloak_admin = KeycloakAdmin(
+ server_url=KEYCLOAK_URL,
+ username=KEYCLOAK_ADMIN_USER,
+ password=KEYCLOAK_ADMIN_PASSWORD,
+ verify=True,
+)
CLIENT_KONG_KEYCLOAK_ID = str(uuid.uuid4())
-keycloak_admin.create_client({
- "id":CLIENT_KONG_KEYCLOAK_ID,
- "clientId":CLIENT_ID,
- "name":CLIENT_ID,
- "enabled": True,
- "redirectUris":[ "/front/*", "/api/*", "/*", "*" ],
-})
+keycloak_admin.create_client(
+ {
+ "id": CLIENT_KONG_KEYCLOAK_ID,
+ "clientId": CLIENT_ID,
+ "name": CLIENT_ID,
+ "enabled": True,
+ "redirectUris": ["/front/*", "/api/*", "/*", "*"],
+ }
+)
CLIENT_SECRET = keycloak_admin.get_client_secrets(CLIENT_KONG_KEYCLOAK_ID)["value"]
-introspection_url = f'http://{KEYCLOAK_HOST_IP}:{KEYCLOAK_PORT}/auth/realms/{REALM_NAME}/protocol/openid-connect/token/introspect'
-discovery_url = f'http://{KEYCLOAK_HOST_IP}:{KEYCLOAK_PORT}/auth/realms/{REALM_NAME}/.well-known/openid-configuration'
+introspection_url = f"http://{KEYCLOAK_HOST_IP}:{KEYCLOAK_PORT}/auth/realms/{REALM_NAME}/protocol/openid-connect/token/introspect"
+discovery_url = f"http://{KEYCLOAK_HOST_IP}:{KEYCLOAK_PORT}/auth/realms/{REALM_NAME}/.well-known/openid-configuration"
for service in services:
data = service
# Create Service
- response = requests.post(f'http://{KONG_HOST_IP}:{KONG_PORT}/services', data=data)
+ response = requests.post(f"http://{KONG_HOST_IP}:{KONG_PORT}/services", data=data)
created_service_id = response.json()["id"]
# Create route
data = {
- 'service.id': f'{created_service_id}',
- 'paths[]': f'/{service["path"]}',
+ "service.id": f"{created_service_id}",
+ "paths[]": f'/{service["path"]}',
}
- response = requests.post(f'http://{KONG_HOST_IP}:{KONG_PORT}/services/{service["name"]}/routes', data=data)
+ response = requests.post(
+ f'http://{KONG_HOST_IP}:{KONG_PORT}/services/{service["name"]}/routes',
+ data=data,
+ )
# Configure OIDC
data = {
- 'name': 'oidc',
- 'config.client_id': f'{CLIENT_ID}',
- 'config.client_secret': f'{CLIENT_SECRET}',
- 'config.realm': f'{REALM_NAME}',
- 'config.bearer_only': 'true',
- 'config.introspection_endpoint': introspection_url,
- 'config.discovery': discovery_url
+ "name": "oidc",
+ "config.client_id": f"{CLIENT_ID}",
+ "config.client_secret": f"{CLIENT_SECRET}",
+ "config.realm": f"{REALM_NAME}",
+ "config.bearer_only": "true",
+ "config.introspection_endpoint": introspection_url,
+ "config.discovery": discovery_url,
}
- response = requests.post(f'http://{KONG_HOST_IP}:{KONG_PORT}/services/{created_service_id}/plugins', data=data)
\ No newline at end of file
+ response = requests.post(
+ f"http://{KONG_HOST_IP}:{KONG_PORT}/services/{created_service_id}/plugins",
+ data=data,
+ )
diff --git a/ops/cours/Dockerfile b/ops/cours/Dockerfile
index 0b46a19e..3e69e679 100644
--- a/ops/cours/Dockerfile
+++ b/ops/cours/Dockerfile
@@ -2,9 +2,10 @@ FROM python:3.10-slim-buster
WORKDIR /app
+COPY . .
+
COPY requirements.txt requirements.txt
RUN pip3 install -r requirements.txt
-COPY . .
CMD [ "python3", "-m" , "flask", "--debug", "run", "--host=0.0.0.0" ]
diff --git a/ops/cours/README.md b/ops/cours/README.md
index 4af82c52..47eb5824 100644
--- a/ops/cours/README.md
+++ b/ops/cours/README.md
@@ -1,6 +1,7 @@
# Docker
- [Docker](#docker)
+ - [Terminology](#terminology)
- [Install Docker Desktop](#install-docker-desktop)
- [Build an image](#build-an-image)
- [Launch a container](#launch-a-container)
@@ -14,6 +15,35 @@
- [Docker registry](#docker-registry)
- [Développer depuis un conteneur](#développer-depuis-un-conteneur)
+## Terminology
+
+### Image
+
+An image is a read-only template with instructions for creating a Docker container.
+Often, an image is based on another image, with some additional customization.
+It contains all the libraries, dependencies, and files that the container needs to run.
+An image is shareable and portable, so you can deploy the same image in multiple locations at once—much like a software binary file.
+
+### Container
+
+A container is a runnable instance of an image.
+You can create, start, stop, move, or delete a container using the Docker API or CLI.
+
+### Registry
+
+A registry is a collection of repositories, and a repository is a collection of images—sort of like a GitHub repository, but for Docker images.
+
+### Tag
+
+A tag is a label applied to a Docker image in a repository.
+Tags are used to specify different versions of the same image, for example, `latest`, `v1`, `v2`.
+
+### Volume
+
+A volume is a persistent data storage mechanism that allows data to exist beyond the lifetime of the container.
+Volumes are used to share data between the host and the container, and between containers.
+
+
## Install Docker Desktop
Windows: https://docs.docker.com/desktop/install/windows-install/
@@ -38,11 +68,22 @@ COPY . .
CMD [ "python3", "-m" , "flask", "--debug", "run", "--host=0.0.0.0" ]
```
-Build image
+Build image:
+
+By default, Docker will look for a file named `Dockerfile` in the directory designated by the `context` argument (the last one specified).
+
```
docker build -t image_name .
```
+This command will build an image named `image_name` from the file named `Dockerfile` in the current directory (`context` arg is .) .
+
+To build an image from a specific Dockerfile, use the `-f` flag.
+
+```
+docker build -t image_name -f dockerfiles/other_Dockerfile whatever_folder/
+```
+
## Launch a container
@@ -101,6 +142,27 @@ Exit with `CTRL-D`
## Docker Compose
+Docker compose is a tool for defining and running multi-container Docker applications. You can see it as a shortcut of the `docker run` command.
+You can also give instructions to build a container.
+With a single command you can create and start all the services from your configuration.
+
+Docker compose is based on a yaml file named by default docker-compose.yml.
+
+
+```docker-compose.yaml
+version: '3.8'
+services:
+ web:
+ build: .
+ ports:
+ - "5000:5000"
+ redis:
+ image: "redis:alpine"
+```
+
+
+### Commands
+
Launch containers
```
docker-compose up
@@ -109,7 +171,7 @@ Launch containers and force rebuild
```
docker-compose up --build
```
-Launch containers in detached mode
+Launch containers in detached mode (background)
```
docker-compose up -d
```
diff --git a/ops/cours/app.py b/ops/cours/app.py
index fbce0a3b..9660184e 100644
--- a/ops/cours/app.py
+++ b/ops/cours/app.py
@@ -1,7 +1,8 @@
from flask import Flask
+
app = Flask(__name__)
-@app.route('/')
-def hello_world():
- return 'Hello, Docker!'
+@app.route("/")
+def hello_world():
+ return "Hello, Docker!"
diff --git a/ops/cours/docker-compose.yml b/ops/cours/docker-compose.yml
index 5ebda332..3d862939 100644
--- a/ops/cours/docker-compose.yml
+++ b/ops/cours/docker-compose.yml
@@ -6,9 +6,11 @@ services:
context: .
dockerfile: Dockerfile
ports:
- - 5000:5000
+ - "5000:5000"
+ volumes:
+ - .:/app
front:
image: nginx:latest
ports:
- - 80:80
\ No newline at end of file
+ - "80:80"
diff --git a/ops/cours/requirements.txt b/ops/cours/requirements.txt
index 2077213c..2ae9d047 100644
--- a/ops/cours/requirements.txt
+++ b/ops/cours/requirements.txt
@@ -1 +1,2 @@
-Flask
\ No newline at end of file
+Flask
+pytest
diff --git a/ops/cours/test.py b/ops/cours/test.py
new file mode 100644
index 00000000..e69de29b
diff --git a/ops/tp/api/Dockerfile b/ops/tp/api/Dockerfile
index 85c1eddb..15c1ab32 100644
--- a/ops/tp/api/Dockerfile
+++ b/ops/tp/api/Dockerfile
@@ -5,4 +5,4 @@ WORKDIR /app
COPY . .
RUN pip3 install -r requirements.txt
-CMD [ "python3", "-m" , "flask", "--debug", "run", "--host=0.0.0.0" ]
+CMD [ "python3", "-m" , "flask", "--debug", "run", "--host=0.0.0.0", "--port=5001" ]
diff --git a/ops/tp/api/app.py b/ops/tp/api/app.py
index 93b6a7bf..67dc567e 100644
--- a/ops/tp/api/app.py
+++ b/ops/tp/api/app.py
@@ -1,7 +1,8 @@
from flask import Flask
+
app = Flask(__name__)
-@app.route('/')
-def hello_world():
- return 'Hello from api!'
+@app.route("/")
+def hello_world():
+ return "Hello from api!"
diff --git a/ops/tp/docker-compose.yml b/ops/tp/docker-compose.yml
index 268cd3b4..1838aa7c 100644
--- a/ops/tp/docker-compose.yml
+++ b/ops/tp/docker-compose.yml
@@ -6,7 +6,7 @@ services:
context: api
dockerfile: Dockerfile
ports:
- - 5000:5000
+ - "5001:5001"
volumes:
- ./api:/app
@@ -14,3 +14,7 @@ services:
build:
context: front
dockerfile: Dockerfile
+ ports:
+ - "8080:8080"
+ volumes:
+ - ./front:/app
diff --git a/ops/tp/front/app.py b/ops/tp/front/app.py
index 9e0a3e8e..7e5d45b8 100644
--- a/ops/tp/front/app.py
+++ b/ops/tp/front/app.py
@@ -3,13 +3,16 @@
app = Flask(__name__)
-# @app.context_processor
-# def utility_processor():
-# def request_api(url=API_URL):
-# response = requests.get(f"http://{url}").text
-# return response
-# return dict(request_api=request_api)
-
-@app.route('/')
+API_URL = "api:5001"
+
+@app.context_processor
+def utility_processor():
+ def request_api(url=API_URL):
+ response = requests.get(f"http://{url}").text
+ return response
+ return dict(request_api=request_api)
+
+
+@app.route("/")
def hello():
- return render_template('./index.html')
\ No newline at end of file
+ return render_template("./index.html")
diff --git a/ops/tp/front/templates/index.html b/ops/tp/front/templates/index.html
index a956dc04..d765df15 100644
--- a/ops/tp/front/templates/index.html
+++ b/ops/tp/front/templates/index.html
@@ -7,5 +7,6 @@
Hello from front!
+ API response: {{ request_api() }}