Skip to content

Commit

Permalink
Add generate API-key API
Browse files Browse the repository at this point in the history
  • Loading branch information
JonasKs committed Apr 26, 2022
1 parent bda61ca commit 2e96dda
Show file tree
Hide file tree
Showing 8 changed files with 101 additions and 8 deletions.
4 changes: 2 additions & 2 deletions app/api/api_v2/api.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
from fastapi import APIRouter

from app.api.api_v2.endpoints import like, tags, user_thumbnail, users
from app.api.api_v2.endpoints import like, tags, user_update, users
from app.api.api_v2.endpoints.video import delete, list_videos, patch_video, upload

api_router = APIRouter()
Expand All @@ -9,6 +9,6 @@
api_router.include_router(delete.router, tags=['video'])
api_router.include_router(patch_video.router, tags=['video'])
api_router.include_router(tags.router, tags=['tags'])
api_router.include_router(user_thumbnail.router, tags=['user'])
api_router.include_router(user_update.router, tags=['user'])
api_router.include_router(users.router, tags=['user'])
api_router.include_router(like.router, tags=['video'])
Original file line number Diff line number Diff line change
Expand Up @@ -9,16 +9,17 @@
from fastapi import APIRouter, Depends, File, HTTPException, UploadFile, status
from sqlmodel.ext.asyncio.session import AsyncSession

from api.security import cognito_signed_in, generate_api_key_internals, get_fernet
from app.api.dependencies import get_boto, yield_db_session
from app.api.security import cognito_signed_in
from app.api.services import await_ffmpeg, generate_user_thumbnail
from app.core.config import settings
from app.models.klepp import User, UserRead
from app.models.klepp import ListResponse, User, UserRead
from models.klepp import UserReadAPIKey

router = APIRouter()


@router.put('/user', response_model=UserRead, status_code=status.HTTP_201_CREATED)
@router.put('/me', response_model=UserRead, status_code=status.HTTP_201_CREATED)
async def user_thumbnail(
file: UploadFile = File(..., description='File to upload'),
user: User = Depends(cognito_signed_in),
Expand Down Expand Up @@ -70,3 +71,21 @@ async def user_thumbnail(
await db_session.commit()
await db_session.refresh(user)
return user.dict()


@router.post('/me/generate-api-key', response_model=ListResponse[UserReadAPIKey], dependencies=[])
async def api_key(
db_session: AsyncSession = Depends(yield_db_session),
user: User = Depends(cognito_signed_in),
) -> User:
"""
Get a list of users
"""
api_key_and_salt = generate_api_key_internals()
user.api_key = get_fernet(api_key_and_salt.salt).encrypt(api_key_and_salt.api_key)
user.salt = api_key_and_salt.salt
db_session.add(user)
await db_session.commit()
await db_session.refresh(user)
user.api_key = api_key_and_salt.api_key
return user
6 changes: 3 additions & 3 deletions app/api/api_v2/endpoints/users.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@

@router.get('/users', response_model=ListResponse[UserRead], dependencies=[Depends(cognito_scheme_or_anonymous)])
async def get_users(
session: AsyncSession = Depends(yield_db_session),
db_session: AsyncSession = Depends(yield_db_session),
offset: int = 0,
limit: int = Query(default=100, lte=100),
) -> dict[str, int | list]:
Expand All @@ -30,8 +30,8 @@ async def get_users(
tag_statement = tag_statement.offset(offset=offset).limit(limit=limit)
# Do DB requests async
tasks = [
asyncio.create_task(session.exec(tag_statement)), # type: ignore
asyncio.create_task(session.exec(count_statement)),
asyncio.create_task(db_session.exec(tag_statement)), # type: ignore
asyncio.create_task(db_session.exec(count_statement)),
]
results, count = await asyncio.gather(*tasks)
count_number = count.one_or_none()
Expand Down
31 changes: 31 additions & 0 deletions app/api/security.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,16 @@
security. If you're using this library as inspiration for anything, please keep that in mind.
"""

import base64
import logging
import secrets
from datetime import datetime, timedelta
from typing import Any

from cryptography.fernet import Fernet
from cryptography.hazmat.backends import default_backend
from cryptography.hazmat.primitives import hashes
from cryptography.hazmat.primitives.kdf.hkdf import HKDF
from fastapi import Depends, HTTPException, status
from fastapi.security import OAuth2AuthorizationCodeBearer, SecurityScopes
from fastapi.security.base import SecurityBase
Expand All @@ -23,6 +29,9 @@
from app.core.config import settings
from app.models.klepp import User
from app.schemas.schemas_v1.user import User as CognitoUser
from schemas.schemas_v2.api_key import APIKeyAndSalt

backend = default_backend()


class InvalidAuth(HTTPException):
Expand Down Expand Up @@ -224,3 +233,25 @@ async def cognito_signed_in(
await db_session.refresh(new_user)
return new_user
return user # type: ignore


def get_fernet(salt: bytes) -> Fernet:
"""
Returns a fernet that can be used for encrypting and decrypting a byte string.
"""
hkdf = HKDF(
algorithm=hashes.SHA256(),
length=32,
salt=salt,
info=b'klepp',
backend=backend,
)
fernet_key = hkdf.derive(settings.SECRET_KEY.encode())
return Fernet(base64.urlsafe_b64encode(fernet_key))


def generate_api_key_internals() -> APIKeyAndSalt:
"""
Generate a new API key and salt
"""
return APIKeyAndSalt(api_key=secrets.token_urlsafe(32).encode(), salt=secrets.token_urlsafe(32).encode())
6 changes: 6 additions & 0 deletions app/models/klepp.py
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,12 @@ class User(UserBase, table=True):
id: Optional[uuid.UUID] = Field(default_factory=uuid.uuid4, primary_key=True, index=True, nullable=False)
videos: List['Video'] = Relationship(back_populates='user')
liked_videos: List['Video'] = Relationship(back_populates='likes', link_model=VideoLikeLink)
api_key: Optional[bytes] = Field(default=None, description='User API key, encrypted')
salt: Optional[bytes] = Field(default=None, description='The salt used to encrypt the API key with')


class UserReadAPIKey(UserBase):
api_key: str = Field(description='API Key, non encrypted. Only visible once')


class UserRead(UserBase):
Expand Down
Empty file.
6 changes: 6 additions & 0 deletions app/schemas/schemas_v2/api_key.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
from pydantic import BaseModel


class APIKeyAndSalt(BaseModel):
api_key: bytes
salt: bytes
31 changes: 31 additions & 0 deletions migrations/versions/bb761dee7bcd_api_key.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
"""api-key
Revision ID: bb761dee7bcd
Revises: 39c1fdf365c0
Create Date: 2022-04-26 22:28:19.688767
"""
from alembic import op
import sqlalchemy as sa
import sqlmodel


# revision identifiers, used by Alembic.
revision = 'bb761dee7bcd'
down_revision = '39c1fdf365c0'
branch_labels = None
depends_on = None


def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.add_column('user', sa.Column('api_key', sa.LargeBinary(), nullable=True))
op.add_column('user', sa.Column('salt', sa.LargeBinary(), nullable=True))
# ### end Alembic commands ###


def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.drop_column('user', 'salt')
op.drop_column('user', 'api_key')
# ### end Alembic commands ###

0 comments on commit 2e96dda

Please sign in to comment.