Skip to content

Commit 71d437c

Browse files
committed
Migrate to declarative authz
1 parent 758c65f commit 71d437c

25 files changed

Lines changed: 401 additions & 273 deletions

pyproject.toml

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -123,15 +123,13 @@ ignore = [
123123
[tool.ruff.lint.per-file-ignores]
124124
"src/app/infrastructure/persistence_sqla/alembic/**" = ["ALL", ]
125125
"tests/**" = [
126+
"ARG002", # unused-method-argument
126127
"PLC2801", # unnecessary-dunder-call
127128
"PLR2004", # magic-value-comparison
128129
"PT011", # pytest-raises-too-broad
129130
"S101", # assert
130-
"S105", # hardcoded-password-string
131131
"S106", # hardcoded-password-func-arg
132132
"S107", # hardcoded-password-default
133-
"SLF001", # private-member-access
134-
"UP012", # unnecessary-encode-utf8
135133
]
136134
#
137135
"src/app/domain/value_objects/base.py" = ["B024", ] # abstract-base-class-without-abstract-method

src/app/application/commands/change_password.py

Lines changed: 18 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,19 @@
11
import logging
22
from dataclasses import dataclass
33

4-
from app.application.common.permissions import AnyOf, IsSelf, IsSuperior
54
from app.application.common.ports.transaction_manager import (
65
TransactionManager,
76
)
87
from app.application.common.ports.user_command_gateway import UserCommandGateway
9-
from app.application.common.services.authorization import AuthorizationService
8+
from app.application.common.services.authorization.authorize import (
9+
authorize,
10+
)
11+
from app.application.common.services.authorization.composite import AnyOf
12+
from app.application.common.services.authorization.permissions import (
13+
CanManageSelf,
14+
CanManageSubordinate,
15+
UserManagementContext,
16+
)
1017
from app.application.common.services.current_user import CurrentUserService
1118
from app.domain.entities.user import User
1219
from app.domain.exceptions.user import UserNotFoundByUsernameError
@@ -40,13 +47,11 @@ class ChangePasswordInteractor:
4047
def __init__(
4148
self,
4249
current_user_service: CurrentUserService,
43-
authorization_service: AuthorizationService,
4450
user_command_gateway: UserCommandGateway,
4551
user_service: UserService,
4652
transaction_manager: TransactionManager,
4753
):
4854
self._current_user_service = current_user_service
49-
self._authorization_service = authorization_service
5055
self._user_command_gateway = user_command_gateway
5156
self._user_service = user_service
5257
self._transaction_manager = transaction_manager
@@ -65,11 +70,15 @@ async def __call__(self, request_data: ChangePasswordRequest) -> None:
6570
if user is None:
6671
raise UserNotFoundByUsernameError(username)
6772

68-
# Declarative authorization: can change own or subordinate's password
69-
self._authorization_service.authorize(
70-
current_user,
71-
AnyOf(IsSelf(), IsSuperior()),
72-
target_user=user,
73+
authorize(
74+
AnyOf(
75+
CanManageSelf(),
76+
CanManageSubordinate(),
77+
),
78+
context=UserManagementContext(
79+
subject=current_user,
80+
target=user,
81+
),
7382
)
7483

7584
self._user_service.change_password(user, password)

src/app/application/commands/create_user.py

Lines changed: 14 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,13 @@
77
TransactionManager,
88
)
99
from app.application.common.ports.user_command_gateway import UserCommandGateway
10-
from app.application.common.services.authorization import AuthorizationService
10+
from app.application.common.services.authorization.authorize import (
11+
authorize,
12+
)
13+
from app.application.common.services.authorization.permissions import (
14+
CanManageRole,
15+
RoleManagementContext,
16+
)
1117
from app.application.common.services.current_user import CurrentUserService
1218
from app.domain.enums.user_role import UserRole
1319
from app.domain.exceptions.user import UsernameAlreadyExistsError
@@ -46,13 +52,11 @@ class CreateUserInteractor:
4652
def __init__(
4753
self,
4854
current_user_service: CurrentUserService,
49-
authorization_service: AuthorizationService,
5055
user_command_gateway: UserCommandGateway,
5156
user_service: UserService,
5257
transaction_manager: TransactionManager,
5358
):
5459
self._current_user_service = current_user_service
55-
self._authorization_service = authorization_service
5660
self._user_command_gateway = user_command_gateway
5761
self._user_service = user_service
5862
self._transaction_manager = transaction_manager
@@ -64,9 +68,13 @@ async def __call__(self, request_data: CreateUserRequest) -> CreateUserResponse:
6468
)
6569

