Skip to content

Commit 2cd6cb5

Browse files
committed
feat: Add dashboard admin feature
1 parent a64a384 commit 2cd6cb5

File tree

8 files changed

+106
-17
lines changed

8 files changed

+106
-17
lines changed

supertokens_python/framework/request.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,10 @@ def __init__(self):
2525
self.wrapper_used = True
2626
self.request = None
2727

28+
@abstractmethod
29+
def get_original_url(self) -> str:
30+
pass
31+
2832
@abstractmethod
2933
def get_query_param(
3034
self, key: str, default: Union[str, None] = None

supertokens_python/recipe/dashboard/__init__.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414

1515
from __future__ import annotations
1616

17-
from typing import Callable, Optional, Union
17+
from typing import Callable, Optional, List
1818

1919
from supertokens_python import AppInfo, RecipeModule
2020

@@ -26,10 +26,12 @@
2626

2727

2828
def init(
29-
api_key: Union[str, None] = None,
29+
api_key: Optional[str] = None,
30+
admins: Optional[List[str]] = None,
3031
override: Optional[InputOverrideConfig] = None,
3132
) -> Callable[[AppInfo], RecipeModule]:
3233
return DashboardRecipe.init(
3334
api_key,
35+
admins,
3436
override,
3537
)

supertokens_python/recipe/dashboard/api/api_key_protector.py

Lines changed: 22 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
# under the License.
1414
from __future__ import annotations
1515

16+
import json
1617
from typing import TYPE_CHECKING, Callable, Optional, Awaitable, Dict, Any
1718

1819
from supertokens_python.framework import BaseResponse
@@ -29,6 +30,8 @@
2930
send_non_200_response_with_message,
3031
)
3132

33+
from ..exceptions import DashboardOperationNotAllowedError
34+
3235

3336
async def api_key_protector(
3437
api_implementation: APIInterface,
@@ -39,9 +42,25 @@ async def api_key_protector(
3942
],
4043
user_context: Dict[str, Any],
4144
) -> Optional[BaseResponse]:
42-
should_allow_access = await api_options.recipe_implementation.should_allow_access(
43-
api_options.request, api_options.config, user_context
44-
)
45+
should_allow_access = False
46+
47+
try:
48+
should_allow_access = (
49+
await api_options.recipe_implementation.should_allow_access(
50+
api_options.request, api_options.config, user_context
51+
)
52+
)
53+
except DashboardOperationNotAllowedError as _:
54+
# api_options.response.set_status_code(403)
55+
# api_options.response.set_json_content({
56+
# "message": "You are not permitted to perform this operation"
57+
# })
58+
# return None
59+
return send_non_200_response_with_message(
60+
json.dumps({"message": "You are not permitted to perform this operation"}),
61+
403,
62+
api_options.response,
63+
)
4564

