Skip to content

Commit d3b2e66

Browse files
authored
fix user update endpoint (#576)
1 parent 52cc15e commit d3b2e66

File tree

21 files changed

+642
-211
lines changed

21 files changed

+642
-211
lines changed

.github/workflows/tests.yml

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ on:
1010
- 'ui/**'
1111
- 'pyproject.toml'
1212
- '.github/workflows/tests.yml'
13+
- 'docker/standard/**'
1314

1415
jobs:
1516
tests_with_sqlite:
@@ -21,11 +22,11 @@ jobs:
2122
with:
2223
python-version: '3.13'
2324
- name: Install required debian packages
24-
run: sudo apt-get install -y poppler-utils # required by pdf2image
25+
run: sudo apt-get update && sudo apt-get install -y poppler-utils # required by pdf2image
2526
- name: Install python dependencies
2627
run: |
2728
python -m pip install --upgrade pip
28-
pip install poetry==1.8
29+
pip install poetry==1.8.2
2930
poetry install --with test && poetry run pytest papermerge/
3031
env:
3132
PAPERMERGE__DATABASE__URL: 'sqlite:///test.sqlite'
@@ -54,11 +55,11 @@ jobs:
5455
with:
5556
python-version: '3.13'
5657
- name: Install required debian packages
57-
run: sudo apt-get install -y poppler-utils # required by pdf2image
58+
run: sudo apt-get update && sudo apt-get install -y poppler-utils # required by pdf2image
5859
- name: Install python dependencies
5960
run: |
6061
python -m pip install --upgrade pip
61-
pip install poetry==1.8
62+
pip install poetry==1.8.2
6263
poetry install --with test -E pg && poetry run pytest papermerge/
6364
env:
6465
PAPERMERGE__DATABASE__URL: 'postgresql://pmguser:pmgpass@localhost:5432/test_pmgdb'

papermerge/core/dbapi.py

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
from .features.page_mngm.db.api import move_pages
2+
from .features.users.db.api import update_user, get_users_count, change_password, get_user
23
from .features.document.db.api import (
34
get_last_doc_ver,
45
upload,
@@ -12,6 +13,7 @@
1213
get_doc_ver_pages
1314
)
1415
from .features.nodes.db.api import get_nodes
16+
from .features.groups.db.api import get_group, sync_perms
1517
from .features.document_types.db.api import (
1618
create_document_type,
1719
get_document_types,
@@ -41,5 +43,13 @@
4143
"get_document_types_without_pagination",
4244
"delete_document_type",
4345
"update_document_type",
44-
"create_custom_field"
46+
"create_custom_field",
47+
# users
48+
"update_user",
49+
"get_user",
50+
"change_password",
51+
"get_users_count",
52+
# groups
53+
"get_group",
54+
"sync_perms"
4555
]

papermerge/core/features/conftest.py

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -573,3 +573,23 @@ def _make_tax(title: str, user: orm.User, parent=None):
573573
return doc
574574

575575
return _make_tax
576+
577+
578+
@pytest.fixture()
579+
def make_group(db_session):
580+
def _maker(name: str):
581+
group = orm.Group(name=name)
582+
db_session.add(group)
583+
db_session.commit()
584+
return group
585+
586+
return _maker
587+
588+
589+
@pytest.fixture()
590+
def random_string():
591+
from random import choice
592+
from string import ascii_uppercase
593+
594+
ret = "".join(choice(ascii_uppercase) for i in range(12))
595+
return ret

papermerge/core/features/groups/db/api.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import logging
22
import math
3+
import uuid
34

45
from sqlalchemy import delete, select, func
56
from sqlalchemy.orm import joinedload
@@ -12,7 +13,7 @@
1213
logger = logging.getLogger(__name__)
1314

1415

15-
def get_group(db_session: Session, group_id: int) -> schema.GroupDetails:
16+
def get_group(db_session: Session, group_id: uuid.UUID) -> schema.GroupDetails:
1617
stmt = (
1718
select(orm.Group)
1819
.options(joinedload(orm.Group.permissions))
@@ -74,7 +75,7 @@ def create_group(
7475

7576

7677
def update_group(
77-
db_session: Session, group_id: int, attrs: schema.UpdateGroup
78+
db_session: Session, group_id: uuid.UUID, attrs: schema.UpdateGroup
7879
) -> schema.Group:
7980
stmt = select(orm.Permission).where(orm.Permission.codename.in_(attrs.scopes))
8081
perms = db_session.execute(stmt).scalars().all()

papermerge/core/features/groups/router.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import logging
2+
import uuid
23
from typing import Annotated
34

45
from fastapi import APIRouter, Depends, HTTPException, Security
@@ -59,7 +60,7 @@ def get_groups(
5960
@router.get("/{group_id}", response_model=schema.GroupDetails)
6061
@utils.docstring_parameter(scope=scopes.GROUP_VIEW)
6162
def get_group(
62-
group_id: int,
63+
group_id: uuid.UUID,
6364
user: Annotated[
6465
schema.User, Security(get_current_user, scopes=[scopes.GROUP_VIEW])
6566
],
@@ -136,7 +137,7 @@ def delete_group(
136137
@router.patch("/{group_id}", status_code=200, response_model=schema.Group)
137138
@utils.docstring_parameter(scope=scopes.GROUP_UPDATE)
138139
def update_group(
139-
group_id: int,
140+
group_id: uuid.UUID,
140141
attrs: schema.UpdateGroup,
141142
cur_user: Annotated[
142143
schema.User, Security(get_current_user, scopes=[scopes.GROUP_UPDATE])

papermerge/core/features/groups/tests/conftest.py

Lines changed: 0 additions & 13 deletions
This file was deleted.

papermerge/core/features/groups/tests/test_router_groups.py

Lines changed: 28 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
from sqlalchemy import func
22

3-
from papermerge.core import schema, db
3+
from papermerge.core.db.engine import Session
4+
from papermerge.core import schema, db, dbapi
45
from papermerge.core.features.groups.db import orm
56
from papermerge.core.tests.types import AuthTestClient
67

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

2122

23+
def test_update_group_route(auth_api_client: AuthTestClient, make_group, db_session):
24+
group = make_group(name="demo")
25+
26+
dbapi.sync_perms(db_session)
27+
response = auth_api_client.patch(
28+
f"/groups/{group.id}",
29+
json={"name": "Admin", "scopes": ["user.view", "custom_field.view"]},
30+
)
31+
32+
assert response.status_code == 200, response.json()
33+
34+
updated_group = dbapi.get_group(db_session, group_id=group.id)
35+
36+
assert set(updated_group.scopes) == {"user.view", "custom_field.view"}
37+
38+
39+
def test_get_group_details(
40+
make_group, auth_api_client: AuthTestClient, db_session: db.Session
41+
):
42+
group = make_group(name="demo")
43+
44+
response = auth_api_client.get(f"/groups/{group.id}")
45+
46+
assert response.status_code == 200, response.json()
47+
48+
2249
def test_pagination_group_route_basic(auth_api_client: AuthTestClient):
2350
params = {"page_number": 1, "page_size": 1}
2451
response = auth_api_client.get("/groups/", params=params)

papermerge/core/features/users/db/api.py

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -255,3 +255,26 @@ def delete_user(
255255
user = db_session.execute(stmt).scalars().one()
256256
db_session.delete(user)
257257
db_session.commit()
258+
259+
260+
def get_users_count(db_session: db.Session) -> int:
261+
stmt = select(func.count(orm.User.id))
262+
return db_session.execute(stmt).scalar()
263+
264+
265+
def change_password(
266+
db_session: db.Session, user_id: uuid.UUID, password: str
267+
) -> Tuple[schema.User | None, err_schema.Error | None]:
268+
db_user = db_session.get(User, user_id)
269+
270+
db_user.password = pbkdf2_sha256.hash(password)
271+
272+
try:
273+
db_session.commit()
274+
except Exception as e:
275+
error = err_schema.Error(messages=[str(e)])
276+
return None, error
277+
278+
user = schema.User.model_validate(db_user)
279+
280+
return user, None

papermerge/core/features/users/router.py

Lines changed: 36 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -101,7 +101,7 @@ def create_user(
101101

102102
@router.get(
103103
"/{user_id}",
104-
status_code=201,
104+
status_code=200,
105105
responses={
106106
404: {
107107
"description": """No user with specified UUID found""",
@@ -157,18 +157,20 @@ def delete_user(
157157
158158
Required scope: `{scope}`
159159
"""
160-
if User.objects.count() == 1:
161-
raise HTTPException(
162-
status_code=432, detail="Deletion not possible. Only one user left."
163-
)
160+
with db.Session() as db_session:
161+
if usr_dbapi.get_users_count(db_session) == 1:
162+
raise HTTPException(
163+
status_code=432, detail="Deletion not possible. Only one user left."
164+
)
164165

165166
try:
166167
if os.environ.get("PAPERMERGE__REDIS__URL"):
167168
delete_user_data.apply_async(kwargs={"user_id": str(user_id)})
168169
else:
169-
delete_user_data(user_id)
170-
except User.DoesNotExist:
171-
raise HTTPException(status_code=404, detail="Does not exists")
170+
usr_dbapi.delete_user(db_session, user_id=user_id)
171+
except Exception as e:
172+
logger.error(e)
173+
raise HTTPException(status_code=469, detail=str(e))
172174

173175

174176
@router.patch("/{user_id}", status_code=200, response_model=schema.UserDetails)
@@ -191,3 +193,29 @@ def update_user(
191193
raise HTTPException(status_code=404, detail=error.model_dump())
192194

193195
return user
196+
197+
198+
@router.post("/{user_id}/change-password", status_code=200, response_model=schema.UserDetails)
199+
@utils.docstring_parameter(scope=scopes.USER_UPDATE)
200+
def change_user_password(
201+
user_id: UUID,
202+
attrs: schema.ChangeUserPassword,
203+
cur_user: Annotated[
204+
schema.User, Security(get_current_user, scopes=[scopes.USER_UPDATE])
205+
],
206+
) -> schema.UserDetails:
207+
"""Change user password
208+
209+
Required scope: `{scope}`
210+
"""
211+
with db.Session() as db_session:
212+
user, error = usr_dbapi.change_password(
213+
db_session,
214+
user_id=UUID(attrs.userId),
215+
password=attrs.password
216+
)
217+
218+
if error:
219+
raise HTTPException(status_code=469, detail=error.model_dump())
220+
221+
return user

papermerge/core/features/users/schema.py

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212

1313

1414
class Group(BaseModel):
15-
id: int
15+
id: UUID
1616
name: str
1717

1818

@@ -22,7 +22,7 @@ class RemoteUser(BaseModel):
2222
username: str
2323
email: str = ""
2424
name: str = ""
25-
groups: list[str] = []
25+
groups: list[str] | None = None
2626

2727

2828
class User(BaseModel):
@@ -93,7 +93,7 @@ class CreateUser(BaseModel):
9393
is_superuser: bool
9494
is_active: bool
9595
scopes: list[str] # list of scope names e.g. "user.create", "user.delete"
96-
group_ids: list[int] # list of group IDs e.g. 65, 72
96+
group_ids: list[UUID]
9797

9898
# Config
9999
model_config = ConfigDict(from_attributes=True)
@@ -106,4 +106,9 @@ class UpdateUser(BaseModel):
106106
is_superuser: bool | None = None
107107
is_active: bool | None = None
108108
scopes: list[str] | None = None
109-
group_ids: list[int] | None = None
109+
group_ids: list[UUID] | None = None
110+
111+
112+
class ChangeUserPassword(BaseModel):
113+
userId: str
114+
password: str

papermerge/core/features/users/tests/__init__.py

Whitespace-only changes.

papermerge/core/features/users/tests/test_dbapi_users.py

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,3 +37,16 @@ def test_user_update(db_session, make_user):
3737
updated_user = db_session.execute(stmt).scalar()
3838

3939
assert updated_user.is_superuser == True
40+
41+
42+
def test_change_password(db_session, make_user):
43+
user: orm.User = make_user("momo", is_superuser=False)
44+
45+
initial_password = user.password
46+
47+
dbapi.change_password(db_session, user_id=user.id, password="updatedpass")
48+
49+
stmt = select(orm.User).where(orm.User.id == user.id)
50+
updated_user = db_session.execute(stmt).scalar()
51+
52+
assert updated_user.password != initial_password

0 commit comments

Comments
 (0)