Skip to content

Commit

Permalink
Merge pull request #37 from Myzel394/add-api-scopes
Browse files Browse the repository at this point in the history
Add api scopes
  • Loading branch information
Myzel394 authored Mar 11, 2023
2 parents b97a795 + 70f3dbf commit 22d9728
Show file tree
Hide file tree
Showing 10 changed files with 136 additions and 53 deletions.
Original file line number Diff line number Diff line change
@@ -1,16 +1,16 @@
"""empty message
Revision ID: 298d6a4e0d71
Revision ID: 5a772f2b370d
Revises: 7e9ad1bb809e
Create Date: 2023-03-09 22:08:08.677664
Create Date: 2023-03-11 14:47:34.318260
"""
from alembic import op
import sqlalchemy as sa
from sqlalchemy.dialects import postgresql

# revision identifiers, used by Alembic.
revision = '298d6a4e0d71'
revision = '5a772f2b370d'
down_revision = '7e9ad1bb809e'
branch_labels = None
depends_on = None
Expand All @@ -21,9 +21,10 @@ def upgrade() -> None:
op.create_table('api_key',
sa.Column('id', postgresql.UUID(as_uuid=True), nullable=False),
sa.Column('user_id', postgresql.UUID(as_uuid=True), nullable=True),
sa.Column('expires_at', sa.DateTime(), nullable=False),
sa.Column('label', sa.String(length=80), nullable=False),
sa.Column('expires_at', sa.Date(), nullable=False),
sa.Column('hashed_key', sa.String(length=97), nullable=False),
sa.Column('scopes', sa.ARRAY(sa.Enum('PROFILE_BASIC', 'FULL_PROFILE', 'PREFERENCES_READ', 'PREFERENCES_UPDATE', 'ALIAS_READ', 'ALIAS_CREATE', 'ALIAS_UPDATE', 'ALIAS_DELETE', 'REPORT_READ', 'REPORT_DELETE', name='apikeyscope')), nullable=False),
sa.Column('scopes', sa.ARRAY(sa.Enum('PROFILE_READ', 'PROFILE_UPDATE', 'PREFERENCES_READ', 'PREFERENCES_UPDATE', 'ALIAS_READ', 'ALIAS_CREATE', 'ALIAS_UPDATE', 'ALIAS_DELETE', 'REPORT_READ', 'REPORT_DELETE', 'ADMIN_CRON_REPORT_READ', 'ADMIN_SETTINGS_READ', 'ADMIN_SETTINGS_UPDATE', 'ADMIN_RESERVED_ALIAS_READ', 'ADMIN_RESERVED_ALIAS_CREATE', 'ADMIN_RESERVED_ALIAS_UPDATE', 'ADMIN_RESERVED_ALIAS_DELETE', name='apikeyscope')), nullable=False),
sa.ForeignKeyConstraint(['user_id'], ['user.id'], ),
sa.PrimaryKeyConstraint('id')
)
Expand Down
28 changes: 0 additions & 28 deletions alembic/versions/e62a5cade75a_.py

This file was deleted.

1 change: 1 addition & 0 deletions app/dependencies/auth.py
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,7 @@ async def _get_credentials():
return AuthResult(
user=api_key.user,
method=AuthResultMethod.API_KEY,
api_key=api_key,
)

