From bb3aa1fd8580d6dd296367281089cb684f06e6b2 Mon Sep 17 00:00:00 2001 From: Stefano Pigozzi Date: Fri, 7 Feb 2020 15:36:19 +0100 Subject: [PATCH] API login routes completed --- royalnet/backpack/stars/__init__.py | 6 ++-- ...oyalnet_login.py => api_login_royalnet.py} | 8 ++--- .../backpack/stars/api_royalnet_version.py | 2 +- royalnet/backpack/stars/api_token_info.py | 13 ++++++++ royalnet/backpack/tables/aliases.py | 7 +++-- royalnet/backpack/tables/tokens.py | 6 ++++ royalnet/commands/commanddata.py | 1 - royalnet/constellation/api/__init__.py | 10 +++--- royalnet/constellation/api/apidata.py | 31 +++++++++++++++++++ royalnet/constellation/api/apidatadict.py | 6 ---- royalnet/constellation/api/apierrors.py | 14 +++++++-- royalnet/constellation/api/apistar.py | 12 ++++--- royalnet/constellation/api/jsonapi.py | 3 +- 13 files changed, 89 insertions(+), 30 deletions(-) rename royalnet/backpack/stars/{api_royalnet_login.py => api_login_royalnet.py} (83%) create mode 100644 royalnet/backpack/stars/api_token_info.py create mode 100644 royalnet/constellation/api/apidata.py delete mode 100644 royalnet/constellation/api/apidatadict.py diff --git a/royalnet/backpack/stars/__init__.py b/royalnet/backpack/stars/__init__.py index 4ca5f3fa..b9fbd55a 100644 --- a/royalnet/backpack/stars/__init__.py +++ b/royalnet/backpack/stars/__init__.py @@ -1,12 +1,14 @@ # Imports go here! from .api_royalnet_version import ApiRoyalnetVersionStar -from .api_royalnet_login import ApiRoyalnetLoginStar +from .api_login_royalnet import ApiLoginRoyalnetStar +from .api_token_info import ApiTokenInfoStar # Enter the PageStars of your Pack here! available_page_stars = [ ApiRoyalnetVersionStar, - ApiRoyalnetLoginStar, + ApiLoginRoyalnetStar, + ApiTokenInfoStar, ] # Don't change this, it should automatically generate __all__ diff --git a/royalnet/backpack/stars/api_royalnet_login.py b/royalnet/backpack/stars/api_login_royalnet.py similarity index 83% rename from royalnet/backpack/stars/api_royalnet_login.py rename to royalnet/backpack/stars/api_login_royalnet.py index c20d14b3..a8b03e4c 100644 --- a/royalnet/backpack/stars/api_royalnet_login.py +++ b/royalnet/backpack/stars/api_login_royalnet.py @@ -6,10 +6,10 @@ from ..tables.aliases import Alias from ..tables.tokens import Token -class ApiRoyalnetLoginStar(ApiStar): - path = "/api/royalnet/login/v1" +class ApiLoginRoyalnetStar(ApiStar): + path = "/api/login/royalnet/v1" - async def api(self, data: ApiDataDict) -> dict: + async def api(self, data: ApiData) -> dict: TokenT = self.alchemy.get(Token) UserT = self.alchemy.get(User) AliasT = self.alchemy.get(Alias) @@ -20,7 +20,7 @@ class ApiRoyalnetLoginStar(ApiStar): async with self.session_acm() as session: user: User = await ru.asyncify(session.query(UserT).filter_by(username=username).one_or_none) if user is None: - raise NotFoundException("User not found") + raise NotFoundError("User not found") pswd_check = user.test_password(password) if not pswd_check: raise ApiError("Invalid password") diff --git a/royalnet/backpack/stars/api_royalnet_version.py b/royalnet/backpack/stars/api_royalnet_version.py index de77030a..65a17425 100644 --- a/royalnet/backpack/stars/api_royalnet_version.py +++ b/royalnet/backpack/stars/api_royalnet_version.py @@ -5,7 +5,7 @@ from royalnet.constellation.api import * class ApiRoyalnetVersionStar(ApiStar): path = "/api/royalnet/version/v1" - async def api(self, data: ApiDataDict) -> dict: + async def api(self, data: ApiData) -> dict: return { "semantic": rv.semantic } diff --git a/royalnet/backpack/stars/api_token_info.py b/royalnet/backpack/stars/api_token_info.py new file mode 100644 index 00000000..3a5e888f --- /dev/null +++ b/royalnet/backpack/stars/api_token_info.py @@ -0,0 +1,13 @@ +import datetime +import royalnet.utils as ru +from royalnet.constellation.api import * +from ..tables.users import User +from ..tables.aliases import Alias + + +class ApiTokenInfoStar(ApiStar): + path = "/api/token/info/v1" + + async def api(self, data: ApiData) -> dict: + token = await data.token() + return token.json() diff --git a/royalnet/backpack/tables/aliases.py b/royalnet/backpack/tables/aliases.py index 544c78f3..d153698a 100644 --- a/royalnet/backpack/tables/aliases.py +++ b/royalnet/backpack/tables/aliases.py @@ -4,6 +4,7 @@ from sqlalchemy import Column, \ ForeignKey from sqlalchemy.orm import relationship from sqlalchemy.ext.declarative import declared_attr +import royalnet.utils as ru class Alias: @@ -22,10 +23,10 @@ class Alias: return relationship("User", backref="aliases") @classmethod - def find_user(cls, alchemy, session, alias: str): - result = session.query(alchemy.get(cls)).filter_by(alias=alias.lower()).one_or_none() + async def find_user(cls, alchemy, session, alias: str): + result = await ru.asyncify(session.query(alchemy.get(cls)).filter_by(alias=alias.lower()).one_or_none) if result is not None: - result = result.royal + result = result.user return result def __init__(self, user: str, alias: str): diff --git a/royalnet/backpack/tables/tokens.py b/royalnet/backpack/tables/tokens.py index 7b332f84..acada45e 100644 --- a/royalnet/backpack/tables/tokens.py +++ b/royalnet/backpack/tables/tokens.py @@ -3,6 +3,8 @@ import secrets from sqlalchemy import * from sqlalchemy.orm import * from sqlalchemy.ext.declarative import declared_attr +import royalnet.utils as ru +from .users import User class Token: @@ -40,3 +42,7 @@ class Token: "token": self.token, "expiration": self.expiration.isoformat() } + + @classmethod + async def authenticate(cls, alchemy, session, token: str) -> "Token": + return await ru.asyncify(session.query(alchemy.get(cls)).filter_by(token=token).one_or_none) diff --git a/royalnet/commands/commanddata.py b/royalnet/commands/commanddata.py index 4b966ca2..fa7b1956 100644 --- a/royalnet/commands/commanddata.py +++ b/royalnet/commands/commanddata.py @@ -75,7 +75,6 @@ class CommandData: Alias.find_user(self._interface.alchemy, self.session, alias) ) - @contextlib.asynccontextmanager async def keyboard(self, text, keys: List["KeyboardKey"]): yield diff --git a/royalnet/constellation/api/__init__.py b/royalnet/constellation/api/__init__.py index a10f1f7d..bff3af0b 100644 --- a/royalnet/constellation/api/__init__.py +++ b/royalnet/constellation/api/__init__.py @@ -1,7 +1,7 @@ from .apistar import ApiStar from .jsonapi import api_response, api_success, api_error -from .apidatadict import ApiDataDict -from .apierrors import ApiError, MissingParameterException, NotFoundException, UnauthorizedException +from .apidata import ApiData +from .apierrors import ApiError, MissingParameterError, NotFoundError, ForbiddenError __all__ = [ @@ -9,8 +9,8 @@ __all__ = [ "api_response", "api_success", "api_error", - "ApiDataDict", + "ApiData", "ApiError", - "MissingParameterException", - "NotFoundException", + "MissingParameterError", + "NotFoundError", ] diff --git a/royalnet/constellation/api/apidata.py b/royalnet/constellation/api/apidata.py new file mode 100644 index 00000000..c20fee4f --- /dev/null +++ b/royalnet/constellation/api/apidata.py @@ -0,0 +1,31 @@ +from .apierrors import MissingParameterError +from royalnet.backpack.tables.tokens import Token +from royalnet.backpack.tables.users import User +from .apierrors import * + + +class ApiData(dict): + def __init__(self, data, star): + super().__init__(data) + self.star = star + self._session = None + + def __missing__(self, key): + raise MissingParameterError(f"Missing '{key}'") + + async def token(self) -> Token: + token = await Token.authenticate(self.star.alchemy, self.session, self["token"]) + if token is None: + raise ForbiddenError("'token' is invalid") + return token + + async def user(self) -> Token: + return (await self.token()).user + + @property + def session(self): + if self._session is None: + if self.star.alchemy is None: + raise UnsupportedError("'alchemy' is not enabled on this Royalnet instance") + self._session = self.star.alchemy.Session() + return self._session diff --git a/royalnet/constellation/api/apidatadict.py b/royalnet/constellation/api/apidatadict.py deleted file mode 100644 index f9c7d217..00000000 --- a/royalnet/constellation/api/apidatadict.py +++ /dev/null @@ -1,6 +0,0 @@ -from .apierrors import MissingParameterException - - -class ApiDataDict(dict): - def __missing__(self, key): - raise MissingParameterException(f"Missing '{key}'") diff --git a/royalnet/constellation/api/apierrors.py b/royalnet/constellation/api/apierrors.py index 040ccc7b..b09fadeb 100644 --- a/royalnet/constellation/api/apierrors.py +++ b/royalnet/constellation/api/apierrors.py @@ -2,13 +2,21 @@ class ApiError(Exception): pass -class NotFoundException(ApiError): +class NotFoundError(ApiError): pass -class UnauthorizedException(ApiError): +class ForbiddenError(ApiError): pass -class MissingParameterException(ApiError): +class MissingParameterError(ApiError): + pass + + +class NotImplementedError(ApiError): + pass + + +class UnsupportedError(NotImplementedError): pass diff --git a/royalnet/constellation/api/apistar.py b/royalnet/constellation/api/apistar.py index 1d7b33e2..d6ea4770 100644 --- a/royalnet/constellation/api/apistar.py +++ b/royalnet/constellation/api/apistar.py @@ -5,8 +5,8 @@ from starlette.requests import Request from starlette.responses import JSONResponse from ..pagestar import PageStar from .jsonapi import api_error, api_success -from .apidatadict import ApiDataDict -from .apierrors import ApiError, NotFoundException +from .apidata import ApiData +from .apierrors import * class ApiStar(PageStar, ABC): @@ -19,9 +19,13 @@ class ApiStar(PageStar, ABC): except JSONDecodeError: data = {} try: - response = await self.api(ApiDataDict(data)) - except NotFoundException as e: + response = await self.api(ApiData(data, self)) + except NotFoundError as e: return api_error(e, code=404) + except ForbiddenError as e: + return api_error(e, code=403) + except NotImplementedError as e: + return api_error(e, code=501) except ApiError as e: return api_error(e, code=400) except Exception as e: diff --git a/royalnet/constellation/api/jsonapi.py b/royalnet/constellation/api/jsonapi.py index e46b3d28..da540729 100644 --- a/royalnet/constellation/api/jsonapi.py +++ b/royalnet/constellation/api/jsonapi.py @@ -27,6 +27,7 @@ def api_error(error: Exception, code: int = 500) -> JSONResponse: result = { "success": False, "error_type": error.__class__.__qualname__, - "error_args": list(error.args) + "error_args": list(error.args), + "error_code": code, } return api_response(result, code=code)