Merge pull request #19 from revanced/dev

resolves #13 #14 and #15
This commit is contained in:
Alexandre Teles 2023-01-05 20:22:06 -03:00 committed by GitHub
commit bc05d099cd
10 changed files with 599 additions and 446 deletions

View File

@ -2,17 +2,18 @@ from redis import asyncio as aioredis
import app.utils.Logger as Logger import app.utils.Logger as Logger
from app.dependencies import load_config from app.dependencies import load_config
from app.utils.RedisConnector import RedisConnector from app.utils.RedisConnector import RedisConnector
from app.models.BallotModel import BallotModel
config: dict = load_config() config: dict = load_config()
class Ballot: class Ballot:
"""Implements a ballot for ReVanced Polling API.""" """Implements a ballot for ReVanced Polling API."""
redis = RedisConnector.connect(config['tokens']['database']) redis = RedisConnector.connect(config['ballots']['database'])
BallotLogger = Logger.BallotLogger() BallotLogger = Logger.BallotLogger()
async def store(self, discord_hashed_id: str, ballot: str) -> bool: async def store(self, discord_hashed_id: str, ballot: BallotModel) -> bool:
"""Store a ballot. """Store a ballot.
Args: Args:
@ -26,7 +27,12 @@ class Ballot:
stored: bool = False stored: bool = False
try: try:
await self.redis.set(name=discord_hashed_id, value=ballot, nx=True) await self.redis.json().set(
name=discord_hashed_id,
path=".",
obj=ballot,
nx=True
)
await self.BallotLogger.log("STORE_BALLOT", None, discord_hashed_id) await self.BallotLogger.log("STORE_BALLOT", None, discord_hashed_id)
stored = True stored = True
except aioredis.RedisError as e: except aioredis.RedisError as e:
@ -34,3 +40,24 @@ class Ballot:
raise e raise e
return stored return stored
async def exists(self, discord_hashed_id: str):
"""Check if the ballot exists.
Args:
discord_hashed_id (str): Discord hashed ID of the voter
Returns:
bool: True if the ballot exists, False otherwise
"""
exists: bool = False
try:
if await self.redis.exists(discord_hashed_id):
exists = True
except aioredis.RedisError as e:
await self.BallotLogger.log("BALLOT_EXISTS", e)
raise e
return exists

View File

@ -2,6 +2,7 @@ from redis import asyncio as aioredis
import app.utils.Logger as Logger import app.utils.Logger as Logger
from app.dependencies import load_config from app.dependencies import load_config
from app.utils.RedisConnector import RedisConnector from app.utils.RedisConnector import RedisConnector
import app.controllers.Ballot as Ballot
config: dict = load_config() config: dict = load_config()
@ -10,6 +11,7 @@ class Clients:
"""Implements a client for ReVanced Polling API.""" """Implements a client for ReVanced Polling API."""
redis = RedisConnector.connect(config['tokens']['database']) redis = RedisConnector.connect(config['tokens']['database'])
ballot = Ballot.Ballot()
UserLogger = Logger.UserLogger() UserLogger = Logger.UserLogger()
@ -41,3 +43,46 @@ class Clients:
return banned return banned
async def is_token_banned(self, token: str) -> bool:
"""Check if the token is banned
Args:
token (str): Token to check
Returns:
bool: True if the token is banned, False otherwise
"""
banned: bool = False
try:
if await self.redis.exists(token):
banned = True
except aioredis.RedisError as e:
await self.UserLogger.log("IS_TOKEN_BANNED", e)
raise e
return banned
async def voted(self, token: str, discord_id: str) -> bool:
"""Check if the user already voted
Args:
token (str): Token to check
Returns:
bool: True if the user voted, False otherwise
"""
voted: bool = False
try:
if (await self.is_token_banned(token) or
await self.ballot.exists(discord_id)):
voted = True
except aioredis.RedisError as e:
await self.UserLogger.log("AUTH_CHECKS", e)
raise e
return voted

View File

@ -1,4 +1,3 @@
from collections import deque
from pydantic import BaseModel from pydantic import BaseModel
class BallotFields(BaseModel): class BallotFields(BaseModel):
@ -9,4 +8,4 @@ class BallotFields(BaseModel):
""" """
cid: str cid: str
weight: int vote: bool