raise HTTPException(
Expand Down
13 changes: 11 additions & 2 deletions app/models/enums/api_key.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,8 @@


class APIKeyScope(str, enum.Enum):
PROFILE_BASIC = "basic_profile"
FULL_PROFILE = "full_profile"
PROFILE_READ = "read:profile"
PROFILE_UPDATE = "update:profile"

PREFERENCES_READ = "read:preferences"
PREFERENCES_UPDATE = "update:preferences"
Expand All @@ -19,3 +19,12 @@ class APIKeyScope(str, enum.Enum):

REPORT_READ = "read:report"
REPORT_DELETE = "delete:report"

ADMIN_CRON_REPORT_READ = "read:admin_cron_report"
ADMIN_SETTINGS_READ = "read:admin_settings"
ADMIN_SETTINGS_UPDATE = "update:admin_settings"

ADMIN_RESERVED_ALIAS_READ = "read:admin_reserved_alias"
ADMIN_RESERVED_ALIAS_CREATE = "create:admin_reserved_alias"
ADMIN_RESERVED_ALIAS_UPDATE = "update:admin_reserved_alias"
ADMIN_RESERVED_ALIAS_DELETE = "delete:admin_reserved_alias"
20 changes: 14 additions & 6 deletions app/routes/account.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
from typing import Union

from fastapi import APIRouter, Depends
from sqlalchemy.orm import Session

from app import logger
from app.controllers.account import update_account_data
from app.database.dependencies import get_db
from app.dependencies.auth import AuthResult, get_auth
from app.dependencies.auth import AuthResult, AuthResultMethod, get_auth
from app.models.enums.api_key import APIKeyScope
from app.schemas.user import UserDetail, UserUpdate
from app.schemas.user import UserDetail, UserDetailWithoutPreferences, UserUpdate

router = APIRouter()

Expand All @@ -18,7 +20,10 @@
def update_account_data_api(
user_data: UserUpdate,
db: Session = Depends(get_db),
auth: AuthResult = Depends(get_auth()),
auth: AuthResult = Depends(get_auth(
allow_api=True,
api_key_scope=APIKeyScope.PROFILE_UPDATE,
)),
):
logger.info(f"Request: Update Account Data -> Update user={auth.user} with {user_data=}.")
update_account_data(
Expand All @@ -33,12 +38,15 @@ def update_account_data_api(

@router.get(
"/me",
response_model=UserDetail
response_model=Union[UserDetail, UserDetailWithoutPreferences]
)
def get_me(
auth: AuthResult = Depends(get_auth(
allow_api=True,
api_key_scope=APIKeyScope.FULL_PROFILE,
api_key_scope=APIKeyScope.PROFILE_READ,
)),
):
return auth.user
if auth.method == AuthResultMethod.JWT or APIKeyScope.PREFERENCES_READ in auth.api_key.scopes:
return UserDetail.from_orm(auth.user)
else:
return UserDetailWithoutPreferences.from_orm(auth.user)
25 changes: 21 additions & 4 deletions app/routes/admin.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
from app.controllers.global_settings import get_settings, update_settings
from app.database.dependencies import get_db
from app.dependencies.auth import AuthResult, get_auth
from app.models.enums.api_key import APIKeyScope
from app.schemas.admin import (
AdminGlobalSettingsDisabledResponseModel,
AdminUpdateGlobalSettingsModel, AdminUsersResponseModel,
Expand All @@ -24,7 +25,11 @@
)
def get_admin_users_api(
db: Session = Depends(get_db),
_: AuthResult = Depends(get_auth(require_admin=True))
_: AuthResult = Depends(get_auth(
require_admin=True,
allow_api=True,
api_key_scope=APIKeyScope.ADMIN_SETTINGS_UPDATE,
))
):
logger.info("Request: Get Admins -> New Request.")

Expand All @@ -45,7 +50,11 @@ def get_admin_users_api(
)
def get_settings_api(
db: Session = Depends(get_db),
_: AuthResult = Depends(get_auth(require_admin=True))
_: AuthResult = Depends(get_auth(
require_admin=True,
allow_api=True,
api_key_scope=APIKeyScope.ADMIN_SETTINGS_READ,
))
):
logger.info("Request: Get Admin Settings -> New Request.")

Expand Down Expand Up @@ -76,7 +85,11 @@ def get_settings_api(
def update_settings_api(
update_data: AdminUpdateGlobalSettingsModel,
db: Session = Depends(get_db),
_: AuthResult = Depends(get_auth(require_admin=True))
_: AuthResult = Depends(get_auth(
require_admin=True,
allow_api=True,
api_key_scope=APIKeyScope.ADMIN_SETTINGS_UPDATE,
))
):
logger.info("Request: Update Admin Settings -> New Request.")

Expand All @@ -101,7 +114,11 @@ def update_settings_api(
@router.get("/cron-report/latest/", response_model=CronReportResponseModel)
def get_cron_jobs(
db: Session = Depends(get_db),
auth: AuthResult = Depends(get_auth(require_admin=True))
_: AuthResult = Depends(get_auth(
require_admin=True,
allow_api=True,
api_key_scope=APIKeyScope.ADMIN_CRON_REPORT_READ,
))
):
logger.info("Request: Get Cron Jobs -> New Request.")

Expand Down
31 changes: 26 additions & 5 deletions app/routes/reserved_alias.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
from app.database.dependencies import get_db
from app.dependencies.auth import AuthResult, get_auth
from app.life_constants import MAIL_DOMAIN
from app.models.enums.api_key import APIKeyScope
from app.schemas._basic import SimpleDetailResponseModel
from app.schemas.reserved_alias import ReservedAliasCreate, ReservedAliasDetail, ReservedAliasUpdate
from app.controllers.reserved_alias import (
Expand All @@ -25,7 +26,11 @@
response_model=Page[ReservedAliasDetail]
)
def get_reserved_aliases_api(
_: AuthResult = Depends(get_auth(require_admin=True)),
_: AuthResult = Depends(get_auth(
require_admin=True,
allow_api=True,
api_key_scope=APIKeyScope.ADMIN_RESERVED_ALIAS_READ,
)),
db: Session = Depends(get_db),
params: Params = Depends(),
query: str = Query(""),
Expand All @@ -52,7 +57,11 @@ def get_reserved_aliases_api(
)
def get_reserved_alias_api(
id: uuid.UUID,
_: AuthResult = Depends(get_auth(require_admin=True)),
_: AuthResult = Depends(get_auth(
require_admin=True,
allow_api=True,
api_key_scope=APIKeyScope.ADMIN_RESERVED_ALIAS_READ,
)),
db: Session = Depends(get_db),
):
logger.info("Request: Get Reserved Alias -> New Request.")
Expand All @@ -75,7 +84,11 @@ def get_reserved_alias_api(
)
def create_reserved_alias_api(
alias_data: ReservedAliasCreate,
_: AuthResult = Depends(get_auth(require_admin=True)),
_: AuthResult = Depends(get_auth(
require_admin=True,
allow_api=True,
api_key_scope=APIKeyScope.ADMIN_RESERVED_ALIAS_CREATE,
)),
db: Session = Depends(get_db),
):
logger.info("Request: Create Reserved Alias -> New Request.")
Expand Down Expand Up @@ -108,7 +121,11 @@ def create_reserved_alias_api(
def update_reserved_alias_api(
id: uuid.UUID,
alias_data: ReservedAliasUpdate,
_: AuthResult = Depends(get_auth(require_admin=True)),
_: AuthResult = Depends(get_auth(
require_admin=True,
allow_api=True,
api_key_scope=APIKeyScope.ADMIN_RESERVED_ALIAS_UPDATE,
)),
db: Session = Depends(get_db),
):
logger.info("Request: Update Reserved Alias -> New Request.")
Expand Down Expand Up @@ -138,7 +155,11 @@ def update_reserved_alias_api(
)
def delete_reserved_alias_api(
id: uuid.UUID,
_: AuthResult = Depends(get_auth(require_admin=True)),
_: AuthResult = Depends(get_auth(
require_admin=True,
allow_api=True,
api_key_scope=APIKeyScope.ADMIN_RESERVED_ALIAS_DELETE,
)),
db: Session = Depends(get_db),
):
logger.info("Request: Delete Reserved Alias -> New Request.")
Expand Down
6 changes: 5 additions & 1 deletion app/routes/user_preference.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
from app.controllers.user_preferences import update_user_preferences
from app.database.dependencies import get_db
from app.dependencies.auth import AuthResult, get_auth
from app.models.enums.api_key import APIKeyScope
from app.schemas.user_preferences import UserPreferencesUpdate

router = APIRouter()
Expand All @@ -15,7 +16,10 @@
)
def update_user_preferences_api(
update: UserPreferencesUpdate,
auth: AuthResult = Depends(get_auth()),
auth: AuthResult = Depends(get_auth(
allow_api=True,
api_key_scope=APIKeyScope.PREFERENCES_UPDATE
)),
db: Session = Depends(get_db),
):
update_user_preferences(
Expand Down
13 changes: 11 additions & 2 deletions app/schemas/user.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
"UserCreate",
"UserUpdate",
"UserDetail",
"UserDetailWithoutPreferences",
"UserPreferences",
]

Expand Down Expand Up @@ -100,13 +101,21 @@ class Config:
orm_mode = True


class UserDetail(UserBase):
class UserDetailWithoutPreferences(UserBase):
id: uuid.UUID
salt: str
created_at: datetime
email: Email
preferences: UserPreferences
is_admin: bool

class Config:
orm_mode = True


class UserDetail(UserDetailWithoutPreferences):
preferences: UserPreferences

class Config:
orm_mode = True


41 changes: 41 additions & 0 deletions tests/test_api_authentication.py
Original file line number Diff line number Diff line change
Expand Up @@ -71,3 +71,44 @@ def test_can_not_authenticate_with_wrong_scope(
)

assert response.status_code == 401, f"Status code should be 401 but is {response.status_code}"


def test_can_get_profile_without_preferences_with_missing_scope(
client: TestClient,
create_user,
create_api_key,
):
user = create_user(is_verified=True)
_, key = create_api_key(user=user, scopes=[APIKeyScope.PROFILE_READ])

response = client.get(
"/v1/account/me/",
headers={
"Authorization": f"Api-Key {key}"
},
)

assert response.status_code == 200, f"Status code should be 200 but is {response.status_code}"
assert response.json().get("preferences") is None, "Preferences should be None"


def test_can_get_profile_with_preferences_with_correct_scope(
client: TestClient,
create_user,
create_api_key,
) -> None:
user = create_user(is_verified=True)
_, key = create_api_key(
user=user,
scopes=[APIKeyScope.PROFILE_READ, APIKeyScope.PREFERENCES_READ]
)

response = client.get(
"/v1/account/me/",
headers={
"Authorization": f"Api-Key {key}"
},
)

assert response.status_code == 200, f"Status code should be 200 but is {response.status_code}"
assert response.json()["preferences"] is not None, "Preferences should not be None"

0 comments on commit 22d9728

Please sign in to comment.