From dcf99d49109017a98777fe937ed1651da3667c02 Mon Sep 17 00:00:00 2001 From: Loan Robert Date: Mon, 11 Mar 2024 17:39:56 +0100 Subject: [PATCH 1/2] refactor: API endpoint refactoring and testing --- web/b3desk/__init__.py | 2 + web/b3desk/endpoints/api.py | 28 ++++++++ web/b3desk/endpoints/meetings.py | 26 -------- web/b3desk/session.py | 4 ++ web/tests/conftest.py | 53 +++++++++++++-- web/tests/test_api.py | 109 +++++++++++++++++++++++++++++++ 6 files changed, 192 insertions(+), 30 deletions(-) create mode 100644 web/b3desk/endpoints/api.py create mode 100644 web/tests/test_api.py diff --git a/web/b3desk/__init__.py b/web/b3desk/__init__.py index 91088da0..e4101b13 100644 --- a/web/b3desk/__init__.py +++ b/web/b3desk/__init__.py @@ -171,11 +171,13 @@ def setup_endpoints(app): import b3desk.endpoints.public import b3desk.endpoints.join import b3desk.endpoints.meetings + import b3desk.endpoints.api import b3desk.endpoints.meeting_files app.register_blueprint(b3desk.endpoints.public.bp) app.register_blueprint(b3desk.endpoints.join.bp) app.register_blueprint(b3desk.endpoints.meetings.bp) + app.register_blueprint(b3desk.endpoints.api.bp) app.register_blueprint(b3desk.endpoints.meeting_files.bp) diff --git a/web/b3desk/endpoints/api.py b/web/b3desk/endpoints/api.py new file mode 100644 index 00000000..e62cd1f2 --- /dev/null +++ b/web/b3desk/endpoints/api.py @@ -0,0 +1,28 @@ +from b3desk.models.users import get_or_create_user +from flask import Blueprint +from flask import request + +from .. import auth + + +bp = Blueprint("api", __name__) + + +@bp.route("/api/meetings") +@auth.token_auth("default", scopes_required=["profile", "email"]) +def api_meetings(): + client = auth.clients["default"] + access_token = auth._parse_access_token(request) + userinfo = client.userinfo_request(access_token).to_dict() + user = get_or_create_user(userinfo) + + return { + "meetings": [ + { + "name": m.name, + "moderator_url": m.get_signin_url("moderator"), + "attendee_url": m.get_signin_url("attendee"), + } + for m in user.meetings + ] + } diff --git a/web/b3desk/endpoints/meetings.py b/web/b3desk/endpoints/meetings.py index 3b1ad229..93960685 100644 --- a/web/b3desk/endpoints/meetings.py +++ b/web/b3desk/endpoints/meetings.py @@ -16,7 +16,6 @@ from b3desk.models import db from b3desk.models.meetings import get_quick_meeting_from_user_and_random_string from b3desk.models.meetings import Meeting -from b3desk.models.users import get_or_create_user from b3desk.models.users import User from flask import abort from flask import Blueprint @@ -50,31 +49,6 @@ def meeting_mailto_params(meeting, role): ).replace("\n", "%0D%0A") -@bp.route("/api/meetings") -@auth.token_auth(provider_name="default") -def api_meetings(): - # TODO: probably unused - if not auth.current_token_identity: - return redirect(url_for("public.index")) - - info = { - "given_name": auth.current_token_identity["given_name"], - "family_name": auth.current_token_identity["family_name"], - "email": auth.current_token_identity["email"], - } - user = get_or_create_user(info) - return { - "meetings": [ - { - "name": m.name, - "moderator_url": m.get_signin_url("moderator"), - "attendee_url": m.get_signin_url("attendee"), - } - for m in user.meetings - ] - } - - @bp.route("/meeting/mail", methods=["POST"]) def quick_mail_meeting(): #### Almost the same as quick meeting but we do not redirect to join diff --git a/web/b3desk/session.py b/web/b3desk/session.py index da59b94b..9456faa0 100644 --- a/web/b3desk/session.py +++ b/web/b3desk/session.py @@ -2,6 +2,7 @@ from b3desk.models.users import get_or_create_user from flask import abort +from flask import current_app from flask import g from flask import session from flask_pyoidc.user_session import UserSession @@ -12,6 +13,9 @@ def get_current_user(): user_session = UserSession(session) info = user_session.userinfo g.user = get_or_create_user(info) + current_app.logger.debug( + f"User authenticated with token: {user_session.access_token}" + ) return g.user diff --git a/web/tests/conftest.py b/web/tests/conftest.py index aadc240c..07223833 100644 --- a/web/tests/conftest.py +++ b/web/tests/conftest.py @@ -1,3 +1,4 @@ +import datetime import threading import time import uuid @@ -17,6 +18,21 @@ from b3desk.models import db +@pytest.fixture +def iam_user(iam_server): + iam_user = iam_server.models.User( + id="user_id", + emails=["alice@domain.tld"], + given_name="Alice", + user_name="Alice_user_name", + family_name="Cooper", + ) + iam_user.save() + + yield iam_user + iam_user.delete() + + @pytest.fixture def iam_client(iam_server): iam_client = iam_server.models.Client( @@ -36,6 +52,28 @@ def iam_client(iam_server): iam_client.delete() +@pytest.fixture +def iam_token(iam_server, iam_client, iam_user): + iam_token = iam_server.models.Token( + access_token="access_token_example", + audience=iam_client, + client=iam_client, + id="token_id", + issue_date=datetime.datetime.now(tz=datetime.UTC), + lifetime=36000, + refresh_token="refresh_token_example", + revokation_date=None, + scope=["openid", "profile", "email"], + subject=iam_user, + token_id="token_id", + type="access_token", + ) + iam_token.save() + + yield iam_token + iam_token.delete() + + @pytest.fixture def configuration(tmp_path, iam_server, iam_client, smtpd): smtpd.config.use_starttls = True @@ -115,19 +153,23 @@ def meeting(client_app, user): @pytest.fixture -def user(client_app): +def user(client_app, iam_user): from b3desk.models.users import User - user = User(email="alice@domain.tld", given_name="Alice", family_name="Cooper") + user = User( + email=iam_user.emails[0], + given_name=iam_user.given_name, + family_name=iam_user.family_name, + ) user.save() yield user @pytest.fixture -def authenticated_user(client_app, user): +def authenticated_user(client_app, user, iam_token, iam_server, iam_user): with client_app.session_transaction() as session: - session["access_token"] = "" + session["access_token"] = iam_token.access_token session["access_token_expires_at"] = "" session["current_provider"] = "default" session["id_token"] = "" @@ -142,6 +184,9 @@ def authenticated_user(client_app, user): } session["refresh_token"] = "" + iam_server.login(iam_user) + iam_server.consent(iam_user) + yield user diff --git a/web/tests/test_api.py b/web/tests/test_api.py new file mode 100644 index 00000000..aef6c6d2 --- /dev/null +++ b/web/tests/test_api.py @@ -0,0 +1,109 @@ +import datetime + + +def test_api_meetings_nominal(client_app, user, meeting, iam_token): + res = client_app.get( + "/api/meetings", headers={"Authorization": f"Bearer {iam_token.access_token}"} + ) + assert res.json["meetings"] + assert res.json["meetings"][0]["name"] == "meeting" + assert ( + f"/meeting/signin/{meeting.id}/creator/{user.id}/hash/" + in res.json["meetings"][0]["moderator_url"] + ) + assert ( + f"/meeting/signin/{meeting.id}/creator/{user.id}/hash/" + in res.json["meetings"][0]["attendee_url"] + ) + + +def test_api_meetings_no_token(client_app): + client_app.get("/api/meetings", status=401) + + +def test_api_meetings_invalid_token(client_app): + client_app.get( + "/api/meetings", headers={"Authorization": "Bearer invalid-token"}, status=403 + ) + + +def test_api_meetings_token_expired(client_app, iam_server, iam_client, iam_user, user): + iam_token = iam_server.models.Token( + access_token="access_token_example", + audience=iam_client, + client=iam_client, + id="token_id", + issue_date=datetime.datetime(2000, 1, 1, tzinfo=datetime.UTC), + lifetime=36000, + refresh_token="refresh_token_example", + revokation_date=None, + scope=["openid", "profile", "email"], + subject=iam_user, + token_id="token_id", + type="access_token", + ) + iam_token.save() + + client_app.get( + "/api/meetings", + headers={"Authorization": f"Bearer {iam_token.access_token}"}, + status=403, + ) + + iam_token.delete() + + +def test_api_meetings_client_id_missing_in_token_audience( + client_app, iam_server, iam_client, iam_user, user +): + iam_token = iam_server.models.Token( + access_token="access_token_example", + audience="some-other-audience", + client=iam_client, + id="token_id", + issue_date=datetime.datetime.now(tz=datetime.UTC), + lifetime=36000, + refresh_token="refresh_token_example", + revokation_date=None, + scope=["openid", "profile", "email"], + subject=iam_user, + token_id="token_id", + type="access_token", + ) + iam_token.save() + + client_app.get( + "/api/meetings", + headers={"Authorization": f"Bearer {iam_token.access_token}"}, + status=403, + ) + + iam_token.delete() + + +def test_api_meetings_missing_scope_in_token( + client_app, iam_server, iam_client, iam_user, user +): + iam_token = iam_server.models.Token( + access_token="access_token_example", + audience=iam_client, + client=iam_client, + id="token_id", + issue_date=datetime.datetime.now(tz=datetime.UTC), + lifetime=36000, + refresh_token="refresh_token_example", + revokation_date=None, + scope=["openid"], + subject=iam_user, + token_id="token_id", + type="access_token", + ) + iam_token.save() + + client_app.get( + "/api/meetings", + headers={"Authorization": f"Bearer {iam_token.access_token}"}, + status=403, + ) + + iam_token.delete() From 95acd9944e59f06181972089a75a4f89e39efcfc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=89loi=20Rivard?= Date: Tue, 12 Mar 2024 10:13:56 +0100 Subject: [PATCH 2/2] documentation: API documentation --- documentation/_static/keycloak-audience.png | Bin 0 -> 41776 bytes documentation/developers/api.md | 63 ++++++++++++++++++++ documentation/developers/index.rst | 1 + web/tests/conftest.py | 2 +- web/tests/test_api.py | 6 +- 5 files changed, 68 insertions(+), 4 deletions(-) create mode 100644 documentation/_static/keycloak-audience.png create mode 100644 documentation/developers/api.md diff --git a/documentation/_static/keycloak-audience.png b/documentation/_static/keycloak-audience.png new file mode 100644 index 0000000000000000000000000000000000000000..cc53fb39a10bdc20826b521f2fa5db4662b2c9ff GIT binary patch literal 41776 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
    wl3>5Gq?(KIQTWP0<1g?>6Z!Aw@z;3bw2V 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 literal 0 HcmV?d00001 diff --git a/documentation/developers/api.md b/documentation/developers/api.md new file mode 100644 index 00000000..94b03654 --- /dev/null +++ b/documentation/developers/api.md @@ -0,0 +1,63 @@ +# API publique + +## Points d'accès + +Une API publique est disponible sur `/api/meetings`. Cette API est utilisée par les greffons Thunderbird et Outlook. + +Les réponses sont au format json sous la forme suivante : + +```json +{ + "meetings": [ + { + "attendee_url": "https://example.tld/meeting/signin/1234/creator/5678/hash/27955e8c3a16ecadbb3cf61df7806a04ce6fd18c", + "moderator_url": "https://example.tld/meeting/signin/1234/creator/5678/hash/3e73643801d5013d389f8fc610e258aba38e597d", + "name": "Mon Séminaire" + } + ] +} +``` + +## Authentification + +L'authentification à l'API se fait en passant un jeton OIDC émis par le serveur d'identifié configuré dans `OIDC_ISSUER`. + +### Tests + +On peut tester le bon fonctionnement de l'API comme ceci (*en renseignant au préalable la variable `$TOKEN`*). + +```bash +curl -s -H "Authorization:Bearer $TOKEN" https://example.tld/api/meetings +``` + +### Problèmes de connexion + +#### Jeton manquant + +Lorsque le jeton d'identification n'a pas été fourni dans la requête, l'API retourne des codes d'erreur HTTP 401. + +#### Jeton expiré + +Lorsque le jeton est expiré, l'API retourne des codes d'erreur HTTP 403. + +#### Mauvaise audience du jeton + +Lorsque l'audience du jeton est incorrecte, l'API retourne des codes d'erreur 403. +Dans les faits il faut s'assurer que le paramètre `aud` du jeton contient bien l'ID client OIDC de l'application, définie dans le paramètre `OIDC_CLIENT_ID` de l'application, et prenant par défaut la valeur `bbb-vision`. +On peut vérifier l'audience d'un token avec des outils tels que [jwt.io](https://jwt.io). + +Par défaut, keycloak ne remplit pas l'audience du jeton avec l'ID client. +Il est nécessaire d'effectuer une configuration dans la console d'aministration de keycloak comme celle-ci (c'est un exemple, d'autres sont probablement possibles) : + +1. Se rendre sur la console d'administration de keycloak +2. Se rendre dans le menu « Clients » +3. Sélectionner le client « bbb-visio » +4. Se rendre dans l'onglet « Mappers » +5. Cliquer sur le bouton « Create » +6. Remplir (par exemple) le formulaire comme ceci: + - Name: bbb-visio-audience + - Mapper Type: audience + - Included Custom Audience: bbb-visio + + ![keycloak](../_static/keycloak-audience.png) +7. Générer un nouveau token et vérifier qu'il contient bien la valeur `bbb-vision` dans le paramètre `aud`. diff --git a/documentation/developers/index.rst b/documentation/developers/index.rst index cf86420d..2b091a14 100644 --- a/documentation/developers/index.rst +++ b/documentation/developers/index.rst @@ -6,6 +6,7 @@ Cette documentation est à destination des développeurs. .. toctree:: :maxdepth: 2 + api contributing ci dockerPersistence diff --git a/web/tests/conftest.py b/web/tests/conftest.py index 07223833..5ca31a5f 100644 --- a/web/tests/conftest.py +++ b/web/tests/conftest.py @@ -59,7 +59,7 @@ def iam_token(iam_server, iam_client, iam_user): audience=iam_client, client=iam_client, id="token_id", - issue_date=datetime.datetime.now(tz=datetime.UTC), + issue_date=datetime.datetime.now(tz=datetime.timezone.utc), lifetime=36000, refresh_token="refresh_token_example", revokation_date=None, diff --git a/web/tests/test_api.py b/web/tests/test_api.py index aef6c6d2..18896e0d 100644 --- a/web/tests/test_api.py +++ b/web/tests/test_api.py @@ -33,7 +33,7 @@ def test_api_meetings_token_expired(client_app, iam_server, iam_client, iam_user audience=iam_client, client=iam_client, id="token_id", - issue_date=datetime.datetime(2000, 1, 1, tzinfo=datetime.UTC), + issue_date=datetime.datetime(2000, 1, 1, tzinfo=datetime.timezone.utc), lifetime=36000, refresh_token="refresh_token_example", revokation_date=None, @@ -61,7 +61,7 @@ def test_api_meetings_client_id_missing_in_token_audience( audience="some-other-audience", client=iam_client, id="token_id", - issue_date=datetime.datetime.now(tz=datetime.UTC), + issue_date=datetime.datetime.now(tz=datetime.timezone.utc), lifetime=36000, refresh_token="refresh_token_example", revokation_date=None, @@ -89,7 +89,7 @@ def test_api_meetings_missing_scope_in_token( audience=iam_client, client=iam_client, id="token_id", - issue_date=datetime.datetime.now(tz=datetime.UTC), + issue_date=datetime.datetime.now(tz=datetime.timezone.utc), lifetime=36000, refresh_token="refresh_token_example", revokation_date=None,