View File

@ -1,4 +1,5 @@
from pydantic import BaseModel from pydantic import BaseModel
from app.models.BallotFields import BallotFields
class BallotModel(BaseModel): class BallotModel(BaseModel):
"""Implements the fields for the ballots. """Implements the fields for the ballots.
@ -7,4 +8,4 @@ class BallotModel(BaseModel):
BaseModel (pydantic.BaseModel): BaseModel from pydantic BaseModel (pydantic.BaseModel): BaseModel from pydantic
""" """
vote: str votes: list[BallotFields]

View File

@ -10,35 +10,25 @@ class InternalServerError(BaseModel):
error: str = "Internal Server Error" error: str = "Internal Server Error"
message: str = "An internal server error occurred. Please try again later." message: str = "An internal server error occurred. Please try again later."
class AnnouncementNotFound(BaseModel): class Conflict(BaseModel):
"""Implements the response fields for when an item is not found. """Implements the response fields for when a conflict occurs.
Args: Args:
BaseModel (pydantic.BaseModel): BaseModel from pydantic BaseModel (pydantic.BaseModel): BaseModel from pydantic
""" """
error: str = "Not Found" error: str = "Conflict"
message: str = "No announcement was found." message: str = "User already voted on this ballot."
class ClientNotFound(BaseModel): class PreconditionFailed(BaseModel):
"""Implements the response fields for when a client is not found. """Implements the response fields for when a precondition fails.
Args: Args:
BaseModel (pydantic.BaseModel): BaseModel from pydantic BaseModel (pydantic.BaseModel): BaseModel from pydantic
""" """
error: str = "Not Found" error: str = "Precondition Failed"
message: str = "No client matches the given ID" message: str = "User is not eligible to vote on this ballot."
class IdNotProvided(BaseModel):
"""Implements the response fields for when the id is not provided.
Args:
BaseModel (pydantic.BaseModel): BaseModel from pydantic
"""
error: str = "Bad Request"
message: str = "Missing client id"
class Unauthorized(BaseModel): class Unauthorized(BaseModel):
"""Implements the response fields for when the client is unauthorized. """Implements the response fields for when the client is unauthorized.
@ -50,22 +40,3 @@ class Unauthorized(BaseModel):
error: str = "Unauthorized" error: str = "Unauthorized"
message: str = "The client is unauthorized to access this resource" message: str = "The client is unauthorized to access this resource"
class MirrorNotFoundError(BaseModel):
"""Implements the response fields for when a mirror is not found.
Args:
BaseModel (pydantic.BaseModel): BaseModel from pydantic
"""
error: str = "Not Found"
message: str = "No mirror was found for the organization, repository, and version provided."
class MirrorAlreadyExistsError(BaseModel):
"""Implements the response fields for when a mirror already exists.
Args:
BaseModel (pydantic.BaseModel): BaseModel from pydantic
"""
error: str = "Conflict"
message: str = "A mirror already exists for the organization, repository, and version provided. Please use the PUT method to update the mirror."

View File

