Check Permissions in FastAPI + Stawberry GraphQL - python

I'm using BasePermission decorator as specified in documentation.
#strawberry.type
class Query:
#strawberry.field(permission_classes=[IsAuthenticated])
def user(self) -> User:
# get by token OFC
return User(user_id=1, email="vladimir#cw.tech", first_name = "Vladimir", last_name = "Kirilov")
In my impementation I use VerifyToken class as described in FastAPI auth0 documentation.
class IsAuthenticated(BasePermission):
message = "User is not authenticated"
def has_permission(self, source: Any, info: Info, **kwargs) -> bool:
print(source)
print(info)
token: str = Depends(token_auth_scheme)
print(token)
result = VerifyToken(token.credentials).verify()
if result.get("status"):
print(result)
return False
return True
So I'm trying to get and verify the BEARER from the request, but I'm not able to extract it to process it further and get the error, please advise.
{
"data": null,
"errors": [
{
"message": "'Depends' object has no attribute 'credentials'",
"locations": [
{
"line": 2,
"column": 3
}
],
"path": [
"user"
]
}
]
}

Figured it out, not the cleanest way, but here it is
class IsAuthenticated(BasePermission):
message = "User is not authenticated"
async def has_permission(self, source: Any, info: Info, **kwargs) -> bool:
request: Union[Request, WebSocket] = info.context["request"]
print(request.headers)
if "Authorization" in request.headers:
print(request.headers['Authorization'])
result = VerifyToken( request.headers['Authorization'][7:] ).verify()
if result.get("status") == "error":
print(result.get("msg"))
return False
if result.get("sub"):
print(result)
return True
return False

Related

"RuntimeError: 1 validation error for WorkBoardCreateUserResponse E success E field required (type=value_error.missing)

I'm trying to unit test my code and make sure the my code returns a dict that matches the class. The error says the success field required (type=value_error.missing). If when I make the bool Optional it shows my actual results with success = None, message = None etc. So my actual class is returning empty and not equal to my wb_response.
class WorkBoardCreateUserResponse(BaseModel):
success: bool
message: Optional[str]
data: Optional[CreateUserData]
error: Optional[str]
error_description: Optional[str]
#mock.patch("workboard.requests.post")
#mock.patch("workboard.requests.get")
def test_create_workboard_user(self, mock_get, mock_post):
wb_response = {
"success":True,
"message": "",
"data": {
"totalCount": 1,
"user":
{ "user_id": 1,
"user_name":"",
"wb_email":"wb#email",
"email": "abc#company",
"first_name": "F name",
"last_name": "L name",
"manager_id": "manager#company",
"profile": {
"title": "emp",
"company": "company"
}
}
}
}
wb_user = WorkBoardUserRequest(
email= "abc#capone",
first_name= "F name",
last_name= "L name",
manager_id= "manager#company",
profile=WorkBoardProfile(
title="title",
company="company"
)
)
workboard_resp = json.dumps(wb_response)
mock_post.return_value.text = token_resp
mock_post.status_code = 200
mock_get.json.return_value.text = workboard_resp
final_response = json.loads(workboard_resp)
print(type(final_response))
employee = WorkBoard("client", "secret", "env", "vendor_token")
response = employee.create_workboard_user(wb_user)
self.assertEqual(response, WorkBoardCreateUserResponse(**final_response))
def create_workboard_user(self, wbUser: WorkBoardUserRequest) -> WorkBoardResponse:
"""
Grant access to the user by invoking the Third Party API
"""
wb_url = (PROD_URL if self.environment ==
"PROD" else NON_PROD_URL) + CREATE_USER_ENDPOINT
# Invoke the WorkBoard API to create user access
try:
create_user_response = requests.post(
wb_url, data=wbUser.json(), verify=True, allow_redirects=False, headers=self.headers
)
response = json.loads(create_user_response.text)
create_user_response = WorkBoardCreateUserResponse(**response)
logger.info(f"WorkBoard create user response : (%s)", create_user_response)
except ValidationError as validation_error:
print(f"Error while reading response : ", str(validation_error))
raise RuntimeError(str(validation_error))
except Exception as err:
logger.exception(f"Error while creating workboard user : %s", str(err))
raise RuntimeError(str(err))
return create_user_response

In FastAPI Getting 'Not authenticated' Error after authenticate

