Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,12 @@ venv:
python3.11 -m venv venv

format:
autoflake -r --in-place --remove-all-unused-imports ./services_backend
isort ./services_backend
black ./services_backend
source ./venv/bin/activate && autoflake -r --in-place --remove-all-unused-imports ./services_backend
source ./venv/bin/activate && isort ./services_backend
source ./venv/bin/activate && black ./services_backend

db:
docker run -d -p 5432:5432 -e POSTGRES_HOST_AUTH_METHOD=trust --name db-services-backend postgres:15

migrate:
alembic upgrade head
source ./venv/bin/activate && alembic upgrade head
34 changes: 34 additions & 0 deletions migrations/versions/660bb7891726_scopes.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
"""Scopes

Revision ID: 660bb7891726
Revises: 6a486347af93
Create Date: 2023-03-16 14:38:26.163590

"""
from alembic import op
import sqlalchemy as sa


# revision identifiers, used by Alembic.
revision = '660bb7891726'
down_revision = '6a486347af93'
branch_labels = None
depends_on = None


def upgrade():
op.create_table(
'scope',
sa.Column('id', sa.Integer(), nullable=False),
sa.Column('name', sa.String(), nullable=True),
sa.Column('category_id', sa.Integer(), nullable=False),
sa.ForeignKeyConstraint(
['category_id'],
['category.id'],
),
sa.PrimaryKeyConstraint('id'),
)


def downgrade():
op.drop_table('scope')
16 changes: 12 additions & 4 deletions migrations/versions/6a486347af93_order.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,11 +23,15 @@ def upgrade():
conn = op.get_bind()
res = conn.execute(sa.text("select id from button")).fetchall()
for i in range(0, len(res)):
conn.execute(sa.text(f"""UPDATE "button"
conn.execute(
sa.text(
f"""UPDATE "button"
SET "order"={i + 1},
"link"='#',
"type"='external'
WHERE id={res[i][0]}"""))
WHERE id={res[i][0]}"""
)
)
op.alter_column('button', 'order', nullable=False)
op.alter_column('button', 'link', nullable=False)
op.alter_column('button', 'type', nullable=False)
Expand All @@ -37,9 +41,13 @@ def upgrade():
op.add_column('category', sa.Column('order', sa.Integer(), nullable=True))
res = conn.execute(sa.text("select id from category")).fetchall()
for i in range(0, len(res)):
conn.execute(sa.text(f"""UPDATE "category"
conn.execute(
sa.text(
f"""UPDATE "category"
SET "order"={i + 1}
WHERE id={res[i][0]}"""))
WHERE id={res[i][0]}"""
)
)
Comment thread
Wudext marked this conversation as resolved.
op.alter_column('category', 'order', nullable=False)
op.alter_column('category', 'name', existing_type=sa.VARCHAR(), nullable=False)
op.alter_column('category', 'type', existing_type=sa.VARCHAR(), nullable=False)
Expand Down
8 changes: 8 additions & 0 deletions services_backend/models/database.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ class Category(Base):
name: Mapped[str] = mapped_column(String)
type: Mapped[str] = mapped_column(String)
buttons: Mapped[list[Button]] = relationship("Button", back_populates="category", foreign_keys="Button.category_id")
scopes: Mapped[list[Scope]] = relationship("Scope", back_populates="category")


class Button(Base):
Expand All @@ -23,3 +24,10 @@ class Button(Base):
icon: Mapped[str] = mapped_column(String)
link: Mapped[str] = mapped_column(String)
type: Mapped[str] = mapped_column(String)


class Scope(Base):
id: Mapped[int] = mapped_column(Integer, primary_key=True)
name: Mapped[str] = mapped_column(String, nullable=True)
category_id: Mapped[int] = mapped_column(Integer, ForeignKey("category.id"))
category: Mapped[Category] = relationship("Category", back_populates="scopes", foreign_keys=[category_id])
8 changes: 7 additions & 1 deletion services_backend/routes/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@

from .button import button
from .category import category
from .scope import scope


settings = get_settings()
Expand All @@ -21,7 +22,11 @@
)


app.add_middleware(DBSessionMiddleware, db_url=settings.DB_DSN, engine_args={"pool_pre_ping": True})
app.add_middleware(
DBSessionMiddleware,
db_url=settings.DB_DSN,
engine_args={"pool_pre_ping": True, "isolation_level": "AUTOCOMMIT"},
)

