From 3e586e615d234626b3e44c021f697d6d50ff2a71 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?V=C3=ADctor=20Fern=C3=A1ndez=20Poyatos?= Date: Thu, 13 Feb 2025 16:54:38 +0100 Subject: [PATCH] feat(social-login): Add social login integration for Google and Github OAuth providers (#6906) --- api/.env.example | 9 + api/CHANGELOG.md | 4 +- api/poetry.lock | 301 ++++++++++-------- api/pyproject.toml | 3 +- api/src/backend/api/adapters.py | 57 ++++ api/src/backend/api/db_router.py | 17 +- api/src/backend/api/specs/v1.yaml | 106 +++++- .../tests/integration/test_authentication.py | 121 +++++++ api/src/backend/api/utils.py | 18 +- api/src/backend/api/v1/serializers.py | 138 +++++--- api/src/backend/api/v1/urls.py | 6 + api/src/backend/api/v1/views.py | 95 +++++- api/src/backend/config/django/base.py | 11 + .../backend/config/settings/social_login.py | 53 +++ 14 files changed, 759 insertions(+), 180 deletions(-) create mode 100644 api/src/backend/api/adapters.py create mode 100644 api/src/backend/config/settings/social_login.py diff --git a/api/.env.example b/api/.env.example index 4b0894b3ba..192317fa02 100644 --- a/api/.env.example +++ b/api/.env.example @@ -39,3 +39,12 @@ POSTGRES_DB=prowler_db VALKEY_HOST=[localhost|valkey] VALKEY_PORT=6379 VALKEY_DB=0 + +# Social login credentials +DJANGO_GOOGLE_OAUTH_CLIENT_ID="" +DJANGO_GOOGLE_OAUTH_CLIENT_SECRET="" +DJANGO_GOOGLE_OAUTH_CALLBACK_URL="" + +DJANGO_GITHUB_OAUTH_CLIENT_ID="" +DJANGO_GITHUB_OAUTH_CLIENT_SECRET="" +DJANGO_GITHUB_OAUTH_CALLBACK_URL="" diff --git a/api/CHANGELOG.md b/api/CHANGELOG.md index 0a3acb7fd4..34f19eb29a 100644 --- a/api/CHANGELOG.md +++ b/api/CHANGELOG.md @@ -4,8 +4,10 @@ All notable changes to the **Prowler API** are documented in this file. --- -## [Unreleased] +## [v1.5.0] (Prowler UNRELEASED) +### Added +- Social login integration with Google and GitHub [(#6906)](https://github.com/prowler-cloud/prowler/pull/6906) --- diff --git a/api/poetry.lock b/api/poetry.lock index 5ce4f9234e..c05ae59d8c 100644 --- a/api/poetry.lock +++ b/api/poetry.lock @@ -13,98 +13,103 @@ files = [ [[package]] name = "aiohappyeyeballs" -version = "2.4.4" +version = "2.4.6" description = "Happy Eyeballs for asyncio" optional = false -python-versions = ">=3.8" +python-versions = ">=3.9" files = [ - {file = "aiohappyeyeballs-2.4.4-py3-none-any.whl", hash = "sha256:a980909d50efcd44795c4afeca523296716d50cd756ddca6af8c65b996e27de8"}, - {file = "aiohappyeyeballs-2.4.4.tar.gz", hash = "sha256:5fdd7d87889c63183afc18ce9271f9b0a7d32c2303e394468dd45d514a757745"}, + {file = "aiohappyeyeballs-2.4.6-py3-none-any.whl", hash = "sha256:147ec992cf873d74f5062644332c539fcd42956dc69453fe5204195e560517e1"}, + {file = "aiohappyeyeballs-2.4.6.tar.gz", hash = "sha256:9b05052f9042985d32ecbe4b59a77ae19c006a78f1344d7fdad69d28ded3d0b0"}, ] [[package]] name = "aiohttp" -version = "3.11.11" +version = "3.11.12" description = "Async http client/server framework (asyncio)" optional = false python-versions = ">=3.9" files = [ - {file = "aiohttp-3.11.11-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:a60804bff28662cbcf340a4d61598891f12eea3a66af48ecfdc975ceec21e3c8"}, - {file = "aiohttp-3.11.11-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:4b4fa1cb5f270fb3eab079536b764ad740bb749ce69a94d4ec30ceee1b5940d5"}, - {file = "aiohttp-3.11.11-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:731468f555656767cda219ab42e033355fe48c85fbe3ba83a349631541715ba2"}, - {file = "aiohttp-3.11.11-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cb23d8bb86282b342481cad4370ea0853a39e4a32a0042bb52ca6bdde132df43"}, - {file = "aiohttp-3.11.11-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f047569d655f81cb70ea5be942ee5d4421b6219c3f05d131f64088c73bb0917f"}, - {file = "aiohttp-3.11.11-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:dd7659baae9ccf94ae5fe8bfaa2c7bc2e94d24611528395ce88d009107e00c6d"}, - {file = "aiohttp-3.11.11-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:af01e42ad87ae24932138f154105e88da13ce7d202a6de93fafdafb2883a00ef"}, - {file = "aiohttp-3.11.11-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5854be2f3e5a729800bac57a8d76af464e160f19676ab6aea74bde18ad19d438"}, - {file = "aiohttp-3.11.11-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:6526e5fb4e14f4bbf30411216780c9967c20c5a55f2f51d3abd6de68320cc2f3"}, - {file = "aiohttp-3.11.11-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:85992ee30a31835fc482468637b3e5bd085fa8fe9392ba0bdcbdc1ef5e9e3c55"}, - {file = "aiohttp-3.11.11-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:88a12ad8ccf325a8a5ed80e6d7c3bdc247d66175afedbe104ee2aaca72960d8e"}, - {file = "aiohttp-3.11.11-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:0a6d3fbf2232e3a08c41eca81ae4f1dff3d8f1a30bae415ebe0af2d2458b8a33"}, - {file = "aiohttp-3.11.11-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:84a585799c58b795573c7fa9b84c455adf3e1d72f19a2bf498b54a95ae0d194c"}, - {file = "aiohttp-3.11.11-cp310-cp310-win32.whl", hash = "sha256:bfde76a8f430cf5c5584553adf9926534352251d379dcb266ad2b93c54a29745"}, - {file = "aiohttp-3.11.11-cp310-cp310-win_amd64.whl", hash = "sha256:0fd82b8e9c383af11d2b26f27a478640b6b83d669440c0a71481f7c865a51da9"}, - {file = "aiohttp-3.11.11-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:ba74ec819177af1ef7f59063c6d35a214a8fde6f987f7661f4f0eecc468a8f76"}, - {file = "aiohttp-3.11.11-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:4af57160800b7a815f3fe0eba9b46bf28aafc195555f1824555fa2cfab6c1538"}, - {file = "aiohttp-3.11.11-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:ffa336210cf9cd8ed117011085817d00abe4c08f99968deef0013ea283547204"}, - {file = "aiohttp-3.11.11-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:81b8fe282183e4a3c7a1b72f5ade1094ed1c6345a8f153506d114af5bf8accd9"}, - {file = "aiohttp-3.11.11-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3af41686ccec6a0f2bdc66686dc0f403c41ac2089f80e2214a0f82d001052c03"}, - {file = "aiohttp-3.11.11-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:70d1f9dde0e5dd9e292a6d4d00058737052b01f3532f69c0c65818dac26dc287"}, - {file = "aiohttp-3.11.11-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:249cc6912405917344192b9f9ea5cd5b139d49e0d2f5c7f70bdfaf6b4dbf3a2e"}, - {file = "aiohttp-3.11.11-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0eb98d90b6690827dcc84c246811feeb4e1eea683c0eac6caed7549be9c84665"}, - {file = "aiohttp-3.11.11-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:ec82bf1fda6cecce7f7b915f9196601a1bd1a3079796b76d16ae4cce6d0ef89b"}, - {file = "aiohttp-3.11.11-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:9fd46ce0845cfe28f108888b3ab17abff84ff695e01e73657eec3f96d72eef34"}, - {file = "aiohttp-3.11.11-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:bd176afcf8f5d2aed50c3647d4925d0db0579d96f75a31e77cbaf67d8a87742d"}, - {file = "aiohttp-3.11.11-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:ec2aa89305006fba9ffb98970db6c8221541be7bee4c1d027421d6f6df7d1ce2"}, - {file = "aiohttp-3.11.11-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:92cde43018a2e17d48bb09c79e4d4cb0e236de5063ce897a5e40ac7cb4878773"}, - {file = "aiohttp-3.11.11-cp311-cp311-win32.whl", hash = "sha256:aba807f9569455cba566882c8938f1a549f205ee43c27b126e5450dc9f83cc62"}, - {file = "aiohttp-3.11.11-cp311-cp311-win_amd64.whl", hash = "sha256:ae545f31489548c87b0cced5755cfe5a5308d00407000e72c4fa30b19c3220ac"}, - {file = "aiohttp-3.11.11-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:e595c591a48bbc295ebf47cb91aebf9bd32f3ff76749ecf282ea7f9f6bb73886"}, - {file = "aiohttp-3.11.11-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:3ea1b59dc06396b0b424740a10a0a63974c725b1c64736ff788a3689d36c02d2"}, - {file = "aiohttp-3.11.11-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8811f3f098a78ffa16e0ea36dffd577eb031aea797cbdba81be039a4169e242c"}, - {file = "aiohttp-3.11.11-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bd7227b87a355ce1f4bf83bfae4399b1f5bb42e0259cb9405824bd03d2f4336a"}, - {file = "aiohttp-3.11.11-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d40f9da8cabbf295d3a9dae1295c69975b86d941bc20f0a087f0477fa0a66231"}, - {file = "aiohttp-3.11.11-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ffb3dc385f6bb1568aa974fe65da84723210e5d9707e360e9ecb51f59406cd2e"}, - {file = "aiohttp-3.11.11-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a8f5f7515f3552d899c61202d99dcb17d6e3b0de777900405611cd747cecd1b8"}, - {file = "aiohttp-3.11.11-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3499c7ffbfd9c6a3d8d6a2b01c26639da7e43d47c7b4f788016226b1e711caa8"}, - {file = "aiohttp-3.11.11-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8e2bf8029dbf0810c7bfbc3e594b51c4cc9101fbffb583a3923aea184724203c"}, - {file = "aiohttp-3.11.11-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:b6212a60e5c482ef90f2d788835387070a88d52cf6241d3916733c9176d39eab"}, - {file = "aiohttp-3.11.11-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:d119fafe7b634dbfa25a8c597718e69a930e4847f0b88e172744be24515140da"}, - {file = "aiohttp-3.11.11-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:6fba278063559acc730abf49845d0e9a9e1ba74f85f0ee6efd5803f08b285853"}, - {file = "aiohttp-3.11.11-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:92fc484e34b733704ad77210c7957679c5c3877bd1e6b6d74b185e9320cc716e"}, - {file = "aiohttp-3.11.11-cp312-cp312-win32.whl", hash = "sha256:9f5b3c1ed63c8fa937a920b6c1bec78b74ee09593b3f5b979ab2ae5ef60d7600"}, - {file = "aiohttp-3.11.11-cp312-cp312-win_amd64.whl", hash = "sha256:1e69966ea6ef0c14ee53ef7a3d68b564cc408121ea56c0caa2dc918c1b2f553d"}, - {file = "aiohttp-3.11.11-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:541d823548ab69d13d23730a06f97460f4238ad2e5ed966aaf850d7c369782d9"}, - {file = "aiohttp-3.11.11-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:929f3ed33743a49ab127c58c3e0a827de0664bfcda566108989a14068f820194"}, - {file = "aiohttp-3.11.11-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:0882c2820fd0132240edbb4a51eb8ceb6eef8181db9ad5291ab3332e0d71df5f"}, - {file = "aiohttp-3.11.11-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b63de12e44935d5aca7ed7ed98a255a11e5cb47f83a9fded7a5e41c40277d104"}, - {file = "aiohttp-3.11.11-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:aa54f8ef31d23c506910c21163f22b124facb573bff73930735cf9fe38bf7dff"}, - {file = "aiohttp-3.11.11-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a344d5dc18074e3872777b62f5f7d584ae4344cd6006c17ba12103759d407af3"}, - {file = "aiohttp-3.11.11-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0b7fb429ab1aafa1f48578eb315ca45bd46e9c37de11fe45c7f5f4138091e2f1"}, - {file = "aiohttp-3.11.11-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c341c7d868750e31961d6d8e60ff040fb9d3d3a46d77fd85e1ab8e76c3e9a5c4"}, - {file = "aiohttp-3.11.11-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ed9ee95614a71e87f1a70bc81603f6c6760128b140bc4030abe6abaa988f1c3d"}, - {file = "aiohttp-3.11.11-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:de8d38f1c2810fa2a4f1d995a2e9c70bb8737b18da04ac2afbf3971f65781d87"}, - {file = "aiohttp-3.11.11-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:a9b7371665d4f00deb8f32208c7c5e652059b0fda41cf6dbcac6114a041f1cc2"}, - {file = "aiohttp-3.11.11-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:620598717fce1b3bd14dd09947ea53e1ad510317c85dda2c9c65b622edc96b12"}, - {file = "aiohttp-3.11.11-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:bf8d9bfee991d8acc72d060d53860f356e07a50f0e0d09a8dfedea1c554dd0d5"}, - {file = "aiohttp-3.11.11-cp313-cp313-win32.whl", hash = "sha256:9d73ee3725b7a737ad86c2eac5c57a4a97793d9f442599bea5ec67ac9f4bdc3d"}, - {file = "aiohttp-3.11.11-cp313-cp313-win_amd64.whl", hash = "sha256:c7a06301c2fb096bdb0bd25fe2011531c1453b9f2c163c8031600ec73af1cc99"}, - {file = "aiohttp-3.11.11-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:3e23419d832d969f659c208557de4a123e30a10d26e1e14b73431d3c13444c2e"}, - {file = "aiohttp-3.11.11-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:21fef42317cf02e05d3b09c028712e1d73a9606f02467fd803f7c1f39cc59add"}, - {file = "aiohttp-3.11.11-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:1f21bb8d0235fc10c09ce1d11ffbd40fc50d3f08a89e4cf3a0c503dc2562247a"}, - {file = "aiohttp-3.11.11-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1642eceeaa5ab6c9b6dfeaaa626ae314d808188ab23ae196a34c9d97efb68350"}, - {file = "aiohttp-3.11.11-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2170816e34e10f2fd120f603e951630f8a112e1be3b60963a1f159f5699059a6"}, - {file = "aiohttp-3.11.11-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8be8508d110d93061197fd2d6a74f7401f73b6d12f8822bbcd6d74f2b55d71b1"}, - {file = "aiohttp-3.11.11-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4eed954b161e6b9b65f6be446ed448ed3921763cc432053ceb606f89d793927e"}, - {file = "aiohttp-3.11.11-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d6c9af134da4bc9b3bd3e6a70072509f295d10ee60c697826225b60b9959acdd"}, - {file = "aiohttp-3.11.11-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:44167fc6a763d534a6908bdb2592269b4bf30a03239bcb1654781adf5e49caf1"}, - {file = "aiohttp-3.11.11-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:479b8c6ebd12aedfe64563b85920525d05d394b85f166b7873c8bde6da612f9c"}, - {file = "aiohttp-3.11.11-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:10b4ff0ad793d98605958089fabfa350e8e62bd5d40aa65cdc69d6785859f94e"}, - {file = "aiohttp-3.11.11-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:b540bd67cfb54e6f0865ceccd9979687210d7ed1a1cc8c01f8e67e2f1e883d28"}, - {file = "aiohttp-3.11.11-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:1dac54e8ce2ed83b1f6b1a54005c87dfed139cf3f777fdc8afc76e7841101226"}, - {file = "aiohttp-3.11.11-cp39-cp39-win32.whl", hash = "sha256:568c1236b2fde93b7720f95a890741854c1200fba4a3471ff48b2934d2d93fd3"}, - {file = "aiohttp-3.11.11-cp39-cp39-win_amd64.whl", hash = "sha256:943a8b052e54dfd6439fd7989f67fc6a7f2138d0a2cf0a7de5f18aa4fe7eb3b1"}, - {file = "aiohttp-3.11.11.tar.gz", hash = "sha256:bb49c7f1e6ebf3821a42d81d494f538107610c3a705987f53068546b0e90303e"}, + {file = "aiohttp-3.11.12-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:aa8a8caca81c0a3e765f19c6953416c58e2f4cc1b84829af01dd1c771bb2f91f"}, + {file = "aiohttp-3.11.12-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:84ede78acde96ca57f6cf8ccb8a13fbaf569f6011b9a52f870c662d4dc8cd854"}, + {file = "aiohttp-3.11.12-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:584096938a001378484aa4ee54e05dc79c7b9dd933e271c744a97b3b6f644957"}, + {file = "aiohttp-3.11.12-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:392432a2dde22b86f70dd4a0e9671a349446c93965f261dbaecfaf28813e5c42"}, + {file = "aiohttp-3.11.12-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:88d385b8e7f3a870146bf5ea31786ef7463e99eb59e31db56e2315535d811f55"}, + {file = "aiohttp-3.11.12-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b10a47e5390c4b30a0d58ee12581003be52eedd506862ab7f97da7a66805befb"}, + {file = "aiohttp-3.11.12-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0b5263dcede17b6b0c41ef0c3ccce847d82a7da98709e75cf7efde3e9e3b5cae"}, + {file = "aiohttp-3.11.12-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:50c5c7b8aa5443304c55c262c5693b108c35a3b61ef961f1e782dd52a2f559c7"}, + {file = "aiohttp-3.11.12-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:d1c031a7572f62f66f1257db37ddab4cb98bfaf9b9434a3b4840bf3560f5e788"}, + {file = "aiohttp-3.11.12-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:7e44eba534381dd2687be50cbd5f2daded21575242ecfdaf86bbeecbc38dae8e"}, + {file = "aiohttp-3.11.12-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:145a73850926018ec1681e734cedcf2716d6a8697d90da11284043b745c286d5"}, + {file = "aiohttp-3.11.12-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:2c311e2f63e42c1bf86361d11e2c4a59f25d9e7aabdbdf53dc38b885c5435cdb"}, + {file = "aiohttp-3.11.12-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:ea756b5a7bac046d202a9a3889b9a92219f885481d78cd318db85b15cc0b7bcf"}, + {file = "aiohttp-3.11.12-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:526c900397f3bbc2db9cb360ce9c35134c908961cdd0ac25b1ae6ffcaa2507ff"}, + {file = "aiohttp-3.11.12-cp310-cp310-win32.whl", hash = "sha256:b8d3bb96c147b39c02d3db086899679f31958c5d81c494ef0fc9ef5bb1359b3d"}, + {file = "aiohttp-3.11.12-cp310-cp310-win_amd64.whl", hash = "sha256:7fe3d65279bfbee8de0fb4f8c17fc4e893eed2dba21b2f680e930cc2b09075c5"}, + {file = "aiohttp-3.11.12-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:87a2e00bf17da098d90d4145375f1d985a81605267e7f9377ff94e55c5d769eb"}, + {file = "aiohttp-3.11.12-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:b34508f1cd928ce915ed09682d11307ba4b37d0708d1f28e5774c07a7674cac9"}, + {file = "aiohttp-3.11.12-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:936d8a4f0f7081327014742cd51d320296b56aa6d324461a13724ab05f4b2933"}, + {file = "aiohttp-3.11.12-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2de1378f72def7dfb5dbd73d86c19eda0ea7b0a6873910cc37d57e80f10d64e1"}, + {file = "aiohttp-3.11.12-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b9d45dbb3aaec05cf01525ee1a7ac72de46a8c425cb75c003acd29f76b1ffe94"}, + {file = "aiohttp-3.11.12-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:930ffa1925393381e1e0a9b82137fa7b34c92a019b521cf9f41263976666a0d6"}, + {file = "aiohttp-3.11.12-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8340def6737118f5429a5df4e88f440746b791f8f1c4ce4ad8a595f42c980bd5"}, + {file = "aiohttp-3.11.12-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4016e383f91f2814e48ed61e6bda7d24c4d7f2402c75dd28f7e1027ae44ea204"}, + {file = "aiohttp-3.11.12-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:3c0600bcc1adfaaac321422d615939ef300df81e165f6522ad096b73439c0f58"}, + {file = "aiohttp-3.11.12-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:0450ada317a65383b7cce9576096150fdb97396dcfe559109b403c7242faffef"}, + {file = "aiohttp-3.11.12-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:850ff6155371fd802a280f8d369d4e15d69434651b844bde566ce97ee2277420"}, + {file = "aiohttp-3.11.12-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:8fd12d0f989c6099e7b0f30dc6e0d1e05499f3337461f0b2b0dadea6c64b89df"}, + {file = "aiohttp-3.11.12-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:76719dd521c20a58a6c256d058547b3a9595d1d885b830013366e27011ffe804"}, + {file = "aiohttp-3.11.12-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:97fe431f2ed646a3b56142fc81d238abcbaff08548d6912acb0b19a0cadc146b"}, + {file = "aiohttp-3.11.12-cp311-cp311-win32.whl", hash = "sha256:e10c440d142fa8b32cfdb194caf60ceeceb3e49807072e0dc3a8887ea80e8c16"}, + {file = "aiohttp-3.11.12-cp311-cp311-win_amd64.whl", hash = "sha256:246067ba0cf5560cf42e775069c5d80a8989d14a7ded21af529a4e10e3e0f0e6"}, + {file = "aiohttp-3.11.12-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:e392804a38353900c3fd8b7cacbea5132888f7129f8e241915e90b85f00e3250"}, + {file = "aiohttp-3.11.12-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:8fa1510b96c08aaad49303ab11f8803787c99222288f310a62f493faf883ede1"}, + {file = "aiohttp-3.11.12-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:dc065a4285307607df3f3686363e7f8bdd0d8ab35f12226362a847731516e42c"}, + {file = "aiohttp-3.11.12-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cddb31f8474695cd61fc9455c644fc1606c164b93bff2490390d90464b4655df"}, + {file = "aiohttp-3.11.12-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9dec0000d2d8621d8015c293e24589d46fa218637d820894cb7356c77eca3259"}, + {file = "aiohttp-3.11.12-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e3552fe98e90fdf5918c04769f338a87fa4f00f3b28830ea9b78b1bdc6140e0d"}, + {file = "aiohttp-3.11.12-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6dfe7f984f28a8ae94ff3a7953cd9678550dbd2a1f9bda5dd9c5ae627744c78e"}, + {file = "aiohttp-3.11.12-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a481a574af914b6e84624412666cbfbe531a05667ca197804ecc19c97b8ab1b0"}, + {file = "aiohttp-3.11.12-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:1987770fb4887560363b0e1a9b75aa303e447433c41284d3af2840a2f226d6e0"}, + {file = "aiohttp-3.11.12-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:a4ac6a0f0f6402854adca4e3259a623f5c82ec3f0c049374133bcb243132baf9"}, + {file = "aiohttp-3.11.12-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:c96a43822f1f9f69cc5c3706af33239489a6294be486a0447fb71380070d4d5f"}, + {file = "aiohttp-3.11.12-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:a5e69046f83c0d3cb8f0d5bd9b8838271b1bc898e01562a04398e160953e8eb9"}, + {file = "aiohttp-3.11.12-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:68d54234c8d76d8ef74744f9f9fc6324f1508129e23da8883771cdbb5818cbef"}, + {file = "aiohttp-3.11.12-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:c9fd9dcf9c91affe71654ef77426f5cf8489305e1c66ed4816f5a21874b094b9"}, + {file = "aiohttp-3.11.12-cp312-cp312-win32.whl", hash = "sha256:0ed49efcd0dc1611378beadbd97beb5d9ca8fe48579fc04a6ed0844072261b6a"}, + {file = "aiohttp-3.11.12-cp312-cp312-win_amd64.whl", hash = "sha256:54775858c7f2f214476773ce785a19ee81d1294a6bedc5cc17225355aab74802"}, + {file = "aiohttp-3.11.12-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:413ad794dccb19453e2b97c2375f2ca3cdf34dc50d18cc2693bd5aed7d16f4b9"}, + {file = "aiohttp-3.11.12-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:4a93d28ed4b4b39e6f46fd240896c29b686b75e39cc6992692e3922ff6982b4c"}, + {file = "aiohttp-3.11.12-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:d589264dbba3b16e8951b6f145d1e6b883094075283dafcab4cdd564a9e353a0"}, + {file = "aiohttp-3.11.12-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e5148ca8955affdfeb864aca158ecae11030e952b25b3ae15d4e2b5ba299bad2"}, + {file = "aiohttp-3.11.12-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:525410e0790aab036492eeea913858989c4cb070ff373ec3bc322d700bdf47c1"}, + {file = "aiohttp-3.11.12-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9bd8695be2c80b665ae3f05cb584093a1e59c35ecb7d794d1edd96e8cc9201d7"}, + {file = "aiohttp-3.11.12-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f0203433121484b32646a5f5ea93ae86f3d9559d7243f07e8c0eab5ff8e3f70e"}, + {file = "aiohttp-3.11.12-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:40cd36749a1035c34ba8d8aaf221b91ca3d111532e5ccb5fa8c3703ab1b967ed"}, + {file = "aiohttp-3.11.12-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a7442662afebbf7b4c6d28cb7aab9e9ce3a5df055fc4116cc7228192ad6cb484"}, + {file = "aiohttp-3.11.12-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:8a2fb742ef378284a50766e985804bd6adb5adb5aa781100b09befdbfa757b65"}, + {file = "aiohttp-3.11.12-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:2cee3b117a8d13ab98b38d5b6bdcd040cfb4181068d05ce0c474ec9db5f3c5bb"}, + {file = "aiohttp-3.11.12-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:f6a19bcab7fbd8f8649d6595624856635159a6527861b9cdc3447af288a00c00"}, + {file = "aiohttp-3.11.12-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:e4cecdb52aaa9994fbed6b81d4568427b6002f0a91c322697a4bfcc2b2363f5a"}, + {file = "aiohttp-3.11.12-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:30f546358dfa0953db92ba620101fefc81574f87b2346556b90b5f3ef16e55ce"}, + {file = "aiohttp-3.11.12-cp313-cp313-win32.whl", hash = "sha256:ce1bb21fc7d753b5f8a5d5a4bae99566386b15e716ebdb410154c16c91494d7f"}, + {file = "aiohttp-3.11.12-cp313-cp313-win_amd64.whl", hash = "sha256:f7914ab70d2ee8ab91c13e5402122edbc77821c66d2758abb53aabe87f013287"}, + {file = "aiohttp-3.11.12-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:7c3623053b85b4296cd3925eeb725e386644fd5bc67250b3bb08b0f144803e7b"}, + {file = "aiohttp-3.11.12-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:67453e603cea8e85ed566b2700efa1f6916aefbc0c9fcb2e86aaffc08ec38e78"}, + {file = "aiohttp-3.11.12-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:6130459189e61baac5a88c10019b21e1f0c6d00ebc770e9ce269475650ff7f73"}, + {file = "aiohttp-3.11.12-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9060addfa4ff753b09392efe41e6af06ea5dd257829199747b9f15bfad819460"}, + {file = "aiohttp-3.11.12-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:34245498eeb9ae54c687a07ad7f160053911b5745e186afe2d0c0f2898a1ab8a"}, + {file = "aiohttp-3.11.12-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8dc0fba9a74b471c45ca1a3cb6e6913ebfae416678d90529d188886278e7f3f6"}, + {file = "aiohttp-3.11.12-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a478aa11b328983c4444dacb947d4513cb371cd323f3845e53caeda6be5589d5"}, + {file = "aiohttp-3.11.12-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c160a04283c8c6f55b5bf6d4cad59bb9c5b9c9cd08903841b25f1f7109ef1259"}, + {file = "aiohttp-3.11.12-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:edb69b9589324bdc40961cdf0657815df674f1743a8d5ad9ab56a99e4833cfdd"}, + {file = "aiohttp-3.11.12-cp39-cp39-musllinux_1_2_armv7l.whl", hash = "sha256:4ee84c2a22a809c4f868153b178fe59e71423e1f3d6a8cd416134bb231fbf6d3"}, + {file = "aiohttp-3.11.12-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:bf4480a5438f80e0f1539e15a7eb8b5f97a26fe087e9828e2c0ec2be119a9f72"}, + {file = "aiohttp-3.11.12-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:e6b2732ef3bafc759f653a98881b5b9cdef0716d98f013d376ee8dfd7285abf1"}, + {file = "aiohttp-3.11.12-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:f752e80606b132140883bb262a457c475d219d7163d996dc9072434ffb0784c4"}, + {file = "aiohttp-3.11.12-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:ab3247d58b393bda5b1c8f31c9edece7162fc13265334217785518dd770792b8"}, + {file = "aiohttp-3.11.12-cp39-cp39-win32.whl", hash = "sha256:0d5176f310a7fe6f65608213cc74f4228e4f4ce9fd10bcb2bb6da8fc66991462"}, + {file = "aiohttp-3.11.12-cp39-cp39-win_amd64.whl", hash = "sha256:74bd573dde27e58c760d9ca8615c41a57e719bff315c9adb6f2a4281a28e8798"}, + {file = "aiohttp-3.11.12.tar.gz", hash = "sha256:7603ca26d75b1b86160ce1bbe2787a0b706e592af5b2504e12caa88a217767b0"}, ] [package.dependencies] @@ -809,13 +814,13 @@ zstd = ["zstandard (==0.22.0)"] [[package]] name = "certifi" -version = "2024.12.14" +version = "2025.1.31" description = "Python package for providing Mozilla's CA Bundle." optional = false python-versions = ">=3.6" files = [ - {file = "certifi-2024.12.14-py3-none-any.whl", hash = "sha256:1275f7a45be9464efc1173084eaa30f866fe2e47d389406136d332ed4967ec56"}, - {file = "certifi-2024.12.14.tar.gz", hash = "sha256:b650d30f370c2b724812bee08008be0c4163b163ddaec3f2546c1caf65f191db"}, + {file = "certifi-2025.1.31-py3-none-any.whl", hash = "sha256:ca78db4565a652026a4db2bcdf68f2fb589ea80d0be70e03929ed730746b84fe"}, + {file = "certifi-2025.1.31.tar.gz", hash = "sha256:3d5da6925056f6f18f119200434a4780a94263f10d1c21d032a6f6b2baa20651"}, ] [[package]] @@ -1368,6 +1373,24 @@ files = [ graph = ["objgraph (>=1.7.2)"] profile = ["gprof2dot (>=2022.7.29)"] +[[package]] +name = "dj-rest-auth" +version = "7.0.1" +description = "Authentication and Registration in Django Rest Framework" +optional = false +python-versions = ">=3.8" +files = [ + {file = "dj-rest-auth-7.0.1.tar.gz", hash = "sha256:3f8c744cbcf05355ff4bcbef0c8a63645da38e29a0fdef3c3332d4aced52fb90"}, +] + +[package.dependencies] +Django = ">=4.2,<6.0" +django-allauth = {version = ">=64.0.0", extras = ["socialaccount"], optional = true, markers = "extra == \"with-social\""} +djangorestframework = ">=3.13.0" + +[package.extras] +with-social = ["django-allauth[socialaccount] (>=64.0.0)"] + [[package]] name = "django" version = "5.1.5" @@ -1388,6 +1411,30 @@ tzdata = {version = "*", markers = "sys_platform == \"win32\""} argon2 = ["argon2-cffi (>=19.1.0)"] bcrypt = ["bcrypt"] +[[package]] +name = "django-allauth" +version = "65.4.1" +description = "Integrated set of Django applications addressing authentication, registration, account management as well as 3rd party (social) account authentication." +optional = false +python-versions = ">=3.8" +files = [ + {file = "django_allauth-65.4.1.tar.gz", hash = "sha256:60b32aef7dbbcc213319aa4fd8f570e985266ea1162ae6ef7a26a24efca85c8c"}, +] + +[package.dependencies] +asgiref = ">=3.8.1" +Django = ">=4.2.16" +pyjwt = {version = ">=1.7", extras = ["crypto"], optional = true, markers = "extra == \"socialaccount\""} +requests = {version = ">=2.0.0", optional = true, markers = "extra == \"socialaccount\""} +requests-oauthlib = {version = ">=0.3.0", optional = true, markers = "extra == \"socialaccount\""} + +[package.extras] +mfa = ["fido2 (>=1.1.2)", "qrcode (>=7.0.0)"] +openid = ["python3-openid (>=3.0.8)"] +saml = ["python3-saml (>=1.15.0,<2.0.0)"] +socialaccount = ["pyjwt[crypto] (>=1.7)", "requests (>=2.0.0)", "requests-oauthlib (>=0.3.0)"] +steam = ["python3-openid (>=3.0.8)"] + [[package]] name = "django-celery-beat" version = "2.7.0" @@ -2073,18 +2120,18 @@ files = [ [[package]] name = "h2" -version = "4.1.0" -description = "HTTP/2 State-Machine based protocol implementation" +version = "4.2.0" +description = "Pure-Python HTTP/2 protocol implementation" optional = false -python-versions = ">=3.6.1" +python-versions = ">=3.9" files = [ - {file = "h2-4.1.0-py3-none-any.whl", hash = "sha256:03a46bcf682256c95b5fd9e9a99c1323584c3eec6440d379b9903d709476bc6d"}, - {file = "h2-4.1.0.tar.gz", hash = "sha256:a83aca08fbe7aacb79fec788c9c0bac936343560ed9ec18b82a13a12c28d2abb"}, + {file = "h2-4.2.0-py3-none-any.whl", hash = "sha256:479a53ad425bb29af087f3458a61d30780bc818e4ebcf01f0b536ba916462ed0"}, + {file = "h2-4.2.0.tar.gz", hash = "sha256:c8a52129695e88b1a0578d8d2cc6842bbd79128ac685463b887ee278126ad01f"}, ] [package.dependencies] -hpack = ">=4.0,<5" -hyperframe = ">=6.0,<7" +hpack = ">=4.1,<5" +hyperframe = ">=6.1,<7" [[package]] name = "hpack" @@ -2482,13 +2529,13 @@ files = [ [[package]] name = "marshmallow" -version = "3.26.0" +version = "3.26.1" description = "A lightweight library for converting complex datatypes to and from native Python datatypes." optional = false python-versions = ">=3.9" files = [ - {file = "marshmallow-3.26.0-py3-none-any.whl", hash = "sha256:1287bca04e6a5f4094822ac153c03da5e214a0a60bcd557b140f3e66991b8ca1"}, - {file = "marshmallow-3.26.0.tar.gz", hash = "sha256:eb36762a1cc76d7abf831e18a3a1b26d3d481bbc74581b8e532a3d3a8115e1cb"}, + {file = "marshmallow-3.26.1-py3-none-any.whl", hash = "sha256:3350409f20a70a7e4e11a27661187b77cdcaeb20abca41c1454fe33636bea09c"}, + {file = "marshmallow-3.26.1.tar.gz", hash = "sha256:e6d8affb6cb61d39d26402096dc0aee12d5a26d490a121f118d2e81dc0719dc6"}, ] [package.dependencies] @@ -2664,20 +2711,20 @@ portalocker = ">=1.4,<3" [[package]] name = "msgraph-core" -version = "1.2.1" +version = "1.3.1" description = "Core component of the Microsoft Graph Python SDK" optional = false python-versions = ">=3.9" files = [ - {file = "msgraph_core-1.2.1-py3-none-any.whl", hash = "sha256:4591c1dc4359a323a50b2d29e4cb75ecaff32b4b6781f1ae743a655fb4a20ad7"}, - {file = "msgraph_core-1.2.1.tar.gz", hash = "sha256:87a3cb4d36dad590a3f02aaedf422547cbac10460bd9f0b6c984fab9556150d3"}, + {file = "msgraph_core-1.3.1-py3-none-any.whl", hash = "sha256:a6e3c19c36d43d00edfb49225647a357f8189351c226c3b7c020c5dfaa24171b"}, + {file = "msgraph_core-1.3.1.tar.gz", hash = "sha256:91c721b4c741d0e9a536e3c43e1cbd2254928b4d207e4f994e9cc7cb7a25bd74"}, ] [package.dependencies] httpx = {version = ">=0.23.0", extras = ["http2"]} -microsoft-kiota-abstractions = ">=1.0.0,<2.0.0" -microsoft-kiota-authentication-azure = ">=1.0.0,<2.0.0" -microsoft-kiota-http = ">=1.0.0,<2.0.0" +microsoft-kiota-abstractions = ">=1.8.0,<2.0.0" +microsoft-kiota-authentication-azure = ">=1.8.0,<2.0.0" +microsoft-kiota-http = ">=1.8.0,<2.0.0" [package.extras] dev = ["bumpver", "isort", "mypy", "pylint", "pytest", "yapf"] @@ -2888,13 +2935,13 @@ files = [ [[package]] name = "narwhals" -version = "1.24.1" +version = "1.25.2" description = "Extremely lightweight compatibility layer between dataframe libraries" optional = false python-versions = ">=3.8" files = [ - {file = "narwhals-1.24.1-py3-none-any.whl", hash = "sha256:d8983fe14851c95d60576ddca37c094bd4ed24ab9ea98396844fb20ad9aaf184"}, - {file = "narwhals-1.24.1.tar.gz", hash = "sha256:b09b8253d945f23cdb683a84685abf3afb9f96114d89e9f35dc876e143f65007"}, + {file = "narwhals-1.25.2-py3-none-any.whl", hash = "sha256:e645f7fc1f8c0a3563a6cdcd0191586cdf88470ad90f0818abba7ceb6c181b00"}, + {file = "narwhals-1.25.2.tar.gz", hash = "sha256:37594746fc06fe4a588967a34a2974b1f3a7ad6ff1571b6e31ac5e58c9591000"}, ] [package.extras] @@ -2995,13 +3042,13 @@ signedtoken = ["cryptography (>=3.0.0)", "pyjwt (>=2.0.0,<3)"] [[package]] name = "opentelemetry-api" -version = "1.29.0" +version = "1.30.0" description = "OpenTelemetry Python API" optional = false python-versions = ">=3.8" files = [ - {file = "opentelemetry_api-1.29.0-py3-none-any.whl", hash = "sha256:5fcd94c4141cc49c736271f3e1efb777bebe9cc535759c54c936cca4f1b312b8"}, - {file = "opentelemetry_api-1.29.0.tar.gz", hash = "sha256:d04a6cf78aad09614f52964ecb38021e248f5714dc32c2e0d8fd99517b4d69cf"}, + {file = "opentelemetry_api-1.30.0-py3-none-any.whl", hash = "sha256:d5f5284890d73fdf47f843dda3210edf37a38d66f44f2b5aedc1e89ed455dc09"}, + {file = "opentelemetry_api-1.30.0.tar.gz", hash = "sha256:375893400c1435bf623f7dfb3bcd44825fe6b56c34d0667c542ea8257b1a1240"}, ] [package.dependencies] @@ -3010,34 +3057,34 @@ importlib-metadata = ">=6.0,<=8.5.0" [[package]] name = "opentelemetry-sdk" -version = "1.29.0" +version = "1.30.0" description = "OpenTelemetry Python SDK" optional = false python-versions = ">=3.8" files = [ - {file = "opentelemetry_sdk-1.29.0-py3-none-any.whl", hash = "sha256:173be3b5d3f8f7d671f20ea37056710217959e774e2749d984355d1f9391a30a"}, - {file = "opentelemetry_sdk-1.29.0.tar.gz", hash = "sha256:b0787ce6aade6ab84315302e72bd7a7f2f014b0fb1b7c3295b88afe014ed0643"}, + {file = "opentelemetry_sdk-1.30.0-py3-none-any.whl", hash = "sha256:14fe7afc090caad881addb6926cec967129bd9260c4d33ae6a217359f6b61091"}, + {file = "opentelemetry_sdk-1.30.0.tar.gz", hash = "sha256:c9287a9e4a7614b9946e933a67168450b9ab35f08797eb9bc77d998fa480fa18"}, ] [package.dependencies] -opentelemetry-api = "1.29.0" -opentelemetry-semantic-conventions = "0.50b0" +opentelemetry-api = "1.30.0" +opentelemetry-semantic-conventions = "0.51b0" typing-extensions = ">=3.7.4" [[package]] name = "opentelemetry-semantic-conventions" -version = "0.50b0" +version = "0.51b0" description = "OpenTelemetry Semantic Conventions" optional = false python-versions = ">=3.8" files = [ - {file = "opentelemetry_semantic_conventions-0.50b0-py3-none-any.whl", hash = "sha256:e87efba8fdb67fb38113efea6a349531e75ed7ffc01562f65b802fcecb5e115e"}, - {file = "opentelemetry_semantic_conventions-0.50b0.tar.gz", hash = "sha256:02dc6dbcb62f082de9b877ff19a3f1ffaa3c306300fa53bfac761c4567c83d38"}, + {file = "opentelemetry_semantic_conventions-0.51b0-py3-none-any.whl", hash = "sha256:fdc777359418e8d06c86012c3dc92c88a6453ba662e941593adb062e48c2eeae"}, + {file = "opentelemetry_semantic_conventions-0.51b0.tar.gz", hash = "sha256:3fabf47f35d1fd9aebcdca7e6802d86bd5ebc3bc3408b7e3248dde6e87a18c47"}, ] [package.dependencies] deprecated = ">=1.2.6" -opentelemetry-api = "1.29.0" +opentelemetry-api = "1.30.0" [[package]] name = "packaging" @@ -3137,15 +3184,18 @@ xml = ["lxml (>=4.9.2)"] [[package]] name = "pbr" -version = "6.1.0" +version = "6.1.1" description = "Python Build Reasonableness" optional = false python-versions = ">=2.6" files = [ - {file = "pbr-6.1.0-py2.py3-none-any.whl", hash = "sha256:a776ae228892d8013649c0aeccbb3d5f99ee15e005a4cbb7e61d55a067b28a2a"}, - {file = "pbr-6.1.0.tar.gz", hash = "sha256:788183e382e3d1d7707db08978239965e8b9e4e5ed42669bf4758186734d5f24"}, + {file = "pbr-6.1.1-py2.py3-none-any.whl", hash = "sha256:38d4daea5d9fa63b3f626131b9d34947fd0c8be9b05a29276870580050a25a76"}, + {file = "pbr-6.1.1.tar.gz", hash = "sha256:93ea72ce6989eb2eed99d0f75721474f69ad88128afdef5ac377eb797c4bf76b"}, ] +[package.dependencies] +setuptools = "*" + [[package]] name = "platformdirs" version = "4.3.6" @@ -3416,8 +3466,8 @@ tzlocal = "5.2" [package.source] type = "git" url = "https://github.com/prowler-cloud/prowler.git" -reference = "v5.3" -resolved_reference = "dfd377f89e034e6be47acc95c3e6666278f8054e" +reference = "master" +resolved_reference = "7469377079bb88487a741625dcd431aa5375e793" [[package]] name = "psutil" @@ -3481,6 +3531,7 @@ files = [ {file = "psycopg2_binary-2.9.9-cp311-cp311-win32.whl", hash = "sha256:dc4926288b2a3e9fd7b50dc6a1909a13bbdadfc67d93f3374d984e56f885579d"}, {file = "psycopg2_binary-2.9.9-cp311-cp311-win_amd64.whl", hash = "sha256:b76bedd166805480ab069612119ea636f5ab8f8771e640ae103e05a4aae3e417"}, {file = "psycopg2_binary-2.9.9-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:8532fd6e6e2dc57bcb3bc90b079c60de896d2128c5d9d6f24a63875a95a088cf"}, + {file = "psycopg2_binary-2.9.9-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:b0605eaed3eb239e87df0d5e3c6489daae3f7388d455d0c0b4df899519c6a38d"}, {file = "psycopg2_binary-2.9.9-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8f8544b092a29a6ddd72f3556a9fcf249ec412e10ad28be6a0c0d948924f2212"}, {file = "psycopg2_binary-2.9.9-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2d423c8d8a3c82d08fe8af900ad5b613ce3632a1249fd6a223941d0735fce493"}, {file = "psycopg2_binary-2.9.9-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2e5afae772c00980525f6d6ecf7cbca55676296b580c0e6abb407f15f3706996"}, @@ -3489,6 +3540,8 @@ files = [ {file = "psycopg2_binary-2.9.9-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:cb16c65dcb648d0a43a2521f2f0a2300f40639f6f8c1ecbc662141e4e3e1ee07"}, {file = "psycopg2_binary-2.9.9-cp312-cp312-musllinux_1_1_ppc64le.whl", hash = "sha256:911dda9c487075abd54e644ccdf5e5c16773470a6a5d3826fda76699410066fb"}, {file = "psycopg2_binary-2.9.9-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:57fede879f08d23c85140a360c6a77709113efd1c993923c59fde17aa27599fe"}, + {file = "psycopg2_binary-2.9.9-cp312-cp312-win32.whl", hash = "sha256:64cf30263844fa208851ebb13b0732ce674d8ec6a0c86a4e160495d299ba3c93"}, + {file = "psycopg2_binary-2.9.9-cp312-cp312-win_amd64.whl", hash = "sha256:81ff62668af011f9a48787564ab7eded4e9fb17a4a6a74af5ffa6a457400d2ab"}, {file = "psycopg2_binary-2.9.9-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:2293b001e319ab0d869d660a704942c9e2cce19745262a8aba2115ef41a0a42a"}, {file = "psycopg2_binary-2.9.9-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:03ef7df18daf2c4c07e2695e8cfd5ee7f748a1d54d802330985a78d2a5a6dca9"}, {file = "psycopg2_binary-2.9.9-cp37-cp37m-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0a602ea5aff39bb9fac6308e9c9d82b9a35c2bf288e184a816002c9fae930b77"}, @@ -4583,13 +4636,13 @@ doc = ["sphinx"] [[package]] name = "std-uritemplate" -version = "2.0.1" +version = "2.0.3" description = "std-uritemplate implementation for Python" optional = false python-versions = "<4.0,>=3.8" files = [ - {file = "std_uritemplate-2.0.1-py3-none-any.whl", hash = "sha256:6405d13f9ff3b8f70e1fa4eaefa373049659e1a61870c4b651441821a3fc984d"}, - {file = "std_uritemplate-2.0.1.tar.gz", hash = "sha256:f4684ae050167e237ed5819423139893e0b5b7f08dc6b7467bba3fdd64608be8"}, + {file = "std_uritemplate-2.0.3-py3-none-any.whl", hash = "sha256:434df26453bf68c6077879fed6609b2c39e2fc73080e74cd157269d5f8abdb3e"}, + {file = "std_uritemplate-2.0.3.tar.gz", hash = "sha256:ad4cb1d671bcf4a3608b3598c687be4b0929867c53a2d69c105989da6a5a2d4c"}, ] [[package]] @@ -5045,4 +5098,4 @@ type = ["pytest-mypy"] [metadata] lock-version = "2.0" python-versions = ">=3.11,<3.13" -content-hash = "f6be3280891827b3162055248a10561638ee42f406f048d7a886cd3532f714a5" +content-hash = "d762c984b70da32828fc0b2c8b9d4849a9342b8a9ac7060c47d6e82071b740f1" diff --git a/api/pyproject.toml b/api/pyproject.toml index 33dca0378c..dab211d862 100644 --- a/api/pyproject.toml +++ b/api/pyproject.toml @@ -8,10 +8,11 @@ description = "Prowler's API (Django/DRF)" license = "Apache-2.0" name = "prowler-api" package-mode = false -version = "1.4.0" +version = "1.5.0" [tool.poetry.dependencies] celery = {extras = ["pytest"], version = "^5.4.0"} +dj-rest-auth = {extras = ["with_social", "jwt"], version = "7.0.1"} django = "5.1.5" django-celery-beat = "^2.7.0" django-celery-results = "^2.5.1" diff --git a/api/src/backend/api/adapters.py b/api/src/backend/api/adapters.py new file mode 100644 index 0000000000..9a8cb2f044 --- /dev/null +++ b/api/src/backend/api/adapters.py @@ -0,0 +1,57 @@ +from allauth.socialaccount.adapter import DefaultSocialAccountAdapter +from django.db import transaction + +from api.db_router import MainRouter +from api.db_utils import rls_transaction +from api.models import Membership, Role, Tenant, User, UserRoleRelationship + + +class ProwlerSocialAccountAdapter(DefaultSocialAccountAdapter): + @staticmethod + def get_user_by_email(email: str): + try: + return User.objects.get(email=email) + except User.DoesNotExist: + return None + + def pre_social_login(self, request, sociallogin): + # Link existing accounts with the same email address + email = sociallogin.account.extra_data.get("email") + if email: + existing_user = self.get_user_by_email(email) + if existing_user: + sociallogin.connect(request, existing_user) + + def save_user(self, request, sociallogin, form=None): + """ + Called after the user data is fully populated from the provider + and is about to be saved to the DB for the first time. + """ + with transaction.atomic(using=MainRouter.admin_db): + user = super().save_user(request, sociallogin, form) + user.save(using=MainRouter.admin_db) + + tenant = Tenant.objects.using(MainRouter.admin_db).create( + name=f"{user.email.split('@')[0]} default tenant" + ) + with rls_transaction(str(tenant.id)): + Membership.objects.using(MainRouter.admin_db).create( + user=user, tenant=tenant, role=Membership.RoleChoices.OWNER + ) + role = Role.objects.using(MainRouter.admin_db).create( + name="admin", + tenant_id=tenant.id, + manage_users=True, + manage_account=True, + manage_billing=True, + manage_providers=True, + manage_integrations=True, + manage_scans=True, + unlimited_visibility=True, + ) + UserRoleRelationship.objects.using(MainRouter.admin_db).create( + user=user, + role=role, + tenant_id=tenant.id, + ) + return user diff --git a/api/src/backend/api/db_router.py b/api/src/backend/api/db_router.py index 939672f88e..dc34c1191a 100644 --- a/api/src/backend/api/db_router.py +++ b/api/src/backend/api/db_router.py @@ -1,22 +1,29 @@ +ALLOWED_APPS = ("django", "socialaccount", "account", "authtoken", "silk") + + class MainRouter: default_db = "default" admin_db = "admin" def db_for_read(self, model, **hints): # noqa: F841 model_table_name = model._meta.db_table - if model_table_name.startswith("django_") or model_table_name.startswith( - "silk_" + if model_table_name.startswith("django_") or any( + model_table_name.startswith(f"{app}_") for app in ALLOWED_APPS ): return self.admin_db return None def db_for_write(self, model, **hints): # noqa: F841 model_table_name = model._meta.db_table - if model_table_name.startswith("django_") or model_table_name.startswith( - "silk_" - ): + if any(model_table_name.startswith(f"{app}_") for app in ALLOWED_APPS): return self.admin_db return None def allow_migrate(self, db, app_label, model_name=None, **hints): # noqa: F841 return db == self.admin_db + + def allow_relation(self, obj1, obj2, **hints): # noqa: F841 + # Allow relations if both objects are in either "default" or "admin" db connectors + if {obj1._state.db, obj2._state.db} <= {self.default_db, self.admin_db}: + return True + return None diff --git a/api/src/backend/api/specs/v1.yaml b/api/src/backend/api/specs/v1.yaml index ca5f1a5b07..b7118f7508 100644 --- a/api/src/backend/api/specs/v1.yaml +++ b/api/src/backend/api/specs/v1.yaml @@ -1,7 +1,7 @@ openapi: 3.0.3 info: title: Prowler API - version: 1.4.0 + version: 1.5.0 description: |- Prowler API specification. @@ -4996,6 +4996,35 @@ paths: schema: $ref: '#/components/schemas/TokenRefreshResponse' description: '' + /api/v1/tokens/switch: + post: + operationId: tokens_switch_create + description: Switch tenant by providing a valid tenant ID. The authenticated + user must belong to the tenant. + summary: Switch tenant using a valid tenant ID + tags: + - Token + requestBody: + content: + application/vnd.api+json: + schema: + $ref: '#/components/schemas/TokenSwitchTenantRequest' + application/x-www-form-urlencoded: + schema: + $ref: '#/components/schemas/TokenSwitchTenantRequest' + multipart/form-data: + schema: + $ref: '#/components/schemas/TokenSwitchTenantRequest' + required: true + security: + - jwtAuth: [] + responses: + '200': + content: + application/vnd.api+json: + schema: + $ref: '#/components/schemas/TokenSwitchTenantResponse' + description: '' /api/v1/users: get: operationId: users_list @@ -10047,6 +10076,81 @@ components: $ref: '#/components/schemas/Token' required: - data + TokenSwitchTenant: + type: object + required: + - type + additionalProperties: false + properties: + type: + allOf: + - $ref: '#/components/schemas/TokenSwitchTenantTypeEnum' + description: The [type](https://jsonapi.org/format/#document-resource-object-identification) + member is used to describe resource objects that share common attributes + and relationships. + attributes: + type: object + properties: + tenant_id: + type: string + format: uuid + writeOnly: true + description: The tenant ID for which to request a new token. + access: + type: string + readOnly: true + refresh: + type: string + readOnly: true + required: + - tenant_id + TokenSwitchTenantRequest: + type: object + properties: + data: + type: object + required: + - type + additionalProperties: false + properties: + type: + type: string + description: The [type](https://jsonapi.org/format/#document-resource-object-identification) + member is used to describe resource objects that share common attributes + and relationships. + enum: + - tokens-switch-tenant + attributes: + type: object + properties: + tenant_id: + type: string + format: uuid + writeOnly: true + description: The tenant ID for which to request a new token. + access: + type: string + readOnly: true + minLength: 1 + refresh: + type: string + readOnly: true + minLength: 1 + required: + - tenant_id + required: + - data + TokenSwitchTenantResponse: + type: object + properties: + data: + $ref: '#/components/schemas/TokenSwitchTenant' + required: + - data + TokenSwitchTenantTypeEnum: + type: string + enum: + - tokens-switch-tenant TokenTypeEnum: type: string enum: diff --git a/api/src/backend/api/tests/integration/test_authentication.py b/api/src/backend/api/tests/integration/test_authentication.py index 85dbbad315..f41e8207c4 100644 --- a/api/src/backend/api/tests/integration/test_authentication.py +++ b/api/src/backend/api/tests/integration/test_authentication.py @@ -3,6 +3,8 @@ from conftest import TEST_PASSWORD, get_api_tokens, get_authorization_header from django.urls import reverse from rest_framework.test import APIClient +from api.models import Membership, User + @pytest.mark.django_db def test_basic_authentication(): @@ -177,3 +179,122 @@ def test_user_me_when_inviting_users(create_test_user, tenants_fixture, roles_fi user2_me = client.get(reverse("user-me"), headers=user2_headers) assert user2_me.status_code == 200 assert user2_me.json()["data"]["attributes"]["email"] == user2_email + + +@pytest.mark.django_db +class TestTokenSwitchTenant: + def test_switch_tenant_with_valid_token(self, tenants_fixture, providers_fixture): + client = APIClient() + + test_user = "test_email@prowler.com" + test_password = "test_password" + + # Check that we can create a new user without any kind of authentication + user_creation_response = client.post( + reverse("user-list"), + data={ + "data": { + "type": "users", + "attributes": { + "name": "test", + "email": test_user, + "password": test_password, + }, + } + }, + format="vnd.api+json", + ) + assert user_creation_response.status_code == 201 + + # Create a new relationship between this user and another tenant + tenant_id = tenants_fixture[0].id + user_instance = User.objects.get(email=test_user) + Membership.objects.create(user=user_instance, tenant_id=tenant_id) + + # Check that using our new user's credentials we can authenticate and get the providers + access_token, _ = get_api_tokens(client, test_user, test_password) + auth_headers = get_authorization_header(access_token) + + user_me_response = client.get( + reverse("user-me"), + headers=auth_headers, + ) + assert user_me_response.status_code == 200 + # Assert this user belongs to two tenants + assert ( + user_me_response.json()["data"]["relationships"]["memberships"]["meta"][ + "count" + ] + == 2 + ) + + provider_response = client.get( + reverse("provider-list"), + headers=auth_headers, + ) + assert provider_response.status_code == 200 + # Empty response since there are no providers in this tenant + assert not provider_response.json()["data"] + + switch_tenant_response = client.post( + reverse("token-switch"), + data={ + "data": { + "type": "tokens-switch-tenant", + "attributes": {"tenant_id": tenant_id}, + } + }, + headers=auth_headers, + ) + assert switch_tenant_response.status_code == 200 + new_access_token = switch_tenant_response.json()["data"]["attributes"]["access"] + new_auth_headers = get_authorization_header(new_access_token) + + provider_response = client.get( + reverse("provider-list"), + headers=new_auth_headers, + ) + assert provider_response.status_code == 200 + # Now it must be data because we switched to another tenant with providers + assert provider_response.json()["data"] + + def test_switch_tenant_with_invalid_token(self, create_test_user, tenants_fixture): + client = APIClient() + + access_token, refresh_token = get_api_tokens( + client, create_test_user.email, TEST_PASSWORD + ) + auth_headers = get_authorization_header(access_token) + + invalid_token_response = client.post( + reverse("token-switch"), + data={ + "data": { + "type": "tokens-switch-tenant", + "attributes": {"tenant_id": "invalid_tenant_id"}, + } + }, + headers=auth_headers, + ) + assert invalid_token_response.status_code == 400 + assert invalid_token_response.json()["errors"][0]["code"] == "invalid" + assert ( + invalid_token_response.json()["errors"][0]["detail"] + == "Must be a valid UUID." + ) + + invalid_tenant_response = client.post( + reverse("token-switch"), + data={ + "data": { + "type": "tokens-switch-tenant", + "attributes": {"tenant_id": tenants_fixture[-1].id}, + } + }, + headers=auth_headers, + ) + assert invalid_tenant_response.status_code == 400 + assert invalid_tenant_response.json()["errors"][0]["code"] == "invalid" + assert invalid_tenant_response.json()["errors"][0]["detail"] == ( + "Tenant does not exist or user is not a " "member." + ) diff --git a/api/src/backend/api/utils.py b/api/src/backend/api/utils.py index 1f60669039..9dd86daeaf 100644 --- a/api/src/backend/api/utils.py +++ b/api/src/backend/api/utils.py @@ -1,15 +1,25 @@ from datetime import datetime, timezone +from allauth.socialaccount.providers.oauth2.client import OAuth2Client +from rest_framework.exceptions import NotFound, ValidationError + +from api.db_router import MainRouter +from api.exceptions import InvitationTokenExpiredException +from api.models import Invitation, Provider from prowler.providers.aws.aws_provider import AwsProvider from prowler.providers.azure.azure_provider import AzureProvider from prowler.providers.common.models import Connection from prowler.providers.gcp.gcp_provider import GcpProvider from prowler.providers.kubernetes.kubernetes_provider import KubernetesProvider -from rest_framework.exceptions import ValidationError, NotFound -from api.db_router import MainRouter -from api.exceptions import InvitationTokenExpiredException -from api.models import Provider, Invitation + +class CustomOAuth2Client(OAuth2Client): + def __init__(self, client_id, secret, *args, **kwargs): + # Remove any duplicate "scope_delimiter" from kwargs + # Bug present in dj-rest-auth after version v7.0.1 + # https://github.com/iMerica/dj-rest-auth/issues/673 + kwargs.pop("scope_delimiter", None) + super().__init__(client_id, secret, *args, **kwargs) def merge_dicts(default_dict: dict, replacement_dict: dict) -> dict: diff --git a/api/src/backend/api/v1/serializers.py b/api/src/backend/api/v1/serializers.py index 4f17d2d61c..1317904f88 100644 --- a/api/src/backend/api/v1/serializers.py +++ b/api/src/backend/api/v1/serializers.py @@ -38,7 +38,65 @@ from api.rls import Tenant # Tokens -class TokenSerializer(TokenObtainPairSerializer): +def generate_tokens(user: User, tenant_id: str) -> dict: + try: + refresh = RefreshToken.for_user(user) + except InvalidKeyError: + # Handle invalid key error + raise ValidationError( + { + "detail": "Token generation failed due to invalid key configuration. Provide valid " + "DJANGO_TOKEN_SIGNING_KEY and DJANGO_TOKEN_VERIFYING_KEY in the environment." + } + ) + except Exception as e: + raise ValidationError({"detail": str(e)}) + + # Post-process the tokens + # Set the tenant_id + refresh["tenant_id"] = tenant_id + + # Set the nbf (not before) claim to the iat (issued at) claim. At this moment, simplejwt does not provide a + # way to set the nbf claim + refresh.payload["nbf"] = refresh["iat"] + + # Get the access token + access = refresh.access_token + + if settings.SIMPLE_JWT["UPDATE_LAST_LOGIN"]: + update_last_login(None, user) + + return {"access": str(access), "refresh": str(refresh)} + + +class BaseTokenSerializer(TokenObtainPairSerializer): + def custom_validate(self, attrs, social: bool = False): + email = attrs.get("email") + password = attrs.get("password") + tenant_id = str(attrs.get("tenant_id", "")) + + # Authenticate user + user = ( + User.objects.get(email=email) + if social + else authenticate(username=email, password=password) + ) + if user is None: + raise ValidationError("Invalid credentials") + + if tenant_id: + if not user.is_member_of_tenant(tenant_id): + raise ValidationError("Tenant does not exist or user is not a member.") + else: + first_membership = user.memberships.order_by("date_joined").first() + if first_membership is None: + raise ValidationError("User has no memberships.") + tenant_id = str(first_membership.tenant_id) + + return generate_tokens(user, tenant_id) + + +class TokenSerializer(BaseTokenSerializer): email = serializers.EmailField(write_only=True) password = serializers.CharField(write_only=True) tenant_id = serializers.UUIDField( @@ -56,53 +114,25 @@ class TokenSerializer(TokenObtainPairSerializer): resource_name = "tokens" def validate(self, attrs): - email = attrs.get("email") - password = attrs.get("password") - tenant_id = str(attrs.get("tenant_id", "")) + return super().custom_validate(attrs) - # Authenticate user - user = authenticate(username=email, password=password) - if user is None: - raise ValidationError("Invalid credentials") - if tenant_id: - if not user.is_member_of_tenant(tenant_id): - raise ValidationError("Tenant does not exist or user is not a member.") - else: - first_membership = user.memberships.order_by("date_joined").first() - if first_membership is None: - raise ValidationError("User has no memberships.") - tenant_id = str(first_membership.tenant_id) +class TokenSocialLoginSerializer(BaseTokenSerializer): + email = serializers.EmailField(write_only=True) - # Generate tokens - try: - refresh = RefreshToken.for_user(user) - except InvalidKeyError: - # Handle invalid key error - raise ValidationError( - { - "detail": "Token generation failed due to invalid key configuration. Provide valid " - "DJANGO_TOKEN_SIGNING_KEY and DJANGO_TOKEN_VERIFYING_KEY in the environment." - } - ) - except Exception as e: - raise ValidationError({"detail": str(e)}) + # Output tokens + refresh = serializers.CharField(read_only=True) + access = serializers.CharField(read_only=True) - # Post-process the tokens - # Set the tenant_id - refresh["tenant_id"] = tenant_id + class JSONAPIMeta: + resource_name = "tokens" - # Set the nbf (not before) claim to the iat (issued at) claim. At this moment, simplejwt does not provide a - # way to set the nbf claim - refresh.payload["nbf"] = refresh["iat"] + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.fields.pop("password", None) - # Get the access token - access = refresh.access_token - - if settings.SIMPLE_JWT["UPDATE_LAST_LOGIN"]: - update_last_login(None, user) - - return {"access": str(access), "refresh": str(refresh)} + def validate(self, attrs): + return super().custom_validate(attrs, social=True) # TODO: Check if we can change the parent class to TokenRefreshSerializer from rest_framework_simplejwt.serializers @@ -140,6 +170,30 @@ class TokenRefreshSerializer(serializers.Serializer): raise ValidationError({"refresh": "Invalid or expired token"}) +class TokenSwitchTenantSerializer(serializers.Serializer): + tenant_id = serializers.UUIDField( + write_only=True, help_text="The tenant ID for which to request a new token." + ) + access = serializers.CharField(read_only=True) + refresh = serializers.CharField(read_only=True) + + class JSONAPIMeta: + resource_name = "tokens-switch-tenant" + + def validate(self, attrs): + request = self.context["request"] + user = request.user + + if not user.is_authenticated: + raise ValidationError("Invalid or expired token.") + + tenant_id = str(attrs.get("tenant_id")) + if not user.is_member_of_tenant(tenant_id): + raise ValidationError("Tenant does not exist or user is not a member.") + + return generate_tokens(user, tenant_id) + + # Base diff --git a/api/src/backend/api/v1/urls.py b/api/src/backend/api/v1/urls.py index 4ff587d913..f26e0cb8f3 100644 --- a/api/src/backend/api/v1/urls.py +++ b/api/src/backend/api/v1/urls.py @@ -6,7 +6,10 @@ from api.v1.views import ( ComplianceOverviewViewSet, CustomTokenObtainView, CustomTokenRefreshView, + CustomTokenSwitchTenantView, FindingViewSet, + GithubSocialLoginView, + GoogleSocialLoginView, InvitationAcceptViewSet, InvitationViewSet, MembershipViewSet, @@ -56,6 +59,7 @@ users_router.register(r"memberships", MembershipViewSet, basename="user-membersh urlpatterns = [ path("tokens", CustomTokenObtainView.as_view(), name="token-obtain"), path("tokens/refresh", CustomTokenRefreshView.as_view(), name="token-refresh"), + path("tokens/switch", CustomTokenSwitchTenantView.as_view(), name="token-switch"), path( "providers/secrets", ProviderSecretViewSet.as_view({"get": "list", "post": "create"}), @@ -106,6 +110,8 @@ urlpatterns = [ ), name="provider_group-providers-relationship", ), + path("tokens/google", GoogleSocialLoginView.as_view(), name="token-google"), + path("tokens/github", GithubSocialLoginView.as_view(), name="token-github"), path("", include(router.urls)), path("", include(tenants_router.urls)), path("", include(users_router.urls)), diff --git a/api/src/backend/api/v1/views.py b/api/src/backend/api/v1/views.py index 722ecbfd7d..88500698da 100644 --- a/api/src/backend/api/v1/views.py +++ b/api/src/backend/api/v1/views.py @@ -1,4 +1,11 @@ +from allauth.socialaccount.providers.github.views import GitHubOAuth2Adapter +from allauth.socialaccount.providers.google.views import GoogleOAuth2Adapter from celery.result import AsyncResult +from config.settings.social_login import ( + GITHUB_OAUTH_CALLBACK_URL, + GOOGLE_OAUTH_CALLBACK_URL, +) +from dj_rest_auth.registration.views import SocialLoginView from django.conf import settings as django_settings from django.contrib.postgres.aggregates import ArrayAgg from django.contrib.postgres.search import SearchQuery @@ -81,7 +88,7 @@ from api.models import ( from api.pagination import ComplianceOverviewPagination from api.rbac.permissions import Permissions, get_providers, get_role from api.rls import Tenant -from api.utils import validate_invitation +from api.utils import CustomOAuth2Client, validate_invitation from api.uuid_utils import datetime_to_uuid7 from api.v1.serializers import ( ComplianceOverviewFullSerializer, @@ -121,6 +128,8 @@ from api.v1.serializers import ( TenantSerializer, TokenRefreshSerializer, TokenSerializer, + TokenSocialLoginSerializer, + TokenSwitchTenantSerializer, UserCreateSerializer, UserRoleRelationshipSerializer, UserSerializer, @@ -187,13 +196,43 @@ class CustomTokenRefreshView(GenericAPIView): ) +@extend_schema( + tags=["Token"], + summary="Switch tenant using a valid tenant ID", + description="Switch tenant by providing a valid tenant ID. The authenticated user must belong to the tenant.", +) +class CustomTokenSwitchTenantView(GenericAPIView): + permission_classes = [permissions.IsAuthenticated] + resource_name = "tokens-switch-tenant" + serializer_class = TokenSwitchTenantSerializer + http_method_names = ["post"] + + def post(self, request): + serializer = TokenSwitchTenantSerializer( + data=request.data, context={"request": request} + ) + + try: + serializer.is_valid(raise_exception=True) + except TokenError as e: + raise InvalidToken(e.args[0]) + + return Response( + data={ + "type": "tokens-switch-tenant", + "attributes": serializer.validated_data, + }, + status=status.HTTP_200_OK, + ) + + @extend_schema(exclude=True) class SchemaView(SpectacularAPIView): serializer_class = None def get(self, request, *args, **kwargs): spectacular_settings.TITLE = "Prowler API" - spectacular_settings.VERSION = "1.4.0" + spectacular_settings.VERSION = "1.5.0" spectacular_settings.DESCRIPTION = ( "Prowler API specification.\n\nThis file is auto-generated." ) @@ -253,6 +292,58 @@ class SchemaView(SpectacularAPIView): return super().get(request, *args, **kwargs) +@extend_schema(exclude=True) +class GoogleSocialLoginView(SocialLoginView): + adapter_class = GoogleOAuth2Adapter + client_class = CustomOAuth2Client + callback_url = GOOGLE_OAUTH_CALLBACK_URL + + def get_response(self): + original_response = super().get_response() + + if self.user and self.user.is_authenticated: + serializer = TokenSocialLoginSerializer(data={"email": self.user.email}) + try: + serializer.is_valid(raise_exception=True) + except TokenError as e: + raise InvalidToken(e.args[0]) + return Response( + data={ + "type": "google-social-tokens", + "attributes": serializer.validated_data, + }, + status=status.HTTP_200_OK, + ) + return original_response + + +@extend_schema(exclude=True) +class GithubSocialLoginView(SocialLoginView): + adapter_class = GitHubOAuth2Adapter + client_class = CustomOAuth2Client + callback_url = GITHUB_OAUTH_CALLBACK_URL + + def get_response(self): + original_response = super().get_response() + + if self.user and self.user.is_authenticated: + serializer = TokenSocialLoginSerializer(data={"email": self.user.email}) + + try: + serializer.is_valid(raise_exception=True) + except TokenError as e: + raise InvalidToken(e.args[0]) + + return Response( + data={ + "type": "github-social-tokens", + "attributes": serializer.validated_data, + }, + status=status.HTTP_200_OK, + ) + return original_response + + @extend_schema_view( list=extend_schema( tags=["User"], diff --git a/api/src/backend/config/django/base.py b/api/src/backend/config/django/base.py index 6494bd0ff3..7820e9126c 100644 --- a/api/src/backend/config/django/base.py +++ b/api/src/backend/config/django/base.py @@ -4,6 +4,7 @@ from config.custom_logging import LOGGING # noqa from config.env import BASE_DIR, env # noqa from config.settings.celery import * # noqa from config.settings.partitions import * # noqa +from config.settings.social_login import * # noqa SECRET_KEY = env("SECRET_KEY", default="secret") DEBUG = env.bool("DJANGO_DEBUG", default=False) @@ -29,6 +30,13 @@ INSTALLED_APPS = [ "django_celery_results", "django_celery_beat", "rest_framework_simplejwt.token_blacklist", + "allauth", + "allauth.account", + "allauth.socialaccount", + "allauth.socialaccount.providers.google", + "allauth.socialaccount.providers.github", + "dj_rest_auth.registration", + "rest_framework.authtoken", ] MIDDLEWARE = [ @@ -42,8 +50,11 @@ MIDDLEWARE = [ "django.contrib.messages.middleware.MessageMiddleware", "django.middleware.clickjacking.XFrameOptionsMiddleware", "api.middleware.APILoggingMiddleware", + "allauth.account.middleware.AccountMiddleware", ] +SITE_ID = 1 + CORS_ALLOWED_ORIGINS = ["http://localhost", "http://127.0.0.1"] ROOT_URLCONF = "config.urls" diff --git a/api/src/backend/config/settings/social_login.py b/api/src/backend/config/settings/social_login.py new file mode 100644 index 0000000000..035a166279 --- /dev/null +++ b/api/src/backend/config/settings/social_login.py @@ -0,0 +1,53 @@ +from config.env import env + +# Google Oauth settings +GOOGLE_OAUTH_CLIENT_ID = env("DJANGO_GOOGLE_OAUTH_CLIENT_ID", default="") +GOOGLE_OAUTH_CLIENT_SECRET = env("DJANGO_GOOGLE_OAUTH_CLIENT_SECRET", default="") +GOOGLE_OAUTH_CALLBACK_URL = env("DJANGO_GOOGLE_OAUTH_CALLBACK_URL", default="") + +GITHUB_OAUTH_CLIENT_ID = env("DJANGO_GITHUB_OAUTH_CLIENT_ID", default="") +GITHUB_OAUTH_CLIENT_SECRET = env("DJANGO_GITHUB_OAUTH_CLIENT_SECRET", default="") +GITHUB_OAUTH_CALLBACK_URL = env("DJANGO_GITHUB_OAUTH_CALLBACK_URL", default="") + +# Allauth settings +ACCOUNT_LOGIN_METHODS = {"email"} # Use Email / Password authentication +ACCOUNT_USERNAME_REQUIRED = False +ACCOUNT_EMAIL_REQUIRED = True +ACCOUNT_EMAIL_VERIFICATION = "none" # Do not require email confirmation +ACCOUNT_USER_MODEL_USERNAME_FIELD = None +REST_AUTH = { + "TOKEN_MODEL": None, + "REST_USE_JWT": True, +} +# django-allauth (social) +# Authenticate if local account with this email address already exists +SOCIALACCOUNT_EMAIL_AUTHENTICATION = True +# Connect local account and social account if local account with that email address already exists +SOCIALACCOUNT_EMAIL_AUTHENTICATION_AUTO_CONNECT = True +SOCIALACCOUNT_ADAPTER = "api.adapters.ProwlerSocialAccountAdapter" +SOCIALACCOUNT_PROVIDERS = { + "google": { + "APP": { + "client_id": GOOGLE_OAUTH_CLIENT_ID, + "secret": GOOGLE_OAUTH_CLIENT_SECRET, + "key": "", + }, + "SCOPE": [ + "email", + "profile", + ], + "AUTH_PARAMS": { + "access_type": "online", + }, + }, + "github": { + "APP": { + "client_id": GITHUB_OAUTH_CLIENT_ID, + "secret": GITHUB_OAUTH_CLIENT_SECRET, + }, + "SCOPE": [ + "user", + "read:org", + ], + }, +}