6670
current_user = await self._current_user_service.get_current_user()
67-
self._authorization_service.authorize_for_subordinate_role(
68-
current_user.role,
69-
target_role=request_data.role,
71+
72+
authorize(
73+
CanManageRole(),
74+
context=RoleManagementContext(
75+
subject=current_user,
76+
target_role=request_data.role,
77+
),
7078
)
7179

7280
username = Username(request_data.username)

src/app/application/commands/grant_admin.py

Lines changed: 13 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,17 @@
11
import logging
22
from dataclasses import dataclass
33

4-
from app.application.common.permissions import CanManageRole
54
from app.application.common.ports.transaction_manager import (
65
TransactionManager,
76
)
87
from app.application.common.ports.user_command_gateway import UserCommandGateway
9-
from app.application.common.services.authorization import AuthorizationService
8+
from app.application.common.services.authorization.authorize import (
9+
authorize,
10+
)
11+
from app.application.common.services.authorization.permissions import (
12+
CanManageRole,
13+
RoleManagementContext,
14+
)
1015
from app.application.common.services.current_user import CurrentUserService
1116
from app.domain.entities.user import User
1217
from app.domain.enums.user_role import UserRole
@@ -39,13 +44,11 @@ class GrantAdminInteractor:
3944
def __init__(
4045
self,
4146
current_user_service: CurrentUserService,
42-
authorization_service: AuthorizationService,
4347
user_command_gateway: UserCommandGateway,
4448
user_service: UserService,
4549
transaction_manager: TransactionManager,
4650
):
4751
self._current_user_service = current_user_service
48-
self._authorization_service = authorization_service
4952
self._user_command_gateway = user_command_gateway
5053
self._user_service = user_service
5154
self._transaction_manager = transaction_manager
@@ -58,10 +61,12 @@ async def __call__(self, request_data: GrantAdminRequest) -> None:
5861

5962
current_user = await self._current_user_service.get_current_user()
6063

61-
# Declarative authorization: only users who can manage ADMIN role
62-
self._authorization_service.authorize(
63-
current_user,
64-
CanManageRole(UserRole.ADMIN),
64+
authorize(
65+
CanManageRole(),
66+
context=RoleManagementContext(
67+
subject=current_user,
68+
target_role=UserRole.ADMIN,
69+
),
6570
)
6671

6772
username = Username(request_data.username)

src/app/application/commands/inactivate_user.py

Lines changed: 22 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,15 @@
66
TransactionManager,
77
)
88
from app.application.common.ports.user_command_gateway import UserCommandGateway
9-
from app.application.common.services.authorization import AuthorizationService
9+
from app.application.common.services.authorization.authorize import (
10+
authorize,
11+
)
12+
from app.application.common.services.authorization.permissions import (
13+
CanManageRole,
14+
CanManageSubordinate,
15+
RoleManagementContext,
16+
UserManagementContext,
17+
)
1018
from app.application.common.services.current_user import CurrentUserService
1119
from app.domain.entities.user import User
1220
from app.domain.enums.user_role import UserRole
@@ -41,14 +49,12 @@ class InactivateUserInteractor:
4149
def __init__(
4250
self,
4351
current_user_service: CurrentUserService,
44-
authorization_service: AuthorizationService,
4552
user_command_gateway: UserCommandGateway,
4653
user_service: UserService,
4754
transaction_manager: TransactionManager,
4855
access_revoker: AccessRevoker,
4956
):
5057
self._current_user_service = current_user_service
51-
self._authorization_service = authorization_service
5258
self._user_command_gateway = user_command_gateway
5359
self._user_service = user_service
5460
self._transaction_manager = transaction_manager
@@ -61,9 +67,13 @@ async def __call__(self, request_data: InactivateUserRequest) -> None:
6167
)
6268