File dir
fastapi_jwt
.env
main.py
app
api.py
model.py
auth
auth_bearer.py
auth_handler.py
fastapi_jwt/.env
secret=please_please_update_me_please
algorithm=HS256
fastapi_jwt/main.py
import uvicorn
if __name__ == "__main__":
uvicorn.run("app.api:app", host="127.0.0.1", port=8000, reload=True)
fastapi_jwt/app/api.py
from fastapi import FastAPI, Body, Depends
from starlette.responses import HTMLResponse
from app.model import PostSchema, UserSchema, UserLoginSchema
from app.auth.auth_bearer import JWTBearer
from app.auth.auth_handler import signJWT
app = FastAPI()
#app.get("/", tags=["root"])
async def read_root():
return {"message": "Welcome to your blog!."}
from app.model import PostSchema
posts = [
{
"id": 1,
"title": "Pancake",
"content": "Lorem Ipsum ..."
}
]
users = []
#app.get("/posts", tags=["posts"])
async def get_posts() -> dict:
return { "data": posts }
#app.get("/posts/{id}", tags=["posts"])
async def get_single_post(id: int) -> dict:
if id > len(posts):
return {
"error": "No such post with the supplied ID."
}
for post in posts:
if post["id"] == id:
return {
"data": post
}
#app.get("/post", dependencies=[Depends(JWTBearer())], tags=["posts"])
#app.post("/post", dependencies=[Depends(JWTBearer())], tags=["posts"])
async def add_post(post: PostSchema) -> dict:
post.id = len(posts) + 1
posts.append(post.dict())
return {"result":"Post added successfully"}
#app.post("/user/signup", tags=["user"])
async def create_user(user: UserSchema = Body(...)):
users.append(user) # replace with db call, making sure to hash the password first
return signJWT(user.email)
def check_user(data: UserLoginSchema):
for user in users:
if user.email == data.email and user.password == data.password:
return True
return False
#app.post("/user/login", tags=["user"])
async def user_login(user: UserLoginSchema = Body(...)):
if check_user(user):
return signJWT(user.email)
return {
"error": "Wrong login details!"
}
fastapi_jwt/app/model.py
from pydantic import BaseModel, Field, EmailStr
class PostSchema(BaseModel):
id: int = Field(default=None)
title: str = Field(...)
content: str = Field(...)
class Config:
schema_extra = {
"example": {
"title": "Securing FastAPI applications with JWT.",
"content": "In this tutorial, you'll learn how to secure your application by enabling authentication using JWT. We'll be using PyJWT to sign, encode and decode JWT tokens...."
}
}
class UserSchema(BaseModel):
fullname: str = Field(...)
email: EmailStr = Field(...)
password: str = Field(...)
class Config:
schema_extra = {
"example": {
"fullname": "Abdulazeez Abdulazeez Adeshina",
"email": "abdulazeez#x.com",
"password": "weakpassword"
}
}
class UserLoginSchema(BaseModel):
email: EmailStr = Field(...)
password: str = Field(...)
class Config:
schema_extra = {
"example": {
"email": "abdulazeez#x.com",
"password": "weakpassword"
}
}
fastapi_jwt/app/auth/auth_bearer.py
from fastapi import Request, HTTPException
from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials
from .auth_handler import decodeJWT
class JWTBearer(HTTPBearer):
def __init__(self, auto_error: bool = True):
super(JWTBearer, self).__init__(auto_error=auto_error)
async def __call__(self, request: Request):
credentials: HTTPAuthorizationCredentials = await super(JWTBearer, self).__call__(request)
if credentials:
if not credentials.scheme == "Bearer":
raise HTTPException(status_code=403, detail="Invalid authentication scheme.")
if not self.verify_jwt(credentials.credentials):
raise HTTPException(status_code=403, detail="Invalid token or expired token.")
return credentials.credentials
else:
raise HTTPException(status_code=403, detail="Invalid authorization code.")
def verify_jwt(self, jwtoken: str) -> bool:
isTokenValid: bool = False
try:
payload = decodeJWT(jwtoken)
except:
payload = None
if payload:
isTokenValid = True
return isTokenValid
fastapi_jwt/app/auth/auth_handler.py
import time
from typing import Dict
import jwt
from decouple import config
JWT_SECRET = config("secret")
JWT_ALGORITHM = config("algorithm")
def token_response(token: str):
return {
"access_token": token
}
def signJWT(user_id: str) -> Dict[str, str]:
payload = {
"user_id": user_id,
"expires": time.time() + 600
}
token = jwt.encode(payload, JWT_SECRET, algorithm=JWT_ALGORITHM)
return token_response(token)
def decodeJWT(token: str) -> dict:
try:
decoded_token = jwt.decode(token, JWT_SECRET, algorithms=[JWT_ALGORITHM])
return decoded_token if decoded_token["expires"] >= time.time() else None
except:
return {}
I run 'main.py', the url
" http://127.0.0.1:8000/post " shows the output "{"detail":"Not authenticated"}".
So go to the url " http://127.0.0.1:8000/docs " for FastAPI -Swagger UI interface like
this.
Then I signup and login to get the jwt token and use that token to authenticate, then go to url
" /post " in FastAPI -Swagger UI and execute to get the output {"result": "Post added successfully"}.
But in " http://127.0.0.1:8000/post " got an error and show the same ouput "{"detail":"Not authenticated"}"

