diff --git a/docs/en/docs/advanced/security/oauth2-scopes.md b/docs/en/docs/advanced/security/oauth2-scopes.md index 67c927cd0..1d5acd7f0 100644 --- a/docs/en/docs/advanced/security/oauth2-scopes.md +++ b/docs/en/docs/advanced/security/oauth2-scopes.md @@ -106,7 +106,17 @@ Now we declare that the *path operation* for `/users/me/items/` requires the sco For this, we import and use `Security` from `fastapi`. -You can use `Security` to declare dependencies (just like `Depends`), but `Security` also receives a parameter `scopes` with a list of scopes (strings). +You can use `Security` to declare dependencies (just like `Depends`), but `Security` also receives a parameter `oauth_scopes` with a list of scopes (strings). + +/// note + +Before version 0.121.4, the name of this parameter was `scopes`. + +Since FastAPI 0.121.4, the `scopes` parameter has been deprecated in favor of `oauth_scopes` +to avoid confusing it with the `scope` parameter, which is used to specify when the exit code +of dependencies with `yield` should run. + +/// In this case, we pass a dependency function `get_current_active_user` to `Security` (the same way we would do with `Depends`). @@ -124,7 +134,7 @@ We are doing it here to demonstrate how **FastAPI** handles scopes declared at d /// -{* ../../docs_src/security/tutorial005_an_py310.py hl[5,141,172] *} +{* ../../docs_src/security/tutorial005_an_py310.py hl[5,141,173] *} /// info | Technical Details @@ -213,13 +223,13 @@ Here's how the hierarchy of dependencies and scopes looks like: * This `security_scopes` parameter has a property `scopes` with a `list` containing all these scopes declared above, so: * `security_scopes.scopes` will contain `["me", "items"]` for the *path operation* `read_own_items`. * `security_scopes.scopes` will contain `["me"]` for the *path operation* `read_users_me`, because it is declared in the dependency `get_current_active_user`. - * `security_scopes.scopes` will contain `[]` (nothing) for the *path operation* `read_system_status`, because it didn't declare any `Security` with `scopes`, and its dependency, `get_current_user`, doesn't declare any `scopes` either. + * `security_scopes.scopes` will contain `[]` (nothing) for the *path operation* `read_system_status`, because it didn't declare any `Security` with `oauth_scopes`, and its dependency, `get_current_user`, doesn't declare any `oauth_scopes` either. /// tip The important and "magic" thing here is that `get_current_user` will have a different list of `scopes` to check for each *path operation*. -All depending on the `scopes` declared in each *path operation* and each dependency in the dependency tree for that specific *path operation*. +All depending on the `oauth_scopes` declared in each *path operation* and each dependency in the dependency tree for that specific *path operation*. /// @@ -271,4 +281,4 @@ But in the end, they are implementing the same OAuth2 standard. ## `Security` in decorator `dependencies` { #security-in-decorator-dependencies } -The same way you can define a `list` of `Depends` in the decorator's `dependencies` parameter (as explained in [Dependencies in path operation decorators](../../tutorial/dependencies/dependencies-in-path-operation-decorators.md){.internal-link target=_blank}), you could also use `Security` with `scopes` there. +The same way you can define a `list` of `Depends` in the decorator's `dependencies` parameter (as explained in [Dependencies in path operation decorators](../../tutorial/dependencies/dependencies-in-path-operation-decorators.md){.internal-link target=_blank}), you could also use `Security` with `oauth_scopes` there. diff --git a/docs_src/security/tutorial005.py b/docs_src/security/tutorial005.py index fdd73bcd8..256101fa4 100644 --- a/docs_src/security/tutorial005.py +++ b/docs_src/security/tutorial005.py @@ -138,7 +138,7 @@ async def get_current_user( async def get_current_active_user( - current_user: User = Security(get_current_user, scopes=["me"]), + current_user: User = Security(get_current_user, oauth_scopes=["me"]), ): if current_user.disabled: raise HTTPException(status_code=400, detail="Inactive user") @@ -167,7 +167,7 @@ async def read_users_me(current_user: User = Depends(get_current_active_user)): @app.get("/users/me/items/") async def read_own_items( - current_user: User = Security(get_current_active_user, scopes=["items"]), + current_user: User = Security(get_current_active_user, oauth_scopes=["items"]), ): return [{"item_id": "Foo", "owner": current_user.username}] diff --git a/docs_src/security/tutorial005_an.py b/docs_src/security/tutorial005_an.py index e1d7b4f62..513049331 100644 --- a/docs_src/security/tutorial005_an.py +++ b/docs_src/security/tutorial005_an.py @@ -139,7 +139,7 @@ async def get_current_user( async def get_current_active_user( - current_user: Annotated[User, Security(get_current_user, scopes=["me"])], + current_user: Annotated[User, Security(get_current_user, oauth_scopes=["me"])], ): if current_user.disabled: raise HTTPException(status_code=400, detail="Inactive user") @@ -170,7 +170,9 @@ async def read_users_me( @app.get("/users/me/items/") async def read_own_items( - current_user: Annotated[User, Security(get_current_active_user, scopes=["items"])], + current_user: Annotated[ + User, Security(get_current_active_user, oauth_scopes=["items"]) + ], ): return [{"item_id": "Foo", "owner": current_user.username}] diff --git a/docs_src/security/tutorial005_an_py310.py b/docs_src/security/tutorial005_an_py310.py index df55951c0..f28ad350f 100644 --- a/docs_src/security/tutorial005_an_py310.py +++ b/docs_src/security/tutorial005_an_py310.py @@ -138,7 +138,7 @@ async def get_current_user( async def get_current_active_user( - current_user: Annotated[User, Security(get_current_user, scopes=["me"])], + current_user: Annotated[User, Security(get_current_user, oauth_scopes=["me"])], ): if current_user.disabled: raise HTTPException(status_code=400, detail="Inactive user") @@ -169,7 +169,9 @@ async def read_users_me( @app.get("/users/me/items/") async def read_own_items( - current_user: Annotated[User, Security(get_current_active_user, scopes=["items"])], + current_user: Annotated[ + User, Security(get_current_active_user, oauth_scopes=["items"]) + ], ): return [{"item_id": "Foo", "owner": current_user.username}] diff --git a/docs_src/security/tutorial005_an_py39.py b/docs_src/security/tutorial005_an_py39.py index 983c1c22c..190547caf 100644 --- a/docs_src/security/tutorial005_an_py39.py +++ b/docs_src/security/tutorial005_an_py39.py @@ -138,7 +138,7 @@ async def get_current_user( async def get_current_active_user( - current_user: Annotated[User, Security(get_current_user, scopes=["me"])], + current_user: Annotated[User, Security(get_current_user, oauth_scopes=["me"])], ): if current_user.disabled: raise HTTPException(status_code=400, detail="Inactive user") @@ -169,7 +169,9 @@ async def read_users_me( @app.get("/users/me/items/") async def read_own_items( - current_user: Annotated[User, Security(get_current_active_user, scopes=["items"])], + current_user: Annotated[ + User, Security(get_current_active_user, oauth_scopes=["items"]) + ], ): return [{"item_id": "Foo", "owner": current_user.username}] diff --git a/docs_src/security/tutorial005_py310.py b/docs_src/security/tutorial005_py310.py index d08e2c59f..c3e89e932 100644 --- a/docs_src/security/tutorial005_py310.py +++ b/docs_src/security/tutorial005_py310.py @@ -137,7 +137,7 @@ async def get_current_user( async def get_current_active_user( - current_user: User = Security(get_current_user, scopes=["me"]), + current_user: User = Security(get_current_user, oauth_scopes=["me"]), ): if current_user.disabled: raise HTTPException(status_code=400, detail="Inactive user") @@ -166,7 +166,7 @@ async def read_users_me(current_user: User = Depends(get_current_active_user)): @app.get("/users/me/items/") async def read_own_items( - current_user: User = Security(get_current_active_user, scopes=["items"]), + current_user: User = Security(get_current_active_user, oauth_scopes=["items"]), ): return [{"item_id": "Foo", "owner": current_user.username}] diff --git a/docs_src/security/tutorial005_py39.py b/docs_src/security/tutorial005_py39.py index 5bde47ef4..4a99de613 100644 --- a/docs_src/security/tutorial005_py39.py +++ b/docs_src/security/tutorial005_py39.py @@ -138,7 +138,7 @@ async def get_current_user( async def get_current_active_user( - current_user: User = Security(get_current_user, scopes=["me"]), + current_user: User = Security(get_current_user, oauth_scopes=["me"]), ): if current_user.disabled: raise HTTPException(status_code=400, detail="Inactive user") @@ -167,7 +167,7 @@ async def read_users_me(current_user: User = Depends(get_current_active_user)): @app.get("/users/me/items/") async def read_own_items( - current_user: User = Security(get_current_active_user, scopes=["items"]), + current_user: User = Security(get_current_active_user, oauth_scopes=["items"]), ): return [{"item_id": "Foo", "owner": current_user.username}] diff --git a/fastapi/dependencies/utils.py b/fastapi/dependencies/utils.py index 4b69e39a1..06b0a70aa 100644 --- a/fastapi/dependencies/utils.py +++ b/fastapi/dependencies/utils.py @@ -126,8 +126,8 @@ def get_parameterless_sub_dependant(*, depends: params.Depends, path: str) -> De "A parameter-less dependency must have a callable dependency" ) use_security_scopes: List[str] = [] - if isinstance(depends, params.Security) and depends.scopes: - use_security_scopes.extend(depends.scopes) + if isinstance(depends, params.Security) and depends.oauth_scopes: + use_security_scopes.extend(depends.oauth_scopes) return get_dependant( path=path, call=depends.dependency, @@ -276,8 +276,8 @@ def get_dependant( ) use_security_scopes = security_scopes or [] if isinstance(param_details.depends, params.Security): - if param_details.depends.scopes: - use_security_scopes.extend(param_details.depends.scopes) + if param_details.depends.oauth_scopes: + use_security_scopes.extend(param_details.depends.oauth_scopes) sub_dependant = get_dependant( path=path, call=param_details.depends.dependency, diff --git a/fastapi/param_functions.py b/fastapi/param_functions.py index 0e4a8b034..2ec498d0f 100644 --- a/fastapi/param_functions.py +++ b/fastapi/param_functions.py @@ -1,3 +1,4 @@ +import warnings from typing import ( Any, Callable, @@ -2322,6 +2323,29 @@ def Security( # noqa: N802 ), ] = None, *, + oauth_scopes: Annotated[ + Optional[ + Union[ + List[str], + Tuple[str, ...], + Set[str], + FrozenSet[str], + ] + ], + Doc( + """ + OAuth2 scopes required for the *path operation* that uses this Security + dependency. + + The term "scope" comes from the OAuth2 specification, it seems to be + intentionally vague and interpretable. It normally refers to permissions, + in cases to roles. + + These scopes are integrated with OpenAPI (and the API docs at `/docs`). + So they are visible in the OpenAPI specification. + """ + ), + ] = None, scopes: Annotated[ Optional[ Union[ @@ -2342,6 +2366,26 @@ def Security( # noqa: N802 These scopes are integrated with OpenAPI (and the API docs at `/docs`). So they are visible in the OpenAPI specification. + + This parameter is deprecated in favor of `oauth_scopes`. + """ + ), + deprecated( + """ + In order to avoid confusion with `scope` parameter, the `scopes` parameter + is deprecated in favor of `oauth_scopes`. + + To specify dependency scope for dependencies with `yield` use `scope` parameter: + + ```python + Security(dependency_fn, scope="function") + ``` + + To specify OAuth2 scopes use `oauth_scopes` parameter: + + ```python + Security(dependency_fn, oauth_scopes=["items", "users"]) + ``` ) """ ), @@ -2391,23 +2435,28 @@ def Security( # noqa: N802 @app.get("/users/me/items/") async def read_own_items( - current_user: Annotated[User, Security(get_current_active_user, scopes=["items"])] + current_user: Annotated[ + User, Security(get_current_active_user, oauth_scopes=["items"]) + ] ): return [{"item_id": "Foo", "owner": current_user.username}] ``` """ - if isinstance(scopes, str): - if scopes in ("function", "request"): - raise FastAPIError( - "Invalid value for `scopes` parameter in Security(). " - "You probably meant to use the `scope` parameter instead of `scopes`. " - "Expected a sequence of strings (e.g., ['admin', 'user']), but received a single string." - ) - raise FastAPIError( - "Invalid value for `scopes` parameter in Security(). " - "Expected a sequence of strings (e.g., ['admin', 'user']), but received a single string. " - "Wrap it in a list: scopes=['your_scope'] instead of scopes='your_scope'." + if scopes is not None: + warnings.warn( + ( + "The 'scopes' parameter in Security() is deprecated in favor of " + "'oauth_scopes' in order to avoid confusion with 'scope' parameter." + ), + DeprecationWarning, + stacklevel=2, ) - return params.Security(dependency=dependency, scopes=scopes, use_cache=use_cache) + oauth_scopes = oauth_scopes or scopes + + return params.Security( + dependency=dependency, + oauth_scopes=oauth_scopes, + use_cache=use_cache, + ) diff --git a/fastapi/params.py b/fastapi/params.py index f92e2025c..3097fd6fd 100644 --- a/fastapi/params.py +++ b/fastapi/params.py @@ -781,7 +781,7 @@ class Depends: @dataclass class Security(Depends): - scopes: Optional[ + oauth_scopes: Optional[ Union[ List[str], Tuple[str, ...], diff --git a/tests/test_dependency_cache.py b/tests/test_dependency_cache.py index 08fb9b74f..d3f43ce39 100644 --- a/tests/test_dependency_cache.py +++ b/tests/test_dependency_cache.py @@ -38,8 +38,8 @@ async def get_sub_counter_no_cache( @app.get("/scope-counter") async def get_scope_counter( count: int = Security(dep_counter), - scope_count_1: int = Security(dep_counter, scopes=["scope"]), - scope_count_2: int = Security(dep_counter, scopes=["scope"]), + scope_count_1: int = Security(dep_counter, oauth_scopes=["scope"]), + scope_count_2: int = Security(dep_counter, oauth_scopes=["scope"]), ): return { "counter": count, diff --git a/tests/test_dependency_paramless.py b/tests/test_dependency_paramless.py index 9c3cc3878..225db29e5 100644 --- a/tests/test_dependency_paramless.py +++ b/tests/test_dependency_paramless.py @@ -28,14 +28,14 @@ def process_auth( @app.get("/get-credentials") def get_credentials( - credentials: Annotated[dict, Security(process_auth, scopes=["a", "b"])], + credentials: Annotated[dict, Security(process_auth, oauth_scopes=["a", "b"])], ): return credentials @app.get( "/parameterless-with-scopes", - dependencies=[Security(process_auth, scopes=["a", "b"])], + dependencies=[Security(process_auth, oauth_scopes=["a", "b"])], ) def get_parameterless_with_scopes(): return {"status": "ok"} diff --git a/tests/test_dependency_security_overrides.py b/tests/test_dependency_security_overrides.py index b89d82db4..efc5a8dd0 100644 --- a/tests/test_dependency_security_overrides.py +++ b/tests/test_dependency_security_overrides.py @@ -25,7 +25,7 @@ def get_data_override(): @app.get("/user") def read_user( - user_data: Tuple[str, List[str]] = Security(get_user, scopes=["foo", "bar"]), + user_data: Tuple[str, List[str]] = Security(get_user, oauth_scopes=["foo", "bar"]), data: List[int] = Depends(get_data), ): return {"user": user_data[0], "scopes": user_data[1], "data": data}