app.add_middleware(
CORSMiddleware,
Expand All @@ -33,3 +38,4 @@

app.include_router(button, prefix='/category/{category_id}/button', tags=["Button"])
app.include_router(category, prefix='/category', tags=["Category"])
app.include_router(scope, prefix='/category/{category_id}/scope', tags=["Scope"])
6 changes: 3 additions & 3 deletions services_backend/routes/button.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ def create_button(
if last_button:
button.order = last_button.order + 1
db.session.add(button)
db.session.commit()
db.session.flush()
return button


Expand Down Expand Up @@ -98,7 +98,7 @@ def remove_button(
raise HTTPException(status_code=404, detail="Button is not in this category")
db.session.delete(button)
db.session.query(Button).filter(Button.order > button.order).update({"order": Button.order - 1})
db.session.commit()
db.session.flush()


@button.patch("/{button_id}", response_model=ButtonUpdate)
Expand Down Expand Up @@ -143,5 +143,5 @@ def update_button(
db.session.query(Button).filter(Button.order > button.order).update({"order": Button.order - 1})

query.update(button_inp.dict(exclude_unset=True, exclude_none=True))
db.session.commit()
db.session.flush()
return button
37 changes: 28 additions & 9 deletions services_backend/routes/category.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,11 @@
from auth_lib.fastapi import UnionAuth
from fastapi import APIRouter, Depends, HTTPException, Query
from fastapi_sqlalchemy import db
from sqlalchemy.orm import joinedload

from ..models.database import Button, Category
from ..models.database import Button, Category, Scope
from .models.category import CategoryCreate, CategoryGet, CategoryUpdate


logger = logging.getLogger(__name__)
category = APIRouter()

Expand All @@ -28,7 +28,7 @@ def create_category(
if last_category:
category.order = last_category.order + 1
db.session.add(category)
db.session.commit()
db.session.flush()
return category


Expand All @@ -42,10 +42,21 @@ def get_categories(
Необходимые scopes: `-`
"""
user_id = user.get('id') if user is not None else None
logger.info(f"User {user_id} triggered get_categories")
if user_id is None:
logger.info("Unauthorised user triggered get_categories")
else:
logger.info(f"User {user_id} triggered get_categories")
Comment thread
Wudext marked this conversation as resolved.

user_scopes = set([scope["name"] for scope in user["session_scopes"]] if user else [])
filtered_categories = []
for category in db.session.query(Category).order_by(Category.order).options(joinedload(Category.scopes)).all():
category_scopes = set([scope.__dict__["name"] for scope in category.scopes])
if (category_scopes == set()) or (user_scopes & category_scopes):
Comment thread
Wudext marked this conversation as resolved.
filtered_categories.append(category)

return [
CategoryGet.from_orm(row).dict(exclude={"buttons"} if 'buttons' not in info else {})
for row in db.session.query(Category).order_by(Category.order).all()
for row in filtered_categories
]


Expand All @@ -59,15 +70,23 @@ def get_category(
Необходимые scopes: `-`
"""
user_id = user.get('id') if user is not None else None
logger.info(f"User {user_id} triggered get_category")
if user_id is None:
logger.info("Unauthorised user triggered get_category")
else:
logger.info(f"User {user_id} triggered get_category")

user_scopes = set([scope["name"] for scope in user["session_scopes"]] if user else [])
category = db.session.query(Category).filter(Category.id == category_id).one_or_none()
if not category:
if not category or (
category.scopes and not (user_scopes & set([scope.__dict__["name"] for scope in category.scopes]))
):
raise HTTPException(status_code=404, detail="Category does not exist")
return {
"id": category_id,
"order": category.order,
"name": category.name,
"type": category.type,
"scopes": category.scopes,
}


Expand All @@ -90,7 +109,7 @@ def remove_category(
db.session.flush()
db.session.query(Category).filter(Category.order > category.order).update({"order": Category.order - 1})
db.session.delete(category)
db.session.commit()
db.session.flush()


@category.patch("/{category_id}", response_model=CategoryUpdate)
Expand Down Expand Up @@ -129,5 +148,5 @@ def update_category(

query = db.session.query(Category).filter(Category.id == category_id)
query.update(category_inp.dict(exclude_unset=True, exclude_none=True))
db.session.commit()
db.session.flush()
return category
2 changes: 2 additions & 0 deletions services_backend/routes/models/category.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
from .base import Base
from .button import ButtonGet
from .scope import ScopeGet


class CategoryCreate(Base):
Expand All @@ -19,3 +20,4 @@ class CategoryGet(Base):
type: str | None
name: str | None
buttons: list[ButtonGet] | None
scopes: list[ScopeGet] | None
10 changes: 10 additions & 0 deletions services_backend/routes/models/scope.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
from .base import Base


class ScopeCreate(Base):
name: str


class ScopeGet(Base):
id: int
name: str
30 changes: 30 additions & 0 deletions services_backend/routes/scope.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
from fastapi import HTTPException, APIRouter, Depends
from fastapi_sqlalchemy import db

from auth_lib.fastapi import UnionAuth
from .models.scope import ScopeGet, ScopeCreate
from ..models.database import Scope

scope = APIRouter()
Comment thread
Wudext marked this conversation as resolved.


@scope.post("/", response_model=ScopeGet)
def create_scope(scope_inp: ScopeCreate, category_id: int, user=Depends(UnionAuth(['services.category.permission.create']))):
scope = Scope(**{"name": scope_inp.name, "category_id": category_id})
db.session.add(scope)
db.session.flush()
return scope


@scope.get("/", response_model=list[ScopeGet])
def get_scopes(category_id: int, offset: int = 0, limit: int = 100):
return db.session.query(Scope).filter(category_id == Scope.category_id).offset(offset).limit(limit).all()


@scope.delete("/{scope_id}", response_model=None)
def delete_scope(category_id: int, scope_id: int, user=Depends(UnionAuth(['services.category.permission.delete']))):
scope = db.session.query(Scope).filter(category_id == Scope.category_id).filter(Scope.id == scope_id).one_or_none()
if not scope:
raise HTTPException(status_code=404, detail="Scope does not exist")
db.session.delete(scope)
db.session.flush()
72 changes: 71 additions & 1 deletion tests/api/category.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
from starlette import status
from services_backend.settings import get_settings
from services_backend.models.database import Category
from pytest_mock import MockerFixture


class TestCategory:
Expand Down Expand Up @@ -32,8 +33,28 @@ def test_post_success(self, client, dbsession):
assert db_category_created.type == body["type"]
assert db_category_created.order == 1
assert not db_category_created.buttons
client.delete(f'{self._url}{db_category_created.id}')

def test_get_by_id_success(self, client, db_category, mocker: MockerFixture):
res = client.get(f'{self._url}{db_category.id}')
assert res.status_code == status.HTTP_200_OK
res_body = res.json()
assert res_body['id'] == db_category.id
assert res_body['type'] == db_category.type
assert res_body['name'] == db_category.name
assert res_body['order'] == db_category.order

user_mock = mocker.patch('auth_lib.fastapi.UnionAuth.__call__')
user_mock.return_value = {
"session_scopes": [{"id": 0, "name": "string", "comment": "string"},
{"id": 1, "name": "test", "comment": "string"}],
"user_scopes": [{"id": 0, "name": "string", "comment": "string"}],
"indirect_groups": [{"id": 0, "name": "string", "parent_id": 0}],
"groups": [{"id": 0, "name": "string", "parent_id": 0}],
"id": 0,
"email": "string",
}

def test_get_by_id_success(self, client, db_category):
res = client.get(f'{self._url}{db_category.id}')
assert res.status_code == status.HTTP_200_OK
res_body = res.json()
Expand Down Expand Up @@ -152,3 +173,52 @@ def test_delete_order(self, db_category, client):

res = client.get(f"{self._url}{res1.json()['id']}")
assert res.json()['order'] == 1

def test_category_scopes(self, client, dbsession, mocker: MockerFixture):
category_body = {"name": "t", "type": "string"}
res1 = client.post(self._url, data=json.dumps(category_body))
assert res1.status_code == status.HTTP_200_OK
res1_body = res1.json()

scope_body = {"name": "test"}
res2 = client.post(f"{self._url}{res1_body['id']}/scope/", data=json.dumps(scope_body))
assert res2.status_code == status.HTTP_200_OK

res = client.get(f"{self._url}{res1_body['id']}")
assert res.status_code == status.HTTP_404_NOT_FOUND

user_mock = mocker.patch('auth_lib.fastapi.UnionAuth.__call__')
user_mock.return_value = {
"session_scopes": [{"id": 0, "name": "string", "comment": "string"}, {"id": 1, "name": "test", "comment": "string"}],
"user_scopes": [{"id": 0, "name": "string", "comment": "string"}],
"indirect_groups": [{"id": 0, "name": "string", "parent_id": 0}],
"groups": [{"id": 0, "name": "string", "parent_id": 0}],
"id": 0,
"email": "string",
}

res = client.get(f"{self._url}{res1_body['id']}")
assert res.status_code == status.HTTP_200_OK

def test_category_invalid_scopes(self, client, dbsession, mocker: MockerFixture):
category_body = {"name": "t", "type": "string"}
res1 = client.post(self._url, data=json.dumps(category_body))
assert res1.status_code == status.HTTP_200_OK
res1_body = res1.json()

scope_body = {"name": "test"}
res2 = client.post(f"{self._url}{res1_body['id']}/scope/", data=json.dumps(scope_body))
assert res2.status_code == status.HTTP_200_OK

user_mock = mocker.patch('auth_lib.fastapi.UnionAuth.__call__')
user_mock.return_value = {
"session_scopes": [{"id": 0, "name": "string", "comment": "string"}, {"id": 3, "name": "lmao", "comment": "string"}],
"user_scopes": [{"id": 0, "name": "string", "comment": "string"}],
"indirect_groups": [{"id": 0, "name": "string", "parent_id": 0}],
"groups": [{"id": 0, "name": "string", "parent_id": 0}],
"id": 0,
"email": "string",
}

res = client.get(f"{self._url}{res1_body['id']}")
assert res.status_code == status.HTTP_404_NOT_FOUND
Loading