11import functools
2- from typing import Optional , Type , TypeVar , Union
2+ from typing import Callable , Optional , Type , TypeVar , Union
33
44import aiohttp
55from loguru import logger
66from pydantic import BaseModel , Extra , Field , parse_obj_as
77
88from ..config import PermitConfig
99from ..exceptions import PermitContextError , handle_api_error , handle_client_error
10- from .context import API_ACCESS_LEVELS , ApiKeyLevel
10+ from .context import API_ACCESS_LEVELS , ApiContextLevel , ApiKeyAccessLevel
1111from .models import APIKeyScopeRead
1212
13+ T = TypeVar ("T" , bound = Callable )
14+ TModel = TypeVar ("TModel" , bound = BaseModel )
15+ TData = TypeVar ("TData" , bound = BaseModel )
1316
14- class ClientConfig (BaseModel ):
15- class Config :
16- extra = Extra .allow
1717
18- base_url : str = Field (
19- ...,
20- description = "base url that will prefix the url fragment sent via the client" ,
21- )
22- headers : dict = Field (..., description = "http headers sent to the API server" )
18+ def required_permissions (access_level : ApiKeyAccessLevel ):
19+ def decorator (func : T ) -> T :
20+ @functools .wraps (func )
21+ async def wrapped (self : BasePermitApi , * args , ** kwargs ):
22+ await self ._ensure_access_level (access_level )
23+ return await func (self , * args , ** kwargs )
24+
25+ return wrapped
26+
27+ return decorator
2328
2429
25- def ensure_context ( call_level : ApiKeyLevel ):
30+ def required_context ( context : ApiContextLevel ):
2631 """
2732 a decorator that ensures that an API endpoint is called only after the SDK has initialized
2833 an API context (authorization level) by inferring it from the API key or manually by the user.
@@ -34,10 +39,10 @@ def ensure_context(call_level: ApiKeyLevel):
3439 PermitContextError: If the API context does not match the required endpoint context.
3540 """
3641
37- def decorator (func ) :
42+ def decorator (func : T ) -> T :
3843 @functools .wraps (func )
3944 async def wrapped (self : BasePermitApi , * args , ** kwargs ):
40- await self .ensure_context ( call_level )
45+ await self ._ensure_context ( context )
4146 return await func (self , * args , ** kwargs )
4247
4348 return wrapped
@@ -49,8 +54,15 @@ def pagination_params(page: int, per_page: int) -> dict:
4954 return {"page" : page , "per_page" : per_page }
5055
5156
52- TModel = TypeVar ("TModel" , bound = BaseModel )
53- TData = TypeVar ("TData" , bound = BaseModel )
57+ class ClientConfig (BaseModel ):
58+ class Config :
59+ extra = Extra .allow
60+
61+ base_url : str = Field (
62+ ...,
63+ description = "base url that will prefix the url fragment sent via the client" ,
64+ )
65+ headers : dict = Field (..., description = "http headers sent to the API server" )
5466
5567
5668class SimpleHttpClient :
@@ -206,76 +218,110 @@ def _build_http_client(self, endpoint_url: str = "", **kwargs):
206218
207219 async def _set_context_from_api_key (self ) -> None :
208220 """
209- Set the API context based on the API key scope.
221+ Set the API context and permitted access level based on the API key scope.
210222 """
223+ logger .debug ("Fetching api key scope" )
211224 scope = await self .__api_keys .get ("/scope" , model = APIKeyScopeRead )
212225
213226 if scope .organization_id is not None :
227+ # saves the permitted access level by that api key
228+ self .config .api_context ._save_api_key_accessible_scope (
229+ org = str (scope .organization_id ),
230+ project = (
231+ str (scope .project_id ) if scope .project_id is not None else None
232+ ),
233+ environment = (
234+ str (scope .environment_id )
235+ if scope .environment_id is not None
236+ else None
237+ ),
238+ )
239+
214240 if scope .project_id is not None :
215241 if scope .environment_id is not None :
216242 # Set environment level context
217243 self .config .api_context .set_environment_level_context (
218- scope .organization_id , scope .project_id , scope .environment_id
244+ str (scope .organization_id ),
245+ str (scope .project_id ),
246+ str (scope .environment_id ),
219247 )
220248 return
221249
222250 # Set project level context
223251 self .config .api_context .set_project_level_context (
224- scope .organization_id , scope .project_id
252+ str ( scope .organization_id ), str ( scope .project_id )
225253 )
226254 return
227255
228256 # Set org level context
229257 self .config .api_context .set_organization_level_context (
230- scope .organization_id
258+ str ( scope .organization_id )
231259 )
232260 return
233261
234262 raise PermitContextError ("Could not set API context level" )
235263
236- async def ensure_context (self , call_level : ApiKeyLevel ) -> None :
264+ async def _ensure_access_level (
265+ self , required_access_level : ApiKeyAccessLevel
266+ ) -> None :
237267 """
238- Ensure that the API context matches the required endpoint context.
268+ Ensure that the API Key has the necessary permissions to successfully call the API endpoint.
269+
270+ Note that this check is not full proof, and the API may still throw 401.
239271
240272 Args:
241- call_level : The required API key level for the endpoint.
273+ required_access_level : The required API Key Access level for the endpoint.
242274
243275 Raises:
244- PermitContextError: If the API context does not match the required endpoint context .
276+ PermitContextError: If the currently set API key access level does not match the required access level .
245277 """
246- if self .config .api_context .level == ApiKeyLevel .WAIT_FOR_INIT :
278+ # should only happen once in the lifetime of the sdk
279+ if (
280+ self .config .api_context .level == ApiContextLevel .WAIT_FOR_INIT
281+ or self .config .api_context .permitted_access_level
282+ == ApiKeyAccessLevel .WAIT_FOR_INIT
283+ ):
247284 await self ._set_context_from_api_key ()
248285
249- if call_level != self .config .api_context .level :
250- if API_ACCESS_LEVELS .index (call_level ) < API_ACCESS_LEVELS .index (
251- self .config .api_context .level
286+ if required_access_level != self .config .api_context .permitted_access_level :
287+ if API_ACCESS_LEVELS .index (required_access_level ) < API_ACCESS_LEVELS .index (
288+ self .config .api_context .permitted_access_level
252289 ):
253290 raise PermitContextError (
254- f"You're trying to use an SDK method that requires an API Key with level: { call_level } , "
255- + f"however the SDK is running with an API key with level { self .config .api_context .level } ."
291+ f"You're trying to use an SDK method that requires an API Key with access level: { required_access_level } , "
292+ + f"however the SDK is running with an API key with level { self .config .api_context .permitted_access_level } ."
256293 )
257294 return
258295
259296 if (
260- call_level == ApiKeyLevel . PROJECT_LEVEL_API_KEY
261- and self . config . api_context . project is None
297+ self . config . api_context . permitted_access_level . value
298+ < required_access_level . value
262299 ):
263300 raise PermitContextError (
264- "You're trying to use an SDK method that's specific to a project, "
265- + "but you haven't set the current project in your client's context yet, "
266- + "or you are using an organization level API key. "
267- + "Please set the context to a specific "
268- + "project using `permit.set_context()` method."
301+ f"You're trying to use an SDK method that requires an api context of { required_context .name } , "
302+ + f"however the SDK is running in a less specific context level: { self .config .api_context .level } ."
269303 )
270304
271- if call_level == ApiKeyLevel .ENVIRONMENT_LEVEL_API_KEY and (
272- self .config .api_context .project is None
273- or self .config .api_context .environment is None
305+ async def _ensure_context (self , required_context : ApiContextLevel ) -> None :
306+ """
307+ Ensure that the API context matches the required endpoint context.
308+
309+ Args:
310+ context: The required API context level for the endpoint.
311+
312+ Raises:
313+ PermitContextError: If the currently set API context level does not match the required context level.
314+ """
315+ # should only happen once in the lifetime of the sdk
316+ if (
317+ self .config .api_context .level == ApiContextLevel .WAIT_FOR_INIT
318+ or self .config .api_context .permitted_access_level
319+ == ApiKeyAccessLevel .WAIT_FOR_INIT
274320 ):
321+ await self ._set_context_from_api_key ()
322+
323+ if self .config .api_context .level .value < required_context .value :
275324 raise PermitContextError (
276- "You're trying to use an SDK method that's specific to an environment, "
277- + "but you haven't set the current environment in your client's context yet, "
278- + "or you are using an organization/project level API key. "
279- + "Please set the context to a specific "
280- + "environment using `permit.set_context()` method."
325+ f"You're trying to use an SDK method that requires an api context of { required_context .name } , "
326+ + f"however the SDK is running in a less specific context level: { self .config .api_context .level } ."
281327 )
0 commit comments