Skip to content

Commit

Permalink
fix users and groups
Browse files Browse the repository at this point in the history
  • Loading branch information
ciur committed Jan 18, 2025
1 parent e0d748e commit 66790e3
Show file tree
Hide file tree
Showing 17 changed files with 495 additions and 205 deletions.
2 changes: 1 addition & 1 deletion docker/standard/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ RUN apk update && apk add linux-headers python3-dev \
libpq-dev \
poppler-utils

RUN pip install --upgrade poetry roco==0.4.2
RUN pip install --upgrade poetry==1.8.2 roco==0.4.2
RUN curl -L -o /bin/env2js https://github.com/papermerge/env2js/releases/download/0.2/env2js.amd64
RUN chmod +x /bin/env2js

Expand Down
7 changes: 6 additions & 1 deletion papermerge/core/dbapi.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
get_doc_ver_pages
)
from .features.nodes.db.api import get_nodes
from .features.groups.db.api import get_group, sync_perms
from .features.document_types.db.api import (
create_document_type,
get_document_types,
Expand Down Expand Up @@ -43,8 +44,12 @@
"delete_document_type",
"update_document_type",
"create_custom_field",
# users
"update_user",
"get_user",
"change_password",
"get_users_count"
"get_users_count",
# groups
"get_group",
"sync_perms"
]
9 changes: 9 additions & 0 deletions papermerge/core/features/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -584,3 +584,12 @@ def _maker(name: str):
return group

return _maker


@pytest.fixture()
def random_string():
from random import choice
from string import ascii_uppercase

ret = "".join(choice(ascii_uppercase) for i in range(12))
return ret
5 changes: 3 additions & 2 deletions papermerge/core/features/groups/db/api.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import logging
import math
import uuid

from sqlalchemy import delete, select, func
from sqlalchemy.orm import joinedload
Expand All @@ -12,7 +13,7 @@
logger = logging.getLogger(__name__)