@ -1,8 +1,10 @@
import os import os
import hmac
from fastapi_paseto_auth import AuthPASETO from fastapi_paseto_auth import AuthPASETO
from fastapi import APIRouter, Request, Response, Depends, status, HTTPException, Header from fastapi import APIRouter, Request, Response, Depends, status, HTTPException, Header
from app.dependencies import load_config from app.dependencies import load_config
from app.controllers.Clients import Clients from app.controllers.Clients import Clients
from app.controllers.Ballot import Ballot
import app.models.ClientModels as ClientModels import app.models.ClientModels as ClientModels
import app.models.GeneralErrors as GeneralErrors import app.models.GeneralErrors as GeneralErrors
import app.models.ResponseModels as ResponseModels import app.models.ResponseModels as ResponseModels
@ -12,6 +14,7 @@ router = APIRouter(
tags=['Authentication'] tags=['Authentication']
) )
clients = Clients() clients = Clients()
ballot = Ballot()
config: dict = load_config() config: dict = load_config()
@router.post('/', response_model=ResponseModels.ClientAuthTokenResponse, status_code=status.HTTP_200_OK) @router.post('/', response_model=ResponseModels.ClientAuthTokenResponse, status_code=status.HTTP_200_OK)
@ -22,7 +25,11 @@ async def auth(request: Request, response: Response, client: ClientModels.Client
access_token: auth token access_token: auth token
""" """
if client.id == os.environ['CLIENT_ID'] and client.secret == os.environ['CLIENT_SECRET']: if(
hmac.compare_digest(client.id, os.environ['CLIENT_ID']) and
hmac.compare_digest(client.secret, os.environ['CLIENT_SECRET'])
):
authenticated: bool = True authenticated: bool = True
if not authenticated: if not authenticated:
@ -32,13 +39,19 @@ async def auth(request: Request, response: Response, client: ClientModels.Client
} }
) )
else: else:
if not ballot.exists(client.discord_id_hash):
user_claims: dict[str, str] = {} user_claims: dict[str, str] = {}
user_claims['discord_id_hash'] = client.discord_id_hash user_claims['discord_id_hash'] = client.discord_id_hash
access_token = Authorize.create_access_token(subject=client.id, access_token = Authorize.create_access_token(subject=client.id,
user_claims=user_claims, user_claims=user_claims,
fresh=True) fresh=True)
return {"access_token": access_token} return {"access_token": access_token}
else:
raise HTTPException(status_code=412, detail={
"error": GeneralErrors.PreconditionFailed().error,
"message": GeneralErrors.PreconditionFailed().message
}
)
else: else:
raise HTTPException(status_code=401, detail={ raise HTTPException(status_code=401, detail={
"error": GeneralErrors.Unauthorized().error, "error": GeneralErrors.Unauthorized().error,
@ -46,6 +59,38 @@ async def auth(request: Request, response: Response, client: ClientModels.Client
} }
) )
@router.put("/exchange", response_model=ResponseModels.ClientAuthTokenResponse, status_code=status.HTTP_200_OK)
async def exchange_token(request: Request, response: Response, Authorize: AuthPASETO = Depends(), Authorization: str = Header(None)) -> dict:
"""Exchange a token for a new one.
Returns:
access_token: auth token
"""
Authorize.paseto_required()
user_claims: dict[str, str | bool] = {}
user_claims['discord_id_hash'] = Authorize.get_user_claims()['discord_id_hash']
user_claims['is_exchange_token'] = True
access_token = Authorize.create_access_token(subject=Authorize.get_subject(),
user_claims=user_claims,
fresh=True)
if not ballot.exists(Authorize.get_subject()):
if await clients.ban_token(Authorize.get_jti()):
return {"access_token": access_token}
else:
raise HTTPException(status_code=500, detail={
"error": GeneralErrors.InternalServerError().error,
"message": GeneralErrors.InternalServerError().message
}
)
else:
raise HTTPException(status_code=412, detail={
"error": GeneralErrors.PreconditionFailed().error,
"message": GeneralErrors.PreconditionFailed().message
}
)
@router.delete("/revoke", response_model=ResponseModels.RevokedTokenResponse, status_code=status.HTTP_200_OK) @router.delete("/revoke", response_model=ResponseModels.RevokedTokenResponse, status_code=status.HTTP_200_OK)
async def revoke_token(request: Request, response: Response, Authorize: AuthPASETO = Depends(), Authorization: str = Header(None)) -> dict: async def revoke_token(request: Request, response: Response, Authorize: AuthPASETO = Depends(), Authorization: str = Header(None)) -> dict:
"""Revoke a token. """Revoke a token.
@ -64,3 +109,4 @@ async def revoke_token(request: Request, response: Response, Authorize: AuthPASE
"message": GeneralErrors.InternalServerError().message "message": GeneralErrors.InternalServerError().message
} }
) )

View File