6369
current_user = await self._current_user_service.get_current_user()
64-
self._authorization_service.authorize_for_subordinate_role(
65-
current_user.role,
66-
target_role=UserRole.USER,
70+
71+
authorize(
72+
CanManageRole(),
73+
context=RoleManagementContext(
74+
subject=current_user,
75+
target_role=UserRole.USER,
76+
),
6777
)
6878

6979
username = Username(request_data.username)
@@ -74,9 +84,12 @@ async def __call__(self, request_data: InactivateUserRequest) -> None:
7484
if user is None:
7585
raise UserNotFoundByUsernameError(username)
7686

77-
self._authorization_service.authorize_for_subordinate_role(
78-
current_user.role,
79-
target_role=user.role,
87+
authorize(
88+
CanManageSubordinate(),
89+
context=UserManagementContext(
90+
subject=current_user,
91+
target=user,
92+
),
8093
)
8194

8295
self._user_service.toggle_user_activation(user, is_active=False)

src/app/application/commands/reactivate_user.py

Lines changed: 22 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,15 @@
55
TransactionManager,
66
)
77
from app.application.common.ports.user_command_gateway import UserCommandGateway
8-
from app.application.common.services.authorization import AuthorizationService
8+
from app.application.common.services.authorization.authorize import (
9+
authorize,
10+
)
11+
from app.application.common.services.authorization.permissions import (
12+
CanManageRole,
13+
CanManageSubordinate,
14+
RoleManagementContext,
15+
UserManagementContext,
16+
)
917
from app.application.common.services.current_user import CurrentUserService
1018
from app.domain.entities.user import User
1119
from app.domain.enums.user_role import UserRole
@@ -39,13 +47,11 @@ class ReactivateUserInteractor:
3947
def __init__(
4048
self,
4149
current_user_service: CurrentUserService,
42-
authorization_service: AuthorizationService,
4350
user_command_gateway: UserCommandGateway,
4451
user_service: UserService,
4552
transaction_manager: TransactionManager,
4653
):
4754
self._current_user_service = current_user_service
48-
self._authorization_service = authorization_service
4955
self._user_command_gateway = user_command_gateway
5056
self._user_service = user_service
5157
self._transaction_manager = transaction_manager
@@ -57,9 +63,13 @@ async def __call__(self, request_data: ReactivateUserRequest) -> None:
5763
)
5864

5965
current_user = await self._current_user_service.get_current_user()
60-
self._authorization_service.authorize_for_subordinate_role(
61-
current_user.role,
62-
target_role=UserRole.USER,
66+
67+
authorize(
68+
CanManageRole(),
69+
context=RoleManagementContext(
70+
subject=current_user,
71+
target_role=UserRole.USER,
72+
),
6373
)
6474

6575
username = Username(request_data.username)
@@ -70,9 +80,12 @@ async def __call__(self, request_data: ReactivateUserRequest) -> None:
7080
if user is None:
7181
raise UserNotFoundByUsernameError(username)
7282

73-
self._authorization_service.authorize_for_subordinate_role(
74-
current_user.role,
75-
target_role=user.role,
83+
authorize(
84+
CanManageSubordinate(),
85+
context=UserManagementContext(
86+
subject=current_user,
87+
target=user,
88+
),
7689
)
7790

7891
self._user_service.toggle_user_activation(user, is_active=True)

src/app/application/commands/revoke_admin.py

Lines changed: 12 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,11 @@
55
TransactionManager,
66
)
77
from app.application.common.ports.user_command_gateway import UserCommandGateway
8-
from app.application.common.services.authorization import AuthorizationService
8+
from app.application.common.services.authorization.authorize import authorize
9+
from app.application.common.services.authorization.permissions import (
10+
CanManageRole,
11+
RoleManagementContext,
12+
)
913
from app.application.common.services.current_user import CurrentUserService
1014
from app.domain.entities.user import User
1115
from app.domain.enums.user_role import UserRole
@@ -38,13 +42,11 @@ class RevokeAdminInteractor:
3842
def __init__(
3943
self,
4044
current_user_service: CurrentUserService,
41-
authorization_service: AuthorizationService,
4245
user_command_gateway: UserCommandGateway,
4346
user_service: UserService,
4447
transaction_manager: TransactionManager,
4548
):
4649
self._current_user_service = current_user_service
47-
self._authorization_service = authorization_service
4850
self._user_command_gateway = user_command_gateway
4951
self._user_service = user_service
5052
self._transaction_manager = transaction_manager
@@ -56,9 +58,13 @@ async def __call__(self, request_data: RevokeAdminRequest) -> None:
5658
)
5759

5860
current_user = await self._current_user_service.get_current_user()
59-
self._authorization_service.authorize_for_subordinate_role(
60-
current_user.role,
61-
target_role=UserRole.ADMIN,
61+
62+
authorize(
63+
CanManageRole(),
64+
context=RoleManagementContext(
65+
subject=current_user,
66+
target_role=UserRole.ADMIN,
67+
),
6268
)
6369

6470
username = Username(request_data.username)

0 commit comments

Comments
 (0)