def get_group(db_session: Session, group_id: int) -> schema.GroupDetails:
def get_group(db_session: Session, group_id: uuid.UUID) -> schema.GroupDetails:
stmt = (
select(orm.Group)
.options(joinedload(orm.Group.permissions))
Expand Down Expand Up @@ -74,7 +75,7 @@ def create_group(


def update_group(
db_session: Session, group_id: int, attrs: schema.UpdateGroup
db_session: Session, group_id: uuid.UUID, attrs: schema.UpdateGroup
) -> schema.Group:
stmt = select(orm.Permission).where(orm.Permission.codename.in_(attrs.scopes))
perms = db_session.execute(stmt).scalars().all()
Expand Down
5 changes: 3 additions & 2 deletions papermerge/core/features/groups/router.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import logging
import uuid
from typing import Annotated

from fastapi import APIRouter, Depends, HTTPException, Security
Expand Down Expand Up @@ -59,7 +60,7 @@ def get_groups(
@router.get("/{group_id}", response_model=schema.GroupDetails)
@utils.docstring_parameter(scope=scopes.GROUP_VIEW)
def get_group(
group_id: int,
group_id: uuid.UUID,
user: Annotated[
schema.User, Security(get_current_user, scopes=[scopes.GROUP_VIEW])
],
Expand Down Expand Up @@ -136,7 +137,7 @@ def delete_group(
@router.patch("/{group_id}", status_code=200, response_model=schema.Group)
@utils.docstring_parameter(scope=scopes.GROUP_UPDATE)
def update_group(
group_id: int,
group_id: uuid.UUID,
attrs: schema.UpdateGroup,
cur_user: Annotated[
schema.User, Security(get_current_user, scopes=[scopes.GROUP_UPDATE])
Expand Down
29 changes: 28 additions & 1 deletion papermerge/core/features/groups/tests/test_router_groups.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
from sqlalchemy import func

from papermerge.core import schema, db
from papermerge.core.db.engine import Session
from papermerge.core import schema, db, dbapi
from papermerge.core.features.groups.db import orm
from papermerge.core.tests.types import AuthTestClient

Expand All @@ -19,6 +20,32 @@ def test_create_group_route(auth_api_client: AuthTestClient, db_session: db.Sess
assert count_after == 1


def test_update_group_route(auth_api_client: AuthTestClient, make_group, db_session):
group = make_group(name="demo")

dbapi.sync_perms(db_session)
response = auth_api_client.patch(
f"/groups/{group.id}",
json={"name": "Admin", "scopes": ["user.view", "custom_field.view"]},
)

assert response.status_code == 200, response.json()

updated_group = dbapi.get_group(db_session, group_id=group.id)

assert set(updated_group.scopes) == {"user.view", "custom_field.view"}


def test_get_group_details(
make_group, auth_api_client: AuthTestClient, db_session: db.Session
):
group = make_group(name="demo")

response = auth_api_client.get(f"/groups/{group.id}")

assert response.status_code == 200, response.json()


def test_pagination_group_route_basic(auth_api_client: AuthTestClient):
params = {"page_number": 1, "page_size": 1}
response = auth_api_client.get("/groups/", params=params)
Expand Down
4 changes: 1 addition & 3 deletions papermerge/core/features/users/db/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -263,9 +263,7 @@ def get_users_count(db_session: db.Session) -> int:


def change_password(
db_session: db.Session,
user_id: uuid.UUID,
password: str
db_session: db.Session, user_id: uuid.UUID, password: str
) -> Tuple[schema.User | None, err_schema.Error | None]:
db_user = db_session.get(User, user_id)

Expand Down
Empty file.
13 changes: 13 additions & 0 deletions papermerge/core/features/users/tests/test_dbapi_users.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,3 +37,16 @@ def test_user_update(db_session, make_user):
updated_user = db_session.execute(stmt).scalar()

assert updated_user.is_superuser == True


def test_change_password(db_session, make_user):
user: orm.User = make_user("momo", is_superuser=False)

initial_password = user.password

dbapi.change_password(db_session, user_id=user.id, password="updatedpass")

stmt = select(orm.User).where(orm.User.id == user.id)
updated_user = db_session.execute(stmt).scalar()

assert updated_user.password != initial_password
32 changes: 11 additions & 21 deletions papermerge/core/features/users/tests/test_users_router.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
import uuid
from passlib.hash import pbkdf2_sha256
from papermerge.core.db.engine import Session

from sqlalchemy import select

from papermerge.core import schema, dbapi, orm
from papermerge.core.tests.types import AuthTestClient

from .utils import verify_password


def test_list_users(make_user, auth_api_client: AuthTestClient):
Expand All @@ -25,18 +26,15 @@ def test_create_user(make_group, auth_api_client: AuthTestClient):
"scopes": [],
"is_active": True,
"is_superuser": False,
"password": "blah"
"password": "blah",
}
response = auth_api_client.post("/users/", json=data)

assert response.status_code == 201, response.json()


def test_get_user_details(
make_user,
make_group,
auth_api_client: AuthTestClient,
db_session
make_user, make_group, auth_api_client: AuthTestClient, db_session
):
"""In this scenario user belongs to one group"""
user = make_user(username="Karl")
Expand All @@ -51,10 +49,7 @@ def test_get_user_details(


def test_delete_user(
make_user,
make_group,
auth_api_client: AuthTestClient,
db_session
make_user, make_group, auth_api_client: AuthTestClient, db_session
):
user = make_user(username="Karl")
group = make_group(name="demo")
Expand All @@ -68,25 +63,20 @@ def test_delete_user(


def test_change_user_password(
make_user,
auth_api_client: AuthTestClient,
db_session
make_user, auth_api_client: AuthTestClient, random_string
):
user = make_user(username="Karl")

data = {
"userId": str(user.id),
"password": "secret"
}
data = {"userId": str(user.id), "password": random_string}

response = auth_api_client.post(f"/users/{user.id}/change-password", json=data)

assert response.status_code == 200, response.text
stmt = select(orm.User).where(orm.User.id == user.id)
with Session() as s:
db_user = s.execute(stmt).scalar()

db_user = db_session.execute(stmt).scalar()

assert pbkdf2_sha256.verify("secret", db_user.password)
assert verify_password(random_string, db_user.password)


def test_users_paginated_result__9_items_first_page(
Expand Down
4 changes: 4 additions & 0 deletions papermerge/core/features/users/tests/utils.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
def verify_password(str_password, hashed_password):
from passlib.hash import pbkdf2_sha256

return pbkdf2_sha256.verify(str_password, hashed_password)
112 changes: 76 additions & 36 deletions ui2/src/components/NavBar/NavBar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,14 @@ import {
selectCommanderViewOption,
selectNavBarCollapsed
} from "@/features/ui/uiSlice"
import {
CUSTOM_FIELD_VIEW,
DOCUMENT_TYPE_VIEW,
GROUP_VIEW,
NODE_VIEW,
TAG_VIEW,
USER_VIEW
} from "@/scopes"
import {
selectCurrentUser,
selectCurrentUserError,
Expand Down Expand Up @@ -59,25 +67,41 @@ function NavBarFull() {
return (
<>
<div className="navbar">
<NavLink to={`/home/${user.home_folder_id}`} onClick={onClick}>
{NavLinkWithFeedback("Home", <IconHome />)}
</NavLink>
<NavLink to={`/inbox/${user.inbox_folder_id}`} onClick={onClick}>
{NavLinkWithFeedback("Inbox", <IconInbox />)}
</NavLink>
<NavLink to="/tags">{NavLinkWithFeedback("Tags", <IconTag />)}</NavLink>
<NavLink to="/custom-fields">
{NavLinkWithFeedback("Custom Fields", <IconAlignJustified />)}
</NavLink>
<NavLink to="/document-types">
{NavLinkWithFeedback("Document Types", <IconFile3d />)}
</NavLink>
<NavLink to="/users">
{NavLinkWithFeedback("Users", <IconUsers />)}
</NavLink>
<NavLink to="/groups">
{NavLinkWithFeedback("Groups", <IconUsersGroup />)}
</NavLink>
{user.scopes.includes(NODE_VIEW) && (
<NavLink to={`/home/${user.home_folder_id}`} onClick={onClick}>
{NavLinkWithFeedback("Home", <IconHome />)}
</NavLink>
)}
{user.scopes.includes(NODE_VIEW) && (
<NavLink to={`/inbox/${user.inbox_folder_id}`} onClick={onClick}>
{NavLinkWithFeedback("Inbox", <IconInbox />)}
</NavLink>
)}
{user.scopes.includes(TAG_VIEW) && (
<NavLink to="/tags">
{NavLinkWithFeedback("Tags", <IconTag />)}
</NavLink>
)}
{user.scopes.includes(CUSTOM_FIELD_VIEW) && (
<NavLink to="/custom-fields">
{NavLinkWithFeedback("Custom Fields", <IconAlignJustified />)}
</NavLink>
)}
{user.scopes.includes(DOCUMENT_TYPE_VIEW) && (
<NavLink to="/document-types">
{NavLinkWithFeedback("Document Types", <IconFile3d />)}
</NavLink>
)}
{user.scopes.includes(USER_VIEW) && (
<NavLink to="/users">
{NavLinkWithFeedback("Users", <IconUsers />)}
</NavLink>
)}
{user.scopes.includes(GROUP_VIEW) && (
<NavLink to="/groups">
{NavLinkWithFeedback("Groups", <IconUsersGroup />)}
</NavLink>
)}
</div>
</>
)
Expand Down Expand Up @@ -115,23 +139,39 @@ function NavBarCollapsed() {
return (
<>
<div className="navbar">
<NavLink to={`/home/${user.home_folder_id}`} onClick={onClick}>
{NavLinkWithFeedbackShort(<IconHome />)}
</NavLink>
<NavLink to={`/inbox/${user.inbox_folder_id}`} onClick={onClick}>
{NavLinkWithFeedbackShort(<IconInbox />)}
</NavLink>
<NavLink to="/tags">{NavLinkWithFeedbackShort(<IconTag />)}</NavLink>
<NavLink to="/custom-fields">
{NavLinkWithFeedbackShort(<IconAlignJustified />)}
</NavLink>
<NavLink to="/document-types">
{NavLinkWithFeedbackShort(<IconFile3d />)}
</NavLink>
<NavLink to="/users">{NavLinkWithFeedbackShort(<IconUsers />)}</NavLink>
<NavLink to="/groups">
{NavLinkWithFeedbackShort(<IconUsersGroup />)}
</NavLink>
{user.scopes.includes(NODE_VIEW) && (
<NavLink to={`/home/${user.home_folder_id}`} onClick={onClick}>
{NavLinkWithFeedbackShort(<IconHome />)}
</NavLink>
)}
{user.scopes.includes(NODE_VIEW) && (
<NavLink to={`/inbox/${user.inbox_folder_id}`} onClick={onClick}>
{NavLinkWithFeedbackShort(<IconInbox />)}
</NavLink>
)}
{user.scopes.includes(TAG_VIEW) && (
<NavLink to="/tags">{NavLinkWithFeedbackShort(<IconTag />)}</NavLink>
)}
{user.scopes.includes(CUSTOM_FIELD_VIEW) && (
<NavLink to="/custom-fields">
{NavLinkWithFeedbackShort(<IconAlignJustified />)}
</NavLink>
)}
{user.scopes.includes(DOCUMENT_TYPE_VIEW) && (
<NavLink to="/document-types">
{NavLinkWithFeedbackShort(<IconFile3d />)}
</NavLink>
)}
{user.scopes.includes(USER_VIEW) && (
<NavLink to="/users">
{NavLinkWithFeedbackShort(<IconUsers />)}
</NavLink>
)}
{user.scopes.includes(GROUP_VIEW) && (
<NavLink to="/groups">
{NavLinkWithFeedbackShort(<IconUsersGroup />)}
</NavLink>
)}
</div>
</>
)
Expand Down
Loading

0 comments on commit 66790e3

Please sign in to comment.