@ -5,11 +5,14 @@ from app.models.BallotModel import BallotModel
import app.models.GeneralErrors as GeneralErrors import app.models.GeneralErrors as GeneralErrors
import app.models.ResponseModels as ResponseModels import app.models.ResponseModels as ResponseModels
import app.controllers.Ballot as Ballot import app.controllers.Ballot as Ballot
import app.controllers.Clients as Clients
router = APIRouter() router = APIRouter()
ballot_controller = Ballot.Ballot() ballot_controller = Ballot.Ballot()
client = Clients.Clients()
config: dict = load_config() config: dict = load_config()
@router.post('/ballot', response_model=ResponseModels.BallotCastedResponse, @router.post('/ballot', response_model=ResponseModels.BallotCastedResponse,
@ -24,11 +27,20 @@ async def cast_ballot(request: Request, response: Response,
""" """
Authorize.paseto_required() Authorize.paseto_required()
discord_hashed_id: str = Authorize.get_paseto_claims()['discord_hashed_id']
stored: bool = await ballot_controller.store(discord_hashed_id, ballot.vote) if (Authorize.get_paseto_claims()['is_exchange_token'] and
not client.voted(
Authorize.get_jti(),
Authorize.get_paseto_claims()['discord_hashed_id']
)):
stored: bool = await ballot_controller.store(
Authorize.get_paseto_claims()['discord_hashed_id'],
ballot
)
if stored: if stored:
await client.ban_token(Authorize.get_jti())
return {"created": stored} return {"created": stored}
else: else:
raise HTTPException(status_code=500, detail={ raise HTTPException(status_code=500, detail={
@ -36,3 +48,9 @@ async def cast_ballot(request: Request, response: Response,
"message": GeneralErrors.InternalServerError().message "message": GeneralErrors.InternalServerError().message
} }
) )
else:
raise HTTPException(status_code=401, detail={
"error": GeneralErrors.Unauthorized().error,
"message": GeneralErrors.Unauthorized().message
}
)

775
poetry.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -28,8 +28,8 @@ uvicorn = ">=0.18.3"
gunicorn = ">=20.1.0" gunicorn = ">=20.1.0"
[tool.poetry.dev-dependencies] [tool.poetry.dev-dependencies]
mypy = ">=0.971" mypy = ">=0.991"
types-toml = ">=0.10.8" types-toml = ">=0.10.8.1"
types-redis = ">=4.3.21.1" types-redis = ">=4.3.21.1"
[build-system] [build-system]

View File

@ -4,60 +4,59 @@ anyio==3.6.2 ; python_version >= "3.10" and python_version < "4.0"
argon2-cffi-bindings==21.2.0 ; python_version >= "3.10" and python_version < "4.0" argon2-cffi-bindings==21.2.0 ; python_version >= "3.10" and python_version < "4.0"
argon2-cffi==21.3.0 ; python_version >= "3.10" and python_version < "4.0" argon2-cffi==21.3.0 ; python_version >= "3.10" and python_version < "4.0"
async-timeout==4.0.2 ; python_version >= "3.10" and python_version < "4.0" async-timeout==4.0.2 ; python_version >= "3.10" and python_version < "4.0"
attrs==21.4.0 ; python_version >= "3.10" and python_version < "4.0" attrs==22.2.0 ; python_version >= "3.10" and python_version < "4.0"
certifi==2022.9.24 ; python_version >= "3.10" and python_version < "4.0" certifi==2022.12.7 ; python_version >= "3.10" and python_version < "4.0"
cffi==1.15.1 ; python_version >= "3.10" and python_version < "4.0" cffi==1.15.1 ; python_version >= "3.10" and python_version < "4.0"
click==8.1.3 ; python_version >= "3.10" and python_version < "4.0" click==8.1.3 ; python_version >= "3.10" and python_version < "4.0"
colorama==0.4.6 ; python_version >= "3.10" and python_version < "4.0" and sys_platform == "win32" or python_version >= "3.10" and python_version < "4.0" and platform_system == "Windows" colorama==0.4.6 ; python_version >= "3.10" and python_version < "4.0" and sys_platform == "win32" or python_version >= "3.10" and python_version < "4.0" and platform_system == "Windows"
cryptography==37.0.4 ; python_version >= "3.10" and python_version < "4.0" cryptography==37.0.4 ; python_version >= "3.10" and python_version < "4.0"
cytoolz==0.12.0 ; python_version >= "3.10" and python_version < "4.0" cytoolz==0.12.1 ; python_version >= "3.10" and python_version < "4.0"
deprecated==1.2.13 ; python_version >= "3.10" and python_version < "4.0" deprecated==1.2.13 ; python_version >= "3.10" and python_version < "4.0"
fastapi-cache2==0.1.9 ; python_version >= "3.10" and python_version < "4.0" fastapi-cache2==0.1.9 ; python_version >= "3.10" and python_version < "4.0"
fastapi-paseto-auth==0.6.0 ; python_version >= "3.10" and python_version < "4.0" fastapi-paseto-auth==0.6.0 ; python_version >= "3.10" and python_version < "4.0"
fastapi==0.85.0 ; python_version >= "3.10" and python_version < "4.0" fastapi==0.85.0 ; python_version >= "3.10" and python_version < "4.0"
fasteners==0.17.3 ; python_version >= "3.10" and python_version < "4.0" fasteners==0.17.3 ; python_version >= "3.10" and python_version < "4.0"
gunicorn==20.1.0 ; python_version >= "3.10" and python_version < "4.0" gunicorn==20.1.0 ; python_version >= "3.10" and python_version < "4.0"
h11==0.12.0 ; python_version >= "3.10" and python_version < "4.0" h11==0.14.0 ; python_version >= "3.10" and python_version < "4.0"
h2==4.1.0 ; python_version >= "3.10" and python_version < "4.0" h2==4.1.0 ; python_version >= "3.10" and python_version < "4.0"
hiredis==2.0.0 ; python_version >= "3.10" and python_version < "4.0" hiredis==2.1.0 ; python_version >= "3.10" and python_version < "4.0"
hpack==4.0.0 ; python_version >= "3.10" and python_version < "4.0" hpack==4.0.0 ; python_version >= "3.10" and python_version < "4.0"
httpcore==0.15.0 ; python_version >= "3.10" and python_version < "4.0" httpcore==0.16.3 ; python_version >= "3.10" and python_version < "4.0"
httpx-cache==0.6.1 ; python_version >= "3.10" and python_version < "4.0" httpx-cache==0.7.0 ; python_version >= "3.10" and python_version < "4.0"
httpx==0.23.0 ; python_version >= "3.10" and python_version < "4.0" httpx==0.23.3 ; python_version >= "3.10" and python_version < "4.0"
httpx[http2]==0.23.0 ; python_version >= "3.10" and python_version < "4.0" httpx[http2]==0.23.3 ; python_version >= "3.10" and python_version < "4.0"
hypercorn[uvloop]==0.14.3 ; python_version >= "3.10" and python_version < "4.0" hypercorn[uvloop]==0.14.3 ; python_version >= "3.10" and python_version < "4.0"
hyperframe==6.0.1 ; python_version >= "3.10" and python_version < "4.0" hyperframe==6.0.1 ; python_version >= "3.10" and python_version < "4.0"
idna==3.4 ; python_version >= "3.10" and python_version < "4.0" idna==3.4 ; python_version >= "3.10" and python_version < "4.0"
iso8601==1.1.0 ; python_version >= "3.10" and python_version < "4.0" iso8601==1.1.0 ; python_version >= "3.10" and python_version < "4.0"
limits==1.6 ; python_version >= "3.10" and python_version < "4.0" limits==2.8.0 ; python_version >= "3.10" and python_version < "4.0"
loguru==0.6.0 ; python_version >= "3.10" and python_version < "4.0" loguru==0.6.0 ; python_version >= "3.10" and python_version < "4.0"
msgpack==1.0.4 ; python_version >= "3.10" and python_version < "4.0" msgpack==1.0.4 ; python_version >= "3.10" and python_version < "4.0"
orjson==3.8.1 ; python_version >= "3.10" and python_version < "4.0" orjson==3.8.4 ; python_version >= "3.10" and python_version < "4.0"
packaging==21.3 ; python_version >= "3.10" and python_version < "4.0" packaging==22.0 ; python_version >= "3.10" and python_version < "4.0"
passlib[argon2]==1.7.4 ; python_version >= "3.10" and python_version < "4.0" passlib[argon2]==1.7.4 ; python_version >= "3.10" and python_version < "4.0"
pendulum==2.1.2 ; python_version >= "3.10" and python_version < "4.0" pendulum==2.1.2 ; python_version >= "3.10" and python_version < "4.0"
priority==2.0.0 ; python_version >= "3.10" and python_version < "4.0" priority==2.0.0 ; python_version >= "3.10" and python_version < "4.0"
pycparser==2.21 ; python_version >= "3.10" and python_version < "4.0" pycparser==2.21 ; python_version >= "3.10" and python_version < "4.0"
pycryptodomex==3.15.0 ; python_version >= "3.10" and python_version < "4.0" pycryptodomex==3.16.0 ; python_version >= "3.10" and python_version < "4.0"
pydantic==1.10.2 ; python_version >= "3.10" and python_version < "4.0" pydantic==1.10.2 ; python_version >= "3.10" and python_version < "4.0"
pyparsing==3.0.9 ; python_version >= "3.10" and python_version < "4.0"
pyseto==1.6.10 ; python_version >= "3.10" and python_version < "4.0" pyseto==1.6.10 ; python_version >= "3.10" and python_version < "4.0"
python-dateutil==2.8.2 ; python_version >= "3.10" and python_version < "4.0" python-dateutil==2.8.2 ; python_version >= "3.10" and python_version < "4.0"
pytzdata==2020.1 ; python_version >= "3.10" and python_version < "4.0" pytzdata==2020.1 ; python_version >= "3.10" and python_version < "4.0"
redis==4.3.4 ; python_version >= "3.10" and python_version < "4.0" redis==4.4.0 ; python_version >= "3.10" and python_version < "4.0"
rfc3986[idna2008]==1.5.0 ; python_version >= "3.10" and python_version < "4.0" rfc3986[idna2008]==1.5.0 ; python_version >= "3.10" and python_version < "4.0"
sentry-sdk==1.11.0 ; python_version >= "3.10" and python_version < "4.0" sentry-sdk==1.12.1 ; python_version >= "3.10" and python_version < "4.0"
setuptools==65.5.1 ; python_version >= "3.10" and python_version < "4.0" setuptools==65.6.3 ; python_version >= "3.10" and python_version < "4.0"
six==1.16.0 ; python_version >= "3.10" and python_version < "4.0" six==1.16.0 ; python_version >= "3.10" and python_version < "4.0"
slowapi==0.1.6 ; python_version >= "3.10" and python_version < "4.0" slowapi==0.1.7 ; python_version >= "3.10" and python_version < "4.0"
sniffio==1.3.0 ; python_version >= "3.10" and python_version < "4.0" sniffio==1.3.0 ; python_version >= "3.10" and python_version < "4.0"
starlette==0.20.4 ; python_version >= "3.10" and python_version < "4.0" starlette==0.20.4 ; python_version >= "3.10" and python_version < "4.0"
toml==0.10.2 ; python_version >= "3.10" and python_version < "4.0" toml==0.10.2 ; python_version >= "3.10" and python_version < "4.0"
toolz==0.12.0 ; python_version >= "3.10" and python_version < "4.0" toolz==0.12.0 ; python_version >= "3.10" and python_version < "4.0"
typing-extensions==4.4.0 ; python_version >= "3.10" and python_version < "4.0" typing-extensions==4.4.0 ; python_version >= "3.10" and python_version < "4.0"
ujson==5.5.0 ; python_version >= "3.10" and python_version < "4.0" ujson==5.6.0 ; python_version >= "3.10" and python_version < "4.0"
urllib3==1.26.12 ; python_version >= "3.10" and python_version < "4" urllib3==1.26.13 ; python_version >= "3.10" and python_version < "4.0"
uvicorn==0.19.0 ; python_version >= "3.10" and python_version < "4.0" uvicorn==0.20.0 ; python_version >= "3.10" and python_version < "4.0"
uvloop==0.17.0 ; platform_system != "Windows" and python_version >= "3.10" and python_version < "4.0" uvloop==0.17.0 ; platform_system != "Windows" and python_version >= "3.10" and python_version < "4.0"
win32-setctime==1.1.0 ; python_version >= "3.10" and python_version < "4.0" and sys_platform == "win32" win32-setctime==1.1.0 ; python_version >= "3.10" and python_version < "4.0" and sys_platform == "win32"
wrapt==1.14.1 ; python_version >= "3.10" and python_version < "4.0" wrapt==1.14.1 ; python_version >= "3.10" and python_version < "4.0"