1
+ import asyncio
1
2
import json
2
3
from abc import ABC , abstractmethod
3
4
from datetime import datetime
4
5
from enum import Enum
5
6
from functools import cached_property , partial
6
- from typing import Any , Literal , TypedDict , Union
7
+ from typing import Any , Literal , Optional , TypedDict , Union
7
8
8
9
from aiohttp import web
9
- from aiohttp_security import check_permission , permits
10
+ from aiohttp_security import authorized_userid , check_permission , permits
10
11
from pydantic import Json , parse_obj_as
11
12
13
+ from ..security import permissions_as_dict
14
+ from ..types import FieldState , InputState
15
+
12
16
Record = dict [str , object ]
13
17
14
18
@@ -25,16 +29,6 @@ def default(self, o: object) -> Any:
25
29
json_response = partial (web .json_response , dumps = partial (json .dumps , cls = Encoder ))
26
30
27
31
28
- class FieldState (TypedDict ):
29
- type : str
30
- props : dict [str , object ]
31
-
32
-
33
- class InputState (FieldState ):
34
- # Whether to show this input in the create form.
35
- show_create : bool
36
-
37
-
38
32
class _Pagination (TypedDict ):
39
33
page : int
40
34
perPage : int
@@ -89,10 +83,11 @@ class AbstractAdminResource(ABC):
89
83
repr_field : str
90
84
91
85
async def filter_by_permissions (self , request : web .Request , perm_type : str ,
92
- record : Record ) -> Record :
86
+ record : Record , original : Optional [ Record ] = None ) -> Record :
93
87
"""Return a filtered record containing permissible fields only."""
94
88
return {k : v for k , v in record .items ()
95
- if await permits (request , f"admin.{ self .name } .{ k } .{ perm_type } " )}
89
+ if await permits (request , f"admin.{ self .name } .{ k } .{ perm_type } " ,
90
+ context = original or record )}
96
91
97
92
@abstractmethod
98
93
async def get_list (self , params : GetListParams ) -> tuple [list [Record ], int ]:
@@ -128,15 +123,29 @@ async def _get_list(self, request: web.Request) -> web.Response:
128
123
await check_permission (request , f"admin.{ self .name } .view" )
129
124
query = parse_obj_as (GetListParams , request .query )
130
125
126
+ # Add filters from advanced permissions.
127
+ if request .app ["identity_callback" ]:
128
+ identity = await authorized_userid (request )
129
+ user_details = await request .app ["identity_callback" ](identity )
130
+ permissions = permissions_as_dict (user_details ["permissions" ])
131
+ filters = permissions .get (f"admin.{ self .name } .view" ,
132
+ permissions .get (f"admin.{ self .name } .*" , {}))
133
+ for k , v in filters .items ():
134
+ query ["filter" ][k ] = v
135
+
131
136
results , total = await self .get_list (query )
132
137
results = [await self .filter_by_permissions (request , "view" , r ) for r in results ]
138
+ results = [r for r in results if await permits (request , f"admin.{ self .name } .view" ,
139
+ context = r )]
133
140
return json_response ({"data" : results , "total" : total })
134
141
135
142
async def _get_one (self , request : web .Request ) -> web .Response :
136
143
await check_permission (request , f"admin.{ self .name } .view" )
137
144
query = parse_obj_as (GetOneParams , request .query )
138
145
139
146
result = await self .get_one (query )
147
+ if not await permits (request , f"admin.{ self .name } .view" , context = result ):
148
+ raise web .HTTPForbidden ()
140
149
result = await self .filter_by_permissions (request , "view" , result )
141
150
return json_response ({"data" : result })
142
151
@@ -145,14 +154,17 @@ async def _get_many(self, request: web.Request) -> web.Response:
145
154
query = parse_obj_as (GetManyParams , request .query )
146
155
147
156
results = await self .get_many (query )
148
- results = [await self .filter_by_permissions (request , "view" , r ) for r in results ]
157
+ results = [await self .filter_by_permissions (request , "view" , r ) for r in results
158
+ if await permits (request , f"admin.{ self .name } .view" , context = r )]
149
159
return json_response ({"data" : results })
150
160
151
161
async def _create (self , request : web .Request ) -> web .Response :
152
- await check_permission (request , f"admin.{ self .name } .add" )
153
162
query = parse_obj_as (CreateParams , request .query )
154
- for k in query ["data" ]:
155
- await check_permission (request , f"admin.{ self .name } .{ k } .add" )
163
+ await check_permission (request , f"admin.{ self .name } .add" , context = query ["data" ])
164
+ for k , v in query ["data" ].items ():
165
+ if v is not None :
166
+ await check_permission (request , f"admin.{ self .name } .{ k } .add" ,
167
+ context = query ["data" ])
156
168
157
169
result = await self .create (query )
158
170
result = await self .filter_by_permissions (request , "view" , result )
@@ -161,8 +173,22 @@ async def _create(self, request: web.Request) -> web.Response:
161
173
async def _update (self , request : web .Request ) -> web .Response :
162
174
await check_permission (request , f"admin.{ self .name } .edit" )
163
175
query = parse_obj_as (UpdateParams , request .query )
164
- # Filter because react-admin still sends fields without an input component.
165
- query ["data" ] = await self .filter_by_permissions (request , "edit" , query ["data" ])
176
+
177
+ # Check original record is allowed by permission filters.
178
+ original = await self .get_one ({"id" : query ["id" ]})
179
+ if not await permits (request , f"admin.{ self .name } .edit" , context = original ):
180
+ raise web .HTTPForbidden ()
181
+
182
+ # Filter rather than forbid because react-admin still sends fields without an
183
+ # input component. The query may not be the complete dict though, so we must
184
+ # pass original for testing.
185
+ query ["data" ] = await self .filter_by_permissions (request , "edit" , query ["data" ], original )
186
+ # Check new values are allowed by permission filters.
187
+ if not await permits (request , f"admin.{ self .name } .edit" , context = query ["data" ]):
188
+ raise web .HTTPForbidden ()
189
+
190
+ if not query ["data" ]:
191
+ raise web .HTTPBadRequest (reason = "No allowed fields to change." )
166
192
167
193
result = await self .update (query )
168
194
result = await self .filter_by_permissions (request , "view" , result )
@@ -172,6 +198,10 @@ async def _delete(self, request: web.Request) -> web.Response:
172
198
await check_permission (request , f"admin.{ self .name } .delete" )
173
199
query = parse_obj_as (DeleteParams , request .query )
174
200
201
+ original = await self .get_one ({"id" : query ["id" ]})
202
+ if not await permits (request , f"admin.{ self .name } .delete" , context = original ):
203
+ raise web .HTTPForbidden ()
204
+
175
205
result = await self .delete (query )
176
206
result = await self .filter_by_permissions (request , "view" , result )
177
207
return json_response ({"data" : result })
@@ -180,6 +210,12 @@ async def _delete_many(self, request: web.Request) -> web.Response:
180
210
await check_permission (request , f"admin.{ self .name } .delete" )
181
211
query = parse_obj_as (DeleteManyParams , request .query )
182
212
213
+ originals = await self .get_many (query )
214
+ allowed = await asyncio .gather (* (permits (request , f"admin.{ self .name } .delete" ,
215
+ context = r ) for r in originals ))
216
+ if not all (allowed ):
217
+ raise web .HTTPForbidden ()
218
+
183
219
ids = await self .delete_many (query )
184
220
return json_response ({"data" : ids })
185
221
0 commit comments