Is it possible to change the pydantic error messages in fastAPI?

In the FastAPI framework, the pydantic error messages are showing like below.
{"detail": [
{
"loc": [
"body",
"location",
"name"
],
"msg": "field required",
"type": "value_error.missing"
},
{
"loc": [
"body",
"location",
"name12"
],
"msg": "extra fields not permitted",
"type": "value_error.extra"
}
]
}
I want to send a simple message: {"field-name":"error message"}.
In Pydantic document they mentioned like, create a model instance in the try: except blocks and construct the error message in the except block. But in fast API, model instance created by fastapi itself, for example, if I write an URL like below
#router.post("/", response_model=DataModelOut)
async def create_location(location: schemas.LocationIn, user: str = Depends(get_current_user) ):
return model.save(location,user)
Here the location instance created by fastapi itself is the problem.
Is there any way to construct the error message?
Actually this error messages coming from fastapi.exceptions, You can achieve that by overriding the custom exceptions,
Imagine i have simple app like this:
from fastapi import Request, status
from fastapi.encoders import jsonable_encoder
from fastapi.exceptions import RequestValidationError
from fastapi.responses import JSONResponse
from pydantic import BaseModel
#app.exception_handler(RequestValidationError)
async def validation_exception_handler(request: Request, exc: RequestValidationError):
return JSONResponse(
status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
content=jsonable_encoder({"detail": exc.errors(),
"body": exc.body,
"your_additional_errors": {"Will be": "Inside", "This":" Error message"}}),
)
class Item(BaseModel):
title: str
size: int
#app.post("/items/")
async def create_item(item: Item):
return item
If i send values invalid values to my Request body
{
"title": 22,
"size": "hehe"
}
Now the error will be more customized:
{
"detail": [
{
"loc": [
"body",
"size"
],
"msg": "value is not a valid integer",
"type": "type_error.integer"
}
],
"body": {
"title": 22,
"size": "hehe"
},
"your_additional_errors": {
"Will be": "Inside the",
"Error": "Message"
}
}
You can change the content of exception, everything is up to you.
I am writing a middle ware for it.
async def genrange(s):
import json
s = json.loads(s)
yield json.dumps({"message":{k.get("loc")[-1]:k.get("msg") for k in s['detail']},
"id":None})
#app.middleware("http")
async def add_process_time_header(request: Request, call_next):
response = await call_next(request)
status_code = response.status_code
if status_code >=300:
async for i in response.body_iterator:
data = genrange(i)
response.body_iterator = data
return response
I'm not sure you'll like my answer better than the other answers. You can create a custom pydantic, with diffrent error messages. Kinda overkill, but could solve you a specific problem. In this example, I am changing the error message, when I insert an unpermitted HttpUrlSceme.
class UrlSchemePermittedError(errors.UrlError):
code = 'url.scheme'
msg_template = 'URL scheme not cool'
def __init__(self, allowed_schemes: Set[str]):
super().__init__(allowed_schemes=allowed_schemes)
class AnyHttpUrlDirrentMessage(AnyUrl):
allowed_schemes = {'http', 'https'}
#classmethod
def validate_parts(cls, parts: Dict[str, str]) -> Dict[str, str]:
"""
A method used to validate parts of an URL.
Could be overridden to set default values for parts if missing
"""
scheme = parts['scheme']
if scheme is None:
raise errors.UrlSchemeError()
if cls.allowed_schemes and scheme.lower() not in cls.allowed_schemes:
raise UrlSchemePermittedError(cls.allowed_schemes)
port = parts['port']
if port is not None and int(port) > 65_535:
raise errors.UrlPortError()
user = parts['user']
if cls.user_required and user is None:
raise errors.UrlUserInfoError()
return parts

Change JSON-RPC RESPONSE

