Uh oh!
There was an error while loading.Please reload this page.
- Notifications
You must be signed in to change notification settings - Fork8.7k
Comments
🐛 Fix cached dependencies when using a dependency inSecurity() and other places (e.g.Depends()) with different OAuth2 scopes#2945
Conversation
codecovbot commentedMar 13, 2021 • edited
Loading Uh oh!
There was an error while loading.Please reload this page.
edited
Uh oh!
There was an error while loading.Please reload this page.
Codecov Report
@@ Coverage Diff @@## master #2945 +/- ##========================================= Coverage 100.00% 100.00% ========================================= Files 533 533 Lines 13746 13755 +9 =========================================+ Hits 13746 13755 +9
Help us with your feedback. Take ten seconds to tell ushow you rate us. Have a feature suggestion?Share it here. |
📝 Docs preview for commit d8b63cdaebaf9c37b195bfa0bca54f3b9b0c8fa2 at:https://604c78b2648d6ca4039cc914--fastapi.netlify.app |
laggardkernel commentedJun 15, 2021 • edited
Loading Uh oh!
There was an error while loading.Please reload this page.
edited
Uh oh!
There was an error while loading.Please reload this page.
I've also considered implementing
|
d8b63cd to2c18368Comparelaggardkernel commentedJun 16, 2021 • edited
Loading Uh oh!
There was an error while loading.Please reload this page.
edited
Uh oh!
There was an error while loading.Please reload this page.
Updated once after tweaking the related test. Also rebased the branch to current master in this force push. |
Security() and other places (e.g.Depends()) with different OAuth2 scopes📝 Docs preview for commit3399de4 at:https://6304d5c087148506d673b746--fastapi.netlify.app |
tiangolo commentedAug 23, 2022
Thanks for the detailed explanation and for your contribution@laggardkernel! 🙇 🚀 🍰 |
qmorek commentedSep 4, 2022 • edited
Loading Uh oh!
There was an error while loading.Please reload this page.
edited
Uh oh!
There was an error while loading.Please reload this page.
Hi guys, fromfastapiimportFastAPI,Depends,Securityfromfastapi.securityimportSecurityScopesdefget_obj(required_scopes:SecurityScopes,):print(f'get_obj called with scopes: "{required_scopes.scope_str}"')return'object'defcheck_user_role_for_obj(required_scopes:SecurityScopes,obj=Depends(get_obj),):print(f'check_user_role_for_obj called for{obj} with scopes:, "{required_scopes.scope_str}"')return'object role'app=FastAPI()@app.get('/',dependencies=[Security(check_user_role_for_obj,scopes=['read_scope'])],)defget_obj_with_scopes(obj=Depends(get_obj),):returnobj@app.put('/',dependencies=[Security(check_user_role_for_obj,scopes=['edit_scope'])],)defput_obj_with_scopes(obj=Depends(get_obj),):returnobj@app.get('/child_obj',dependencies=[Security(check_user_role_for_obj,scopes=['read_child_scope'])],)defget_obj_with_scopes():return'child object' Here is output from calling get_obj_with_scopes endpoint: as you can see get_obj was called twice.... I am just throwing it for consideration. |
nparley commentedSep 5, 2022
Using a database session inside a |
qmorek commentedSep 20, 2022
Hi@tiangolo, |
laggardkernel commentedSep 21, 2022 • edited
Loading Uh oh!
There was an error while loading.Please reload this page.
edited
Uh oh!
There was an error while loading.Please reload this page.
To understand dependency cache, we may start from what features are added by FastAPI upon Starlette.
In the view/router function, we parse the
The introduction of dependency ( asyncdefsum_c(a:int=0,b:int=0):returna+b@app.get("/sum")asyncdefcalc_sum(c:int=Depends(sum_c)):returnc In the above example, the view func doesn't accept query param To avoid duplicated, high cost calculation in some
nparley qmorek |
qmorek commentedSep 21, 2022
Hi@laggardkernel, could you eplain what you mean by:
|
qmorek commentedSep 21, 2022 • edited
Loading Uh oh!
There was an error while loading.Please reload this page.
edited
Uh oh!
There was an error while loading.Please reload this page.
I took example from:https://fastapi.tiangolo.com/advanced/security/oauth2-scopes/#dependency-tree-and-scopes fromtypingimportDictfromdatetimeimportdatetime,timedeltafromtypingimportList,UnionfromfastapiimportDepends,FastAPI,HTTPException,Security,statusfromfastapi.securityimport (OAuth2PasswordBearer,OAuth2PasswordRequestForm,SecurityScopes,)fromjoseimportJWTError,jwtfrompydanticimportBaseModel,ValidationError# to get a string like this run:# openssl rand -hex 32SECRET_KEY="09d25e094faa6ca2556c818166b7a9563b93f7099f6f0f4caa6cf63b88e8d3e7"ALGORITHM="HS256"ACCESS_TOKEN_EXPIRE_MINUTES=30fake_dict_db= {"users": {"johndoe": {"username":"johndoe","full_name":"John Doe","email":"johndoe@example.com","cleartext_password":"password","disabled":False, },"alice": {"username":"alice","full_name":"Alice Chains","email":"alicechains@example.com","cleartext_password":"password","disabled":True, }, },"items": [ {"name":"First" }, {"name":"Second" } ]}classToken(BaseModel):access_token:strtoken_type:strclassTokenData(BaseModel):username:Union[str,None]=Nonescopes:List[str]= []classUser(BaseModel):username:stremail:Union[str,None]=Nonefull_name:Union[str,None]=Nonedisabled:Union[bool,None]=NoneclassUserInDB(User):cleartext_password:stroauth2_scheme=OAuth2PasswordBearer(tokenUrl="token",scopes={"me":"Read information about the current user.","items":"Read items."},)app=FastAPI()defverify_password(plain_password,db_password):returnplain_password==db_passworddefget_user(db,username:str):users_db=db['users']ifusernameinusers_db:user_dict=users_db[username]returnUserInDB(**user_dict)defauthenticate_user(fake_db,username:str,password:str):user=get_user(fake_db,username)ifnotuser:returnFalseifnotverify_password(password,user.cleartext_password):returnFalsereturnuserdefcreate_access_token(data:dict,expires_delta:Union[timedelta,None]=None):to_encode=data.copy()ifexpires_delta:expire=datetime.utcnow()+expires_deltaelse:expire=datetime.utcnow()+timedelta(minutes=15)to_encode.update({"exp":expire})encoded_jwt=jwt.encode(to_encode,SECRET_KEY,algorithm=ALGORITHM)returnencoded_jwtdefget_db()->Dict:print('get_db called')returnfake_dict_dbasyncdefget_current_user(security_scopes:SecurityScopes,db:Dict=Depends(get_db),token:str=Depends(oauth2_scheme),):print('required scopes are:',security_scopes.scope_str)ifsecurity_scopes.scopes:authenticate_value=f'Bearer scope="{security_scopes.scope_str}"'else:authenticate_value=f"Bearer"credentials_exception=HTTPException(status_code=status.HTTP_401_UNAUTHORIZED,detail="Could not validate credentials",headers={"WWW-Authenticate":authenticate_value}, )try:payload=jwt.decode(token,SECRET_KEY,algorithms=[ALGORITHM])username:str=payload.get("sub")ifusernameisNone:raisecredentials_exceptiontoken_scopes=payload.get("scopes", [])token_data=TokenData(scopes=token_scopes,username=username)except (JWTError,ValidationError):raisecredentials_exceptionuser=get_user(db,username=token_data.username)ifuserisNone:raisecredentials_exceptionforscopeinsecurity_scopes.scopes:ifscopenotintoken_data.scopes:raiseHTTPException(status_code=status.HTTP_401_UNAUTHORIZED,detail="Not enough permissions",headers={"WWW-Authenticate":authenticate_value}, )returnuserasyncdefget_current_active_user(current_user:User=Security(get_current_user,scopes=["me"])):ifcurrent_user.disabled:raiseHTTPException(status_code=400,detail="Inactive user")returncurrent_user@app.post("/token",response_model=Token)asyncdeflogin_for_access_token(db:Dict=Depends(get_db),form_data:OAuth2PasswordRequestForm=Depends(),):user=authenticate_user(db,form_data.username,form_data.password)ifnotuser:raiseHTTPException(status_code=400,detail="Incorrect username or password")access_token_expires=timedelta(minutes=ACCESS_TOKEN_EXPIRE_MINUTES)access_token=create_access_token(data={"sub":user.username,"scopes":form_data.scopes},expires_delta=access_token_expires, )return {"access_token":access_token,"token_type":"bearer"}@app.get("/users/me/",response_model=User)asyncdefread_users_me(current_user:User=Depends(get_current_active_user)):returncurrent_user@app.get("/users/me/items/")asyncdefread_own_items(db:Dict=Depends(get_db),current_user:User=Security(get_current_active_user,scopes=["items"])):return {'items':db['items'],'current_user':current_user }@app.get("/status/")asyncdefread_system_status(current_user:User=Depends(get_current_user)):return {"status":"ok"} if you run this example code and call endpoint: /users/me/items/ you will see in console: so as you see get_db dependency is called twice... Maybe my modifications are not correct, but it consistent with project template creted by tiangolo:https://github.com/tiangolo/full-stack-fastapi-postgresql I am aware that it was created before this bug was discovered, so may also not be correct in this matter, but I would love to know how to do this correctly.... |
laggardkernel commentedSep 23, 2022
@qmorek Sorry, my former solution is wrong. I forgot something, the @app.get("/users/me/items/")asyncdefread_own_items(db:Dict=Depends(get_db),current_user:User=Security(get_current_active_user,scopes=["items"])):asyncdefget_current_active_user(current_user:User=Security(get_current_user,scopes=["me"])):asyncdefget_current_user(security_scopes:SecurityScopes,db:Dict=Depends(get_db),token:str=Depends(oauth2_scheme),):
confirmed by printing dependency_cacheTwo different cache keys
@tiangolo It seems making |
laggardkernel commentedSep 23, 2022
@tiangolo I have a solution but not sure if this contradicts the original security design.
|
qmorek commentedDec 8, 2022
@tiangolo would you be able to have a look on this please? |
nyte-owl commentedMay 10, 2023
This issue of scopes affecting |
DurandA commentedJun 27, 2023
I am also affected by this issue and opened a discussion (#6024).
@laggardkernel This is the way I wanted to implement a fix when poking with the cache code. I could also make a PR if we get some indication from@tiangolo that this approach is acceptable. Also we could be on the conservative side and introduce a new parameter to |
DurandA commentedJul 3, 2023
I created#9790 to fix this issue.@laggardkernel Let me know what you think of it. |
Barnyclark commentedAug 5, 2023
Is there a timeline for this to be PR to be merged? This bug is causing a few issues with running FastAPI in production because it halves our available db connections - every request requires two db sessions. |
Barnyclark commentedAug 7, 2023
@DurandA we have been doing some more testing on this and have found that if you set up the DB using middleware you don't have this issue. This is because although you still have two entries in the cache for This approach also limits the amount of refactoring needed because you are still using |
DurandA commentedAug 7, 2023
Thank you for sharing your solution. IMHO, this is clean enough to be put in production while waiting for an update on this. The only downside I see is that a session is established regardless of whether a DB session is required by the request. |
DurandA commentedAug 8, 2023
Hi@brian-montgomery. You provided an interesting solution to this problem in a comment here but it somehow disappeared. Was there any particular issue with your proposal? |
YuriiMotov commentedFeb 27, 2024
We continue to discuss the mentioned problem (about dependency caching) caused by this PR. |
Uh oh!
There was an error while loading.Please reload this page.
To make
.security_scopescontribute to.cache_key, parametersecurity_scopesmust be passed toDependant.__init__(). We forgot to do it in this way. Reassignment after initdependant_instance.security_scopes = ["dummy"]doesn't change the.cache_keyanymore. Main related logic is atget_sub_dependant().This makes
Securitywith different scopes (OAuth2 scopes) share the same cache. If we haveSecurity(func)andSecurity(func, scopes=['scope1'])in a dependency tree. Only one of them will be really called.The bug could be traced back tobff5dbb on 6/6/19 when the
dependency_cacheis first introduced. Seems a serious bug for anyone usingSecurityin dependencies, better fix it soon.