I am developing a Flask API using Flask Restful. I wonder if there is a clean way for authorizing users that would not force me to code duplication. I use Flask-JWT-Extended for authentication in my API.
I have got some endpoints that I want to be accessible only by user with admin role OR the user, that is related to a given resource.
So, let's say I'd like to enable user to obtain information about their account and I'd like to prevent other users from accessing this information. For now, I am solving it this way:
from flask import request, Response
from flask_restful import Resource, reqparse
from flask_jwt_extended import (create_access_token, create_refresh_token, jwt_required, get_jwt_identity)
from app.mod_auth.models import User
[...]
class UserApi(Resource):
#jwt_required()
def get(self, name):
current_user = User.find_by_login(get_jwt_identity())
if current_user.login==name or current_user.role==1:
user = User.query.filter_by(login=name).first_or_404(description="User not found")
return user.json()
else:
return {'message': 'You are not authorized to access this data.'}, 403
[...]
So first I check, if there's correct and valid JWT token in the request, and then, basing on the token I check if the user related with the token is the same, as the user, whose data is being returned. Other way for accessing data is user with role 1, which I treat as an administrative role.
This is the part of my User's model:
[...]
class User(Base):
login = db.Column(db.String(128), nullable=False)
email = db.Column(db.String(128), unique=True, nullable=False)
password = db.Column(db.String(192), nullable=False)
active = db.Column(db.Boolean(), default=True, nullable=False)
role = db.Column(db.SmallInteger, default=0, nullable=False)
[...]
Of course, soon I'll have a few endpoints with data specific for user.
I have found an example of custom operator in Flask-JWT-Extended, that provides an authorization for admin users: flask-jwt-extended admin authz - but on the other hand, it does not support user-specific authorization. I have no idea, how to improve that snippet in order to verify, if the user requesting for an resource is a specific user with rights to the resource.
How can I define a custom operator, that will provide correct access to the user-specific data?
Maybe I should include some kind of owner data in each DB model, that should support authorization, and verify that in the requests, as in the example above?
Thanks in advance
You can create a decorator that checks the identity of the user:
def validate_user(role_authorized:list() = [1]):
def decorator(fn):
#wraps(fn)
def wrapper(*args, **kwargs):
name = request.path.rsplit('/', 1)[-1]
current_user = User.find_by_login(get_jwt_identity())
if (current_user.login == name) or (current_user.role in role_authorized):
kwargs["logged_user"] = current_user # If you need to use the user object in the future you can use this by passing it through the kwargs params
return fn(*args, **kwargs)
else:
return {'message': 'You are not authorized to access this data.'}, 403
return wrapper
return decorator
class Test(Resource):
#jwt_required()
#list of the roles authorized for this endpoint = [1]
#validate_user([1])
def post(self, name, **kwargs):
#logged_user = kwargs["logged_user"] # Logged in User object
user = User.query.filter_by(login=name).first_or_404(description="User not found")
return user.json()
I have a FastAPI project in which I am using Async SQLAlchemy orm and postgresql as db. Basically it is a simple CRUD based Job Board I've manage to create the CRUD operations successfully and they are working as I expected. The issue I'm stumbled upon is user authentication I'm trying to implementing authentication via JWT on user registration, user will fill out the fields, username, email and password then an verification email will be sent to that user email to verify the JWT token after that is_active field will be True which is by default False. I tried couple of ways but couldn't succeed, I'm having difficulty adding the user to the database.
routes/route_user.py:
from fastapi import APIRouter, HTTPException, status
from fastapi import Depends
from jose import jwt
from db.models.users import User
from schemas.users import UserCreate, ShowUser
from db.repository.users_data_access_layer import Users
from core.auth import Auth
from core.hashing import Hasher
from core.mailer import Mailer
from core.config import Settings
from depends import get_user_db
router = APIRouter()
get_settings = Settings()
#router.post("/", response_model=ShowUser)
async def create_user(form_data: UserCreate = Depends(), users: Users = Depends(get_user_db)):
if await users.check_user(email=form_data.email) is not None:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="User already exists"
)
elif await users.check_username(username=form_data.username) is not None:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="Username already exists"
)
new_user = User(email=form_data.email,
username=form_data.username,
hashed_password=Auth.get_password_hash(form_data.password)
)
await users.register_user(new_user)
print(new_user)
confirmation = Auth.get_confirmation_token(new_user.id)
print(confirmation)
new_user.confirmation = confirmation["jti"]
try:
Mailer.send_confirmation_message(confirmation["token"], form_data.email)
except ConnectionRefusedError:
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail="Email couldn't be send. Please try again."
)
return await users.register_user(form_data)
#router.get("/verify/{token}")
async def verify(token: str, users: Users = Depends(get_user_db)):
invalid_token_error = HTTPException(status_code=400, detail="Invalid Token")
try:
payload = jwt.decode(token, get_settings.SECRET_KEY, algorithms=[get_settings.TOKEN_ALGORITHM])
print(payload['sub'])
except jwt.JWSError:
raise HTTPException(status_code=403, detail="Token has Expired")
if payload['scope'] != 'registration':
raise invalid_token_error
print(payload['sub'])
user = await users.get_user_by_id(id=payload['sub'])
print(user)
print('hello2')
if not user or await users.get_confirmation_uuid(str(User.confirmation)) != payload['jti']:
print('hello')
raise invalid_token_error
if user.is_active:
print('hello2')
raise HTTPException(status_code=403, detail="User already Activated")
user.confirmation = None
user.is_active = True
return await users.register_user(user)
the route above outputs the Attribute error:
File ".\db\repository\users_data_access_layer.py", line 26, in register_user
hashed_password=user.password,
AttributeError: 'User' object has no attribute 'password'
user_data_access_layer.py
This is where all db communications are happening. Here I think I need some kind of save method to add to db for convenience but I don't know how to implement it. I tried something like this:
from core.hashing import Hasher
from sqlalchemy.orm import Session
from sqlalchemy.sql.expression import select
from sqlalchemy.sql import exists
from db.models.users import User
from schemas.users import UserCreate
from core.hashing import Hasher
db_session = Session
class Users():
def __init__(self, db_session: Session):
self.db_session = db_session
async def save(self):
if self.id == None:
self.db_session.add(self)
return await self.db_session.flush()
#print('user created')
async def register_user(self, user: UserCreate):
new_user = User(username=user.username,
email=user.email,
hashed_password=user.password,
is_active = False,
is_superuser=False
)
self.db_session.add(new_user)
await self.db_session.flush()
return new_user
async def check_user(self, email: str):
user_exist = await self.db_session.execute(select(User).filter(User.email==email))
#print(user_exist)
return user_exist.scalar_one_or_none()
async def check_username(self, username: str):
user_exist = await self.db_session.execute(select(User).filter(User.username==username))
#print(user_exist)
return user_exist.scalar_one_or_none()
async def get_user_by_id(self, id: int):
user_exist = await self.db_session.execute(select(User).filter(User.id==id)
#print(user_exist)
return user_exist.scalar_one_or_none()
async def get_confirmation_uuid(self, confirmation_uuid:str):
user_exist = await self.db_session.execute(select(User).filter(str(User.confirmation)==confirmation_uuid))
#print(user_exist)
return user_exist
schemas/users.py
from typing import Optional
from pydantic import BaseModel, EmailStr
class UserBase(BaseModel):
username: str
email: EmailStr
password: str
class UserCreate(UserBase):
username: str
email: EmailStr
password: str
class ShowUser(UserBase):
username: str
email: EmailStr
is_active: bool
class Config():
orm_mode = True
models/users.py
import uuid
from sqlalchemy.dialects.postgresql import UUID
from sqlalchemy import Column, Integer, String, Boolean, ForeignKey
from sqlalchemy.orm import relationship
from db.base_class import Base
class User(Base):
id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
username = Column(String, unique=True, nullable=False)
email = Column(String, nullable=False, unique=True, index=True)
hashed_password = Column(String(255), nullable=False)
is_active = Column(Boolean, default=False)
is_superuser = Column(Boolean, default=False)
confirmation = Column(UUID(as_uuid=True), nullable=True, default=uuid.uuid4)
jobs = relationship("Job", back_populates="owner")
depends.py
from db.session import async_session
from db.repository.jobs_data_access_layer import JobBoard
from db.repository.users_data_access_layer import Users
async def get_job_db():
async with async_session() as session:
async with session.begin():
yield JobBoard(session)
async def get_user_db():
async with async_session() as session:
async with session.begin():
yield Users(session)
Since this is all new stuff and wherever I reached I hit a wall and I'm working on this project for weeks now and couldn't find my way around it yet so any assistance would be appreciate.
There are different problems in the code. Firstly some calls to the method of the class model/Users are called with the wrong parameters. Indeed, some are called with a User object as parameter while those are expecting a Pydantic UserCreate model. So, when you send a User object instead of the Pydantic model, the password attribute does not exist.
Secondly, afterwards, other problems will appear, since your methods to retrieve a User object actually return a list (ChunkIterator). However, you make comparisons as if you were receiving an object.
I took the liberty of proposing an alternative by reformulating some of your code.
Now, I have created methods to save eventual modification of users in your DB, and created some methods that return a user according to different criteria (id, username, email) except that contrary to your code, those return an object (or None) instead of a list.
user_data_access_layer.py
from fastapi import HTTPException, status
from db.models.users import User
from schemas.users import UserCreate
from db_config import SESSION
from auth import Auth
class Users():
def __init__(self):
pass
#classmethod
async def save(cls, user_instance):
try:
SESSION.add(user_instance)
SESSION.commit()
except Exception as error:
SESSION.rollback()
raise HTTPException(status_code=status.HTTP_500_INTERNAL_SERVER_ERROR)
#classmethod
async def get_user_by_id(cls, id):
user = await SESSION.query(User).filter(User.id == id).one_or_none()
return user
#classmethod
async def get_user_by_username(cls, username):
user = await SESSION.query(User).filter(User.username == username).one_or_none()
return user
#classmethod
async def get_user_by_email(cls, email):
user = await SESSION.query(User).filter(User.email == email).one_or_none()
return user
#classmethod
async def get_user_by_confirmation(cls, confirmation):
user = await SESSION.query(User).filter(User.confirmation == confirmation).one_or_none()
return user
#classmethod
async def create_user(self, user: UserCreate):
new_user = User(username=user.username,
email=user.email,
hashed_password=Auth.get_password_hash(user.password)
)
cls.save(new_user)
return new_user
As you can see, I removed the Session creation from your layer file and put it in a global variable, in a separate file.
db_config.py
from sqlalchemy import create_engine
from sqlalchemy.engine import Engine
from sqlalchemy.orm import sessionmaker
from sqlalchemy.orm.session import Session
ENGINE: Engine = create_engine(your_config_url, pool_pre_ping=True)
SESSION: Session = sessionmaker(bind=ENGINE)()
And to conclude, here is your endpoint updated according to the proposed code. I have added comments inside it to make it easier to understand.
route.py
#router.post("/", response_model=ShowUser)
async def create_user(form_data: UserCreate = Depends(), users: Users = Depends(get_user_db)):
# CHECK IF USER ALREADY EXISTS
if await Users.get_user_by_email(email=form_data.email) is not None:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="User already exists"
)
# CHECK IF USERNAME ALREADY EXISTS
elif await Users.get_user_by_username(username=form_data.username) is not None:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="Username already exists"
)
# CREATE USER WITH USERS METHOD
# now the hashing of the password is done directly in the creation method
new_user = await Users.create_user(form_data)
# GET TOKEN
# we no longer create a new uid for JTI, but use the one created automatically during user creation
# so I modified the get_confirmation_token function so that it takes the user's JTI uid as a parameter
confirmation_token = Auth.get_confirmation_token(
new_user.id,
new_user.confirmation)
table has not password, table has only hashed_password
new_user = User(username=user.username,
email=user.email,
hashed_password=user.hashed_password,
is_active = False,
is_superuser=False
)
I tried like the solution given at Get Discord user ID from username, but both solutions return None. What am I doing wrong?
async def name_to_user_object(self, ctx, user):
user = user.split("#")
user_id = discord.utils.get(self.bot.get_all_members(), name=user[0], discriminator=user[1]).id
return user_id
Thanks in advance!
You can take use of MemberConverter or UserConverter, simply typehint the user argument to discord.Member or discord.User and library will do the rest for you, here's how:
async def name_to_user_object(self, ctx, user: discord.Member): # or `discord.User`
print(user, type(user)) # it will already be a `discord.Member` or `discord.User` obj
Reference:
MemberConverter
UserConverter
I'm writing tests for a fastAPI on django with an ASGI server (adapted this tutorial). My fastAPI side of the test keeps returning errors and I'm trying in vain to fix it.
My need is about creating a user to test the API.
#sync_to_async
def _create_user(self, username, email):
try:
return User.objects.create(username=username, email=email)
except:
return None
async def setUp(self):
task = asyncio.create_task(self._create_user(username="user", email="email#email.com"))
self.user = await task
Running this test, it turn out that self.user is a coroutine and it's impossible to access the attributes I expect.
How to solve this ?
Update :
Removed async for _create_user(self, username, email).
According to the docs https://docs.djangoproject.com/en/3.1/topics/async/#asgiref.sync.sync_to_async
decorator sync_to_async should decorates sync functions. (see example)
I have an answer,.......................
Simply Create a normal synchronize function and call with with sync_to_async() function
This function is used to get the user_object
def get_user_object(mobile_no):
user_object = CustomUser.objects.get(mobile_no=mobile_no)
return user_object
This method is running in async so we need to call a sync function to async to get rid of error......
from channels.db import database_sync_to_async
async def connect(self):
username = await database_sync_to_async(get_user_object)(mobile_no="+9999999999")
print(user_object)
I want to run a consumer which requires authenticated user in my channels application
channels disconnecting the consumer saying that user isn't authenticated
but the user is authenticated and his cookies are updated in the browser
I have followed channels documentation which authenticates user
class LikeConsumer(AsyncJsonWebsocketConsumer):
async def connect(self):
self.user = self.scope["user"]
# user = self.scope['user']
user=self.user
print(user)
if user.is_anonymous:
await self.close()
print("user is anonymous")
else:
await self.accept()
# user_group = await self._get_user_group(self.scope['user'])
await self.channel_layer.group_add("{}".format(user.id), self.channel_name)
print(f"Add {self.channel_name} channel to post's group")
print('connected')
# #database_sync_to_async
# def _get_user_group(self, user):
# if not user.is_authenticated:
# raise Exception('User is not authenticated.')
# else:
# print("user is not authenticated")
# return user
async def disconnect(self,close_code):
user = self.scope['user']
await self.channel_layer.group_discard("{}".format(user.id), self.channel_name)
print(f"Remove {self.channel_name} channel from post's group")
i'm not sure what exactly the mistake is
user is anonymous
WebSocket DISCONNECT /like/ [127.0.0.1:50710]