I am using ODOO 11.0
how to return simple JSON object without JSON-RPC additional parameters
Here is my odoo controller code:
#http.route('/userappoint/webhook_test/',type='json', auth='public',method=['POST'], csrf=False,website=True)
def webhook_test(self,**kw):
response = {
'speech' : 'hello my name is shubham',
'displayText' : 'hello testing',
'source' : 'webhook'
}
return response
And I am getting this result :
{
"result": {
"displayText": "hello testing",
"source": "webhook",
"speech": "hello my name is shubham"
},
"id": "6eaced3e-6b0d-4518-9710-de91eaf16dd9",
"jsonrpc": "2.0"
}
But I need this :
{
"speech": "hello my name is shubham",
"displayText": "hello testing",
"source": "webhook"
}
Any help to point me in the right direction?
Thanks
Works on Odoo11. Just edit below portion of the function _json_response defined at odoo/odoo11/odoo/http.py near about line no : 621-630 as below & Restart the odoo service.
def _json_response(self, result=None, error=None):
response = {
'jsonrpc': '2.0',
'id': self.jsonrequest.get('id')
}
if error is not None:
response['error'] = error
if result is not None:
response['result'] = result
to new:
def _json_response(self, result=None, error=None):
response = {}
if error is not None:
response = error
if result is not None:
response = result
Then, restart the odoo service
Place the following code in any of your controller before you initialize the controller class
from odoo import http
from odoo.http import request, Response, JsonRequest
from odoo.tools import date_utils
class JsonRequestNew(JsonRequest):
def _json_response(self, result=None, error=None):
# response = {
# 'jsonrpc': '2.0',
# 'id': self.jsonrequest.get('id')
# }
# if error is not None:
# response['error'] = error
# if result is not None:
# response['result'] = result
responseData = super(JsonRequestNew, self)._json_response(result=result,error=error)
response = {}
if error is not None:
response = error
if result is not None:
response = result
mime = 'application/json'
body = json.dumps(response, default=date_utils.json_default)
return Response(
body, status=error and error.pop('http_status', 200) or 200,
headers=[('Content-Type', mime), ('Content-Length', len(body))]
)
class RootNew(http.Root):
def get_request(self, httprequest):
# deduce type of request
jsonResponse = super(RootNew, self).get_request(httprequest=httprequest)
if httprequest.mimetype in ("application/json", "application/json-rpc"):
return JsonRequestNew(httprequest)
else:
return jsonResponse
http.root = RootNew()
class MyController(http.Controller):

Creating a base response for API calls

I want to create an API by using django-rest-framework. So far I've managed to setup one endpoint of API and managed to fetch all items. A basic response (without the BaseResponse class described later) would look like this:
[
{
"uuid": "1db6a08d-ec63-4beb-8b41-9b042c53ab83",
"created_at": "2018-03-12T19:25:07.073620Z",
"updated_at": "2018-03-12T19:25:37.904350Z",
"deleted_at": null,
"random_name": "random name"
}
]
The result I would like to achieve would be something like this:
[
"success": true
"message": "Some exception message",
"data" :{
"uuid": "1db6a08d-ec63-4beb-8b41-9b042c53ab83",
"created_at": "2018-03-12T19:25:07.073620Z",
"updated_at": "2018-03-12T19:25:37.904350Z",
"deleted_at": null,
"random_name": "random name"
}
]
I managed to achieve this by creating a BaseReponse class and in view I simply return BaseResponse.to_dict() (a method that I have created inside of class).
class BaseResponse(object):
data = None
success = False
message = None
def __init__(self, data, exception):
self.data = data
self.message = str(exception) if exception is not None else None
self.success = exception is None
def to_dict(self):
return {
'success': self.success,
'message': self.message,
'data': self.data,
}
View:
class RandomModelList(APIView):
def get(self, request, format=None):
exception = None
models = None
try:
models = RandomModel.objects.all()
except Exception as e:
exception = e
serializer = RandomModelSerializer(models, many=True)
base_response = BaseResponse(data=serializer.data, exception=exception)
return Response(base_response.to_dict())
I want to mention that with the current code everything its working as expected but I have a huge double about the code (I just feel like I reinvented the wheel). Can someone tell me if this is the optimal solution for my problem and if not what should I change/use?
You can create a Custom Renderer instead. Something like
class CustomRenderer(JSONRenderer):
def render(self, data, accepted_media_type=None, renderer_context=None):
resp = {
'data': data
}
return super(CustomRenderer, self).render(resp, accepted_media_type, renderer_context)
Then create a middleware like
class CustomResponseMiddleware:
def __init__(self, get_response):
self.get_response = get_response
def __call__(self, request):
response = self.get_response(request)
response.data.update({'success': is_client_error(response.status_code) or is_server_error(response.status_code)})
return response

Categories

Resources