1+ import asyncio
12import json
23from abc import ABC , abstractmethod
34from datetime import datetime
45from enum import Enum
56from functools import cached_property , partial
6- from typing import Any , Literal , TypedDict , Union
7+ from typing import Any , Literal , Optional , TypedDict , Union
78
89from aiohttp import web
9- from aiohttp_security import check_permission , permits
10+ from aiohttp_security import authorized_userid , check_permission , permits
1011from pydantic import Json , parse_obj_as
1112
13+ from ..security import permissions_as_dict
14+ from ..types import FieldState , InputState
15+
1216Record = dict [str , object ]
1317
1418
@@ -25,16 +29,6 @@ def default(self, o: object) -> Any:
2529json_response = partial (web .json_response , dumps = partial (json .dumps , cls = Encoder ))
2630
2731
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-
3832class _Pagination (TypedDict ):
3933 page : int
4034 perPage : int
@@ -89,10 +83,11 @@ class AbstractAdminResource(ABC):
8983 repr_field : str
9084
9185 async def filter_by_permissions (self , request : web .Request , perm_type : str ,
92- record : Record ) -> Record :
86+ record : Record , original : Optional [ Record ] = None ) -> Record :
9387 """Return a filtered record containing permissible fields only."""
9488 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 )}
9691
9792 @abstractmethod
9893 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:
128123 await check_permission (request , f"admin.{ self .name } .view" )
129124 query = parse_obj_as (GetListParams , request .query )
130125
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+
131136 results , total = await self .get_list (query )
132137 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 )]
133140 return json_response ({"data" : results , "total" : total })
134141
135142 async def _get_one (self , request : web .Request ) -> web .Response :
136143 await check_permission (request , f"admin.{ self .name } .view" )
137144 query = parse_obj_as (GetOneParams , request .query )
138145
139146 result = await self .get_one (query )
147+ if not await permits (request , f"admin.{ self .name } .view" , context = result ):
148+ raise web .HTTPForbidden ()
140149 result = await self .filter_by_permissions (request , "view" , result )
141150 return json_response ({"data" : result })
142151
@@ -145,14 +154,17 @@ async def _get_many(self, request: web.Request) -> web.Response:
145154 query = parse_obj_as (GetManyParams , request .query )
146155
147156 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 )]
149159 return json_response ({"data" : results })
150160
151161 async def _create (self , request : web .Request ) -> web .Response :
152- await check_permission (request , f"admin.{ self .name } .add" )
153162 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" ])
156168
157169 result = await self .create (query )
158170 result = await self .filter_by_permissions (request , "view" , result )
@@ -161,8 +173,22 @@ async def _create(self, request: web.Request) -> web.Response:
161173 async def _update (self , request : web .Request ) -> web .Response :
162174 await check_permission (request , f"admin.{ self .name } .edit" )
163175 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." )
166192
167193 result = await self .update (query )
168194 result = await self .filter_by_permissions (request , "view" , result )
@@ -172,6 +198,10 @@ async def _delete(self, request: web.Request) -> web.Response:
172198 await check_permission (request , f"admin.{ self .name } .delete" )
173199 query = parse_obj_as (DeleteParams , request .query )
174200
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+
175205 result = await self .delete (query )
176206 result = await self .filter_by_permissions (request , "view" , result )
177207 return json_response ({"data" : result })
@@ -180,6 +210,12 @@ async def _delete_many(self, request: web.Request) -> web.Response:
180210 await check_permission (request , f"admin.{ self .name } .delete" )
181211 query = parse_obj_as (DeleteManyParams , request .query )
182212
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+
183219 ids = await self .delete_many (query )
184220 return json_response ({"data" : ids })
185221
0 commit comments