4665
if should_allow_access is False:
4766
return send_non_200_response_with_message(

supertokens_python/recipe/dashboard/exceptions.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,3 +3,7 @@
33

44
class SuperTokensDashboardError(SuperTokensError):
55
pass
6+
7+
8+
class DashboardOperationNotAllowedError(SuperTokensDashboardError):
9+
pass

supertokens_python/recipe/dashboard/recipe.py

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -87,12 +87,14 @@ def __init__(
8787
self,
8888
recipe_id: str,
8989
app_info: AppInfo,
90-
api_key: Union[str, None],
91-
override: Union[InputOverrideConfig, None] = None,
90+
api_key: Optional[str],
91+
admins: Optional[List[str]],
92+
override: Optional[InputOverrideConfig] = None,
9293
):
9394
super().__init__(recipe_id, app_info)
9495
self.config = validate_and_normalise_user_input(
9596
api_key,
97+
admins,
9698
override,
9799
)
98100
recipe_implementation = RecipeImplementation()
@@ -349,15 +351,17 @@ def get_all_cors_headers(self) -> List[str]:
349351

350352
@staticmethod
351353
def init(
352-
api_key: Union[str, None],
353-
override: Union[InputOverrideConfig, None] = None,
354+
api_key: Optional[str],
355+
admins: Optional[List[str]],
356+
override: Optional[InputOverrideConfig] = None,
354357
):
355358
def func(app_info: AppInfo):
356359
if DashboardRecipe.__instance is None:
357360
DashboardRecipe.__instance = DashboardRecipe(
358361
DashboardRecipe.recipe_id,
359362
app_info,
360363
api_key,
364+
admins,
361365
override,
362366
)
363367
return DashboardRecipe.__instance

supertokens_python/recipe/dashboard/recipe_implementation.py

Lines changed: 44 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -18,10 +18,16 @@
1818
from supertokens_python.constants import DASHBOARD_VERSION
1919
from supertokens_python.framework import BaseRequest
2020
from supertokens_python.normalised_url_path import NormalisedURLPath
21+
from supertokens_python.utils import log_debug_message
2122
from supertokens_python.querier import Querier
23+
from supertokens_python.recipe.dashboard.constants import (
24+
DASHBOARD_ANALYTICS_API,
25+
EMAIL_PASSSWORD_SIGNOUT,
26+
)
2227

2328
from .interfaces import RecipeInterface
2429
from .utils import DashboardConfig, validate_api_key
30+
from .exceptions import DashboardOperationNotAllowedError
2531

2632

2733
class RecipeImplementation(RecipeInterface):
@@ -34,9 +40,9 @@ async def should_allow_access(
3440
config: DashboardConfig,
3541
user_context: Dict[str, Any],
3642
) -> bool:
37-
if config.auth_mode == "email-password":
43+
# For cases where we're not using the API key, the JWT is being used; we allow their access by default
44+
if config.api_key is not None:
3845
auth_header_value = request.get_header("authorization")
39-
4046
if not auth_header_value:
4147
return False
4248

@@ -47,8 +53,40 @@ async def should_allow_access(
4753
{"sessionId": auth_header_value},
4854
)
4955
)
50-
return (
51-
"status" in session_verification_response
52-
and session_verification_response["status"] == "OK"
53-
)
56+
if session_verification_response.get("status") != "OK":
57+
return False
58+
59+
# For all non GET requests we also want to check if the
60+
# user is allowed to perform this operation
61+
if request.method() != "GET": # TODO: Use normalize http method?
62+
# We dont want to block the analytics API
63+
if request.get_original_url().startswith(DASHBOARD_ANALYTICS_API):
64+
return True
65+
66+
# We do not want to block the sign out request
67+
if request.get_original_url().endswith(EMAIL_PASSSWORD_SIGNOUT):
68+
return True
69+
70+
admins = config.admins
71+
72+
# If the user has provided no admins, allow
73+
if len(admins) == 0:
74+
return True
75+
76+
email_in_headers = request.get_header("email")
77+
78+
if email_in_headers is None:
79+
log_debug_message(
80+
"User Dashboard: Returniing OPERATION_NOT_ALLOWED because no email was provided in headers"
81+
)
82+
return False
83+
84+
if email_in_headers not in admins:
85+
log_debug_message(
86+
"User Dashboard: Throwing OPERATION_NOT_ALLOWED because user is not an admin"
87+
)
88+
raise DashboardOperationNotAllowedError()
89+
90+
return True
91+
5492
return validate_api_key(request, config, user_context)

supertokens_python/recipe/dashboard/utils.py

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@
4343
get_user_by_id as tppless_get_user_by_id,
4444
)
4545
from supertokens_python.types import User
46-
from supertokens_python.utils import Awaitable
46+
from supertokens_python.utils import Awaitable, log_debug_message, normalise_email
4747

4848
from ...normalised_url_path import NormalisedURLPath
4949
from .constants import (
@@ -181,24 +181,38 @@ def __init__(
181181

182182
class DashboardConfig:
183183
def __init__(
184-
self, api_key: Union[str, None], override: OverrideConfig, auth_mode: str
184+
self,
185+
api_key: Optional[str],
186+
admins: List[str],
187+
override: OverrideConfig,
188+
auth_mode: str,
185189
):
186190
self.api_key = api_key
191+
self.admins = admins
187192
self.override = override
188193
self.auth_mode = auth_mode
189194

190195

191196
def validate_and_normalise_user_input(
192197
# app_info: AppInfo,
193198
api_key: Union[str, None],
199+
admins: Optional[List[str]],
194200
override: Optional[InputOverrideConfig] = None,
195201
) -> DashboardConfig:
196202

197203
if override is None:
198204
override = InputOverrideConfig()
199205

206+
if api_key is not None and admins is not None:
207+
log_debug_message(
208+
"User Dashboard: Providing 'admins' has no effect when using an api key."
209+
)
210+
211+
admins = [normalise_email(a) for a in admins] if admins is not None else []
212+
200213
return DashboardConfig(
201214
api_key,
215+
admins,
202216
OverrideConfig(
203217
functions=override.functions,
204218
apis=override.apis,

supertokens_python/utils.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -360,3 +360,7 @@ def __exit__(self, exc_type: Any, exc_value: Any, traceback: Any):
360360

361361
if exc_type is not None:
362362
raise exc_type(exc_value).with_traceback(traceback)
363+
364+
365+
def normalise_email(email: str) -> str:
366+
return email.strip().lower()

0 commit comments

Comments
 (0)