From 005d6e4b85cbddfc42360f5018ec9ca556ffe6ff Mon Sep 17 00:00:00 2001 From: Joao-Pedro-P-Holanda Date: Thu, 13 Feb 2025 18:53:01 -0300 Subject: [PATCH 01/12] test: adding tests to security scopes verification adding tests to validate an user scope created with a simple string or a list of strings --- tests/test_security_oauth2_scopes.py | 54 ++++++++++++++++++++++++++++ 1 file changed, 54 insertions(+) create mode 100644 tests/test_security_oauth2_scopes.py diff --git a/tests/test_security_oauth2_scopes.py b/tests/test_security_oauth2_scopes.py new file mode 100644 index 000000000..690ccca8a --- /dev/null +++ b/tests/test_security_oauth2_scopes.py @@ -0,0 +1,54 @@ +from typing import Annotated +from fastapi import FastAPI, Security +from fastapi.params import Depends +from fastapi.security import OAuth2PasswordBearer +from fastapi.security.oauth2 import SecurityScopes +from fastapi.testclient import TestClient + +app = FastAPI() + +oauth2_scheme = OAuth2PasswordBearer( + tokenUrl="token", + scopes={"me": "Read information about the current user.", "items": "Read items."}, +) + + +def get_security_scopes( + security_scopes: SecurityScopes, token: Annotated[str, Depends(oauth2_scheme)] +): + return security_scopes.scopes + + +@app.get("/me") +async def read_single_scope( + current_scope: Annotated[list[str], Security(get_security_scopes, scopes="me")], +): + return {"scopes": current_scope} + + +@app.get("/me-and-items") +async def read_single_scope( + current_scope: Annotated[ + list[str], Security(get_security_scopes, scopes=["me", "items"]) + ], +): + return {"scopes": current_scope} + + +client = TestClient(app) + + +def test_single_scope_string(): + response = client.get("/me", headers={"Authorization": "Bearer sometoken"}) + + assert response.status_code == 200 + assert response.json() == {"scopes": ["me"]} + + +def test_list_scopes(): + response = client.get( + "/me-and-items", headers={"Authorization": "Bearer sometoken"} + ) + + assert response.status_code == 200 + assert response.json() == {"scopes": ["me", "items"]} From 62d92fa5ef26db38390239003bc7a45145bc77f3 Mon Sep 17 00:00:00 2001 From: Joao-Pedro-P-Holanda Date: Thu, 13 Feb 2025 18:54:19 -0300 Subject: [PATCH 02/12] fix: correcting Security scopes when string is given creating a list explicitly with scopes value when scopes receives a string --- fastapi/params.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/fastapi/params.py b/fastapi/params.py index 8f5601dd3..2740b5824 100644 --- a/fastapi/params.py +++ b/fastapi/params.py @@ -783,4 +783,7 @@ class Security(Depends): use_cache: bool = True, ): super().__init__(dependency=dependency, use_cache=use_cache) - self.scopes = scopes or [] + if isinstance(scopes, str): + self.scopes = [scopes] + else: + self.scopes = scopes or [] From 7ddaebfc5827922b74708163f8df498acf94c14d Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Thu, 13 Feb 2025 22:11:52 +0000 Subject: [PATCH 03/12] =?UTF-8?q?=F0=9F=8E=A8=20[pre-commit.ci]=20Auto=20f?= =?UTF-8?q?ormat=20from=20pre-commit.com=20hooks?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- tests/test_security_oauth2_scopes.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/test_security_oauth2_scopes.py b/tests/test_security_oauth2_scopes.py index 690ccca8a..9d55081e1 100644 --- a/tests/test_security_oauth2_scopes.py +++ b/tests/test_security_oauth2_scopes.py @@ -1,4 +1,5 @@ from typing import Annotated + from fastapi import FastAPI, Security from fastapi.params import Depends from fastapi.security import OAuth2PasswordBearer From c25d62ff4b32c04b5b64d1a91a660e66e8867591 Mon Sep 17 00:00:00 2001 From: Joao-Pedro-P-Holanda Date: Thu, 13 Feb 2025 20:34:47 -0300 Subject: [PATCH 04/12] chore: adding type hint for scopes string case adding Sequence[str] type hint on the self.scopes declaration when scopes is a string in Security --- fastapi/params.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/fastapi/params.py b/fastapi/params.py index 2740b5824..b18a3f064 100644 --- a/fastapi/params.py +++ b/fastapi/params.py @@ -784,6 +784,6 @@ class Security(Depends): ): super().__init__(dependency=dependency, use_cache=use_cache) if isinstance(scopes, str): - self.scopes = [scopes] + self.scopes: Sequence[str] = [scopes] else: self.scopes = scopes or [] From 33f7758db01f1ddb23ffcc196fc7af46ad8dbbef Mon Sep 17 00:00:00 2001 From: Joao-Pedro-P-Holanda Date: Thu, 13 Feb 2025 20:39:40 -0300 Subject: [PATCH 05/12] fix: removing Annotated usages removing Annotated to keep compatibility with python 3.8 --- tests/test_security_oauth2_scopes.py | 12 +++--------- 1 file changed, 3 insertions(+), 9 deletions(-) diff --git a/tests/test_security_oauth2_scopes.py b/tests/test_security_oauth2_scopes.py index 9d55081e1..7bd0e6c2c 100644 --- a/tests/test_security_oauth2_scopes.py +++ b/tests/test_security_oauth2_scopes.py @@ -1,5 +1,3 @@ -from typing import Annotated - from fastapi import FastAPI, Security from fastapi.params import Depends from fastapi.security import OAuth2PasswordBearer @@ -14,24 +12,20 @@ oauth2_scheme = OAuth2PasswordBearer( ) -def get_security_scopes( - security_scopes: SecurityScopes, token: Annotated[str, Depends(oauth2_scheme)] -): +def get_security_scopes(security_scopes: SecurityScopes, token=Depends(oauth2_scheme)): return security_scopes.scopes @app.get("/me") async def read_single_scope( - current_scope: Annotated[list[str], Security(get_security_scopes, scopes="me")], + current_scope=Security(get_security_scopes, scopes="me"), ): return {"scopes": current_scope} @app.get("/me-and-items") async def read_single_scope( - current_scope: Annotated[ - list[str], Security(get_security_scopes, scopes=["me", "items"]) - ], + current_scope=Security(get_security_scopes, scopes=["me", "items"]), ): return {"scopes": current_scope} From 872f59b4d6acb9c09613103627c0df55e2f0bd8e Mon Sep 17 00:00:00 2001 From: Joao-Pedro-P-Holanda Date: Thu, 13 Feb 2025 20:46:33 -0300 Subject: [PATCH 06/12] fix: correcting redeclaration of function changing function redeclaration in tests/test_security_oauth2_scopes.py --- tests/test_security_oauth2_scopes.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_security_oauth2_scopes.py b/tests/test_security_oauth2_scopes.py index 7bd0e6c2c..52ce4b790 100644 --- a/tests/test_security_oauth2_scopes.py +++ b/tests/test_security_oauth2_scopes.py @@ -24,7 +24,7 @@ async def read_single_scope( @app.get("/me-and-items") -async def read_single_scope( +async def read_multiple_scopes( current_scope=Security(get_security_scopes, scopes=["me", "items"]), ): return {"scopes": current_scope} From 55d8392d8510526317fc427adbb9c38126b42cf6 Mon Sep 17 00:00:00 2001 From: Joao-Pedro-P-Holanda Date: Fri, 14 Feb 2025 10:43:27 -0300 Subject: [PATCH 07/12] chore: adding type to get_security_scopes adding type hint for get_security_scopes in tests/test_security_oauth2_scopes.py --- tests/test_security_oauth2_scopes.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/tests/test_security_oauth2_scopes.py b/tests/test_security_oauth2_scopes.py index 52ce4b790..83fb4f492 100644 --- a/tests/test_security_oauth2_scopes.py +++ b/tests/test_security_oauth2_scopes.py @@ -12,7 +12,9 @@ oauth2_scheme = OAuth2PasswordBearer( ) -def get_security_scopes(security_scopes: SecurityScopes, token=Depends(oauth2_scheme)): +def get_security_scopes( + security_scopes: SecurityScopes, token=Depends(oauth2_scheme) +) -> list[str]: return security_scopes.scopes From 9513d48196541fcc093578d9e6d199fb0779de72 Mon Sep 17 00:00:00 2001 From: Joao-Pedro-P-Holanda Date: Fri, 14 Feb 2025 13:52:06 -0300 Subject: [PATCH 08/12] fix: type hinting get_security_scopes with typing List using List from typing package, as python's list can't be subscriptable in python 3.8 --- tests/test_security_oauth2_scopes.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tests/test_security_oauth2_scopes.py b/tests/test_security_oauth2_scopes.py index 83fb4f492..a09ca62e5 100644 --- a/tests/test_security_oauth2_scopes.py +++ b/tests/test_security_oauth2_scopes.py @@ -1,3 +1,4 @@ +from typing import List from fastapi import FastAPI, Security from fastapi.params import Depends from fastapi.security import OAuth2PasswordBearer @@ -14,7 +15,7 @@ oauth2_scheme = OAuth2PasswordBearer( def get_security_scopes( security_scopes: SecurityScopes, token=Depends(oauth2_scheme) -) -> list[str]: +) -> List[str]: return security_scopes.scopes From 64b01d7762b288cf4594ef579ea0c4f6c111576f Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Fri, 14 Feb 2025 16:52:53 +0000 Subject: [PATCH 09/12] =?UTF-8?q?=F0=9F=8E=A8=20[pre-commit.ci]=20Auto=20f?= =?UTF-8?q?ormat=20from=20pre-commit.com=20hooks?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- tests/test_security_oauth2_scopes.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/test_security_oauth2_scopes.py b/tests/test_security_oauth2_scopes.py index a09ca62e5..3711d6556 100644 --- a/tests/test_security_oauth2_scopes.py +++ b/tests/test_security_oauth2_scopes.py @@ -1,4 +1,5 @@ from typing import List + from fastapi import FastAPI, Security from fastapi.params import Depends from fastapi.security import OAuth2PasswordBearer From 5e2896fd3c0b0aa880a9daec873c02e595da18b7 Mon Sep 17 00:00:00 2001 From: Joao-Pedro-P-Holanda Date: Fri, 14 Feb 2025 15:00:13 -0300 Subject: [PATCH 10/12] docs: documenting edge case in Security's scopes parameter adding a short information about the case where a string is given in place of a list of strings (or similar) on the Doc of the Security function in fastapi/param_functions.py --- fastapi/param_functions.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/fastapi/param_functions.py b/fastapi/param_functions.py index b3621626c..e44ce4f70 100644 --- a/fastapi/param_functions.py +++ b/fastapi/param_functions.py @@ -2295,7 +2295,8 @@ def Security( # noqa: N802 Doc( """ OAuth2 scopes required for the *path operation* that uses this Security - dependency. + dependency. If a string is given, the list of scopes will have only one scope + defined by the string value. The term "scope" comes from the OAuth2 specification, it seems to be intentionally vague and interpretable. It normally refers to permissions, From 329ee226b06598b36d55202f3588411a50d90179 Mon Sep 17 00:00:00 2001 From: Joao-Pedro-P-Holanda Date: Fri, 16 May 2025 08:59:37 -0300 Subject: [PATCH 11/12] refactor: change Security class typing for scopes param Make scopes a str or Sequence of str explicitly --- fastapi/params.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/fastapi/params.py b/fastapi/params.py index b18a3f064..f7619c09c 100644 --- a/fastapi/params.py +++ b/fastapi/params.py @@ -779,7 +779,7 @@ class Security(Depends): self, dependency: Optional[Callable[..., Any]] = None, *, - scopes: Optional[Sequence[str]] = None, + scopes: Optional[str | Sequence[str]] = None, use_cache: bool = True, ): super().__init__(dependency=dependency, use_cache=use_cache) From 13783d24783d147bf6a5137612d21f15af86e9e8 Mon Sep 17 00:00:00 2001 From: Joao-Pedro-P-Holanda Date: Fri, 16 May 2025 09:10:26 -0300 Subject: [PATCH 12/12] fix: use Union instead of pipe in scopes type hint --- fastapi/params.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/fastapi/params.py b/fastapi/params.py index f7619c09c..a3a03090a 100644 --- a/fastapi/params.py +++ b/fastapi/params.py @@ -779,7 +779,7 @@ class Security(Depends): self, dependency: Optional[Callable[..., Any]] = None, *, - scopes: Optional[str | Sequence[str]] = None, + scopes: Optional[Union[str, Sequence[str]]] = None, use_cache: bool = True, ): super().__init__(dependency=dependency, use_cache=use_cache)