1
Fork 0
mirror of https://github.com/RYGhub/royalnet.git synced 2024-11-27 13:34:28 +00:00

Do some token things

This commit is contained in:
Steffo 2020-02-07 16:47:52 +01:00
parent bb3aa1fd85
commit 56cbd5b5c6
11 changed files with 138 additions and 18 deletions

View file

@ -1,4 +1,4 @@
from typing import Set, Dict, Union from typing import *
from contextlib import contextmanager, asynccontextmanager from contextlib import contextmanager, asynccontextmanager
from royalnet.utils import asyncify from royalnet.utils import asyncify
from royalnet.alchemy.errors import TableNotFoundError from royalnet.alchemy.errors import TableNotFoundError
@ -6,7 +6,7 @@ from sqlalchemy import create_engine
from sqlalchemy.engine import Engine from sqlalchemy.engine import Engine
from sqlalchemy.schema import Table from sqlalchemy.schema import Table
from sqlalchemy.ext.declarative import declarative_base from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.ext.declarative.api import DeclarativeMeta from sqlalchemy.ext.declarative.api import DeclarativeMeta, AbstractConcreteBase
from sqlalchemy.orm import sessionmaker from sqlalchemy.orm import sessionmaker
@ -26,7 +26,7 @@ class Alchemy:
raise NotImplementedError("sqlite databases aren't supported, as they can't be used in multithreaded" raise NotImplementedError("sqlite databases aren't supported, as they can't be used in multithreaded"
" applications") " applications")
self._engine: Engine = create_engine(database_uri) self._engine: Engine = create_engine(database_uri)
self._Base: DeclarativeMeta = declarative_base(bind=self._engine) self._Base = declarative_base(bind=self._engine)
self.Session: sessionmaker = sessionmaker(bind=self._engine) self.Session: sessionmaker = sessionmaker(bind=self._engine)
self._tables: Dict[str, Table] = {} self._tables: Dict[str, Table] = {}
for table in tables: for table in tables:
@ -38,7 +38,7 @@ class Alchemy:
self._tables[name] = bound_table self._tables[name] = bound_table
self._Base.metadata.create_all() self._Base.metadata.create_all()
def get(self, table: Union[str, type]) -> Table: def get(self, table: Union[str, type]) -> DeclarativeMeta:
"""Get the table with a specified name or class. """Get the table with a specified name or class.
Args: Args:

View file

@ -2,13 +2,16 @@
from .api_royalnet_version import ApiRoyalnetVersionStar from .api_royalnet_version import ApiRoyalnetVersionStar
from .api_login_royalnet import ApiLoginRoyalnetStar from .api_login_royalnet import ApiLoginRoyalnetStar
from .api_token_info import ApiTokenInfoStar from .api_token_info import ApiTokenInfoStar
from .api_token_passwd import ApiTokenPasswdStar
from .api_token_create import ApiTokenCreateStar
# Enter the PageStars of your Pack here! # Enter the PageStars of your Pack here!
available_page_stars = [ available_page_stars = [
ApiRoyalnetVersionStar, ApiRoyalnetVersionStar,
ApiLoginRoyalnetStar, ApiLoginRoyalnetStar,
ApiTokenInfoStar, ApiTokenInfoStar,
ApiTokenPasswdStar,
ApiTokenCreateStar,
] ]
# Don't change this, it should automatically generate __all__ # Don't change this, it should automatically generate __all__

View file

@ -0,0 +1,21 @@
from typing import *
import datetime
import royalnet.utils as ru
from royalnet.constellation.api import *
from ..tables.tokens import Token
from sqlalchemy import and_
class ApiTokenCreateStar(ApiStar):
path = "/api/token/create/v1"
async def api(self, data: ApiData) -> dict:
user = await data.user()
try:
duration = int(data["duration"])
except ValueError:
raise InvalidParameterError("Duration is not a valid integer")
new_token = Token.generate(self.alchemy, user, datetime.timedelta(seconds=duration))
data.session.add(new_token)
await data.session_commit()
return new_token.json()

View file

@ -0,0 +1,33 @@
from typing import *
import datetime
import royalnet.utils as ru
from royalnet.constellation.api import *
from sqlalchemy import and_
from ..tables.tokens import Token
class ApiTokenPasswdStar(ApiStar):
path = "/api/token/passwd/v1"
async def api(self, data: ApiData) -> dict:
TokenT = self.alchemy.get(Token)
token = await data.token()
user = token.user
user.set_password(data["new_password"])
tokens: List[Token] = await ru.asyncify(
data.session
.query(self.alchemy.get(Token))
.filter(
and_(
TokenT.user == user,
TokenT.expiration >= datetime.datetime.now()
))
.all
)
for t in tokens:
if t.token != token.token:
t.expired = True
await data.session_commit()
return {
"revoked_tokens": len(tokens) - 1
}

View file

@ -4,7 +4,6 @@ from sqlalchemy import *
from sqlalchemy.orm import * from sqlalchemy.orm import *
from sqlalchemy.ext.declarative import declared_attr from sqlalchemy.ext.declarative import declared_attr
import royalnet.utils as ru import royalnet.utils as ru
from .users import User
class Token: class Token:
@ -30,10 +29,18 @@ class Token:
def expired(self): def expired(self):
return datetime.datetime.now() > self.expiration return datetime.datetime.now() > self.expiration
@expired.setter
def expired(self, value):
if value is True:
self.expiration = datetime.datetime.fromtimestamp(0)
else:
raise ValueError("'expired' can only be set to True.")
@classmethod @classmethod
def generate(cls, user, expiration_delta: datetime.timedelta): def generate(cls, alchemy, user, expiration_delta: datetime.timedelta):
# noinspection PyArgumentList # noinspection PyArgumentList
token = cls(user=user, expiration=datetime.datetime.now() + expiration_delta, token=secrets.token_urlsafe()) TokenT = alchemy.get(cls)
token = TokenT(user=user, expiration=datetime.datetime.now() + expiration_delta, token=secrets.token_urlsafe())
return token return token
def json(self) -> dict: def json(self) -> dict:

View file

@ -26,6 +26,7 @@ class CommandData:
if self._session is None: if self._session is None:
if self._interface.alchemy is None: if self._interface.alchemy is None:
raise UnsupportedError("'alchemy' is not enabled on this Royalnet instance") raise UnsupportedError("'alchemy' is not enabled on this Royalnet instance")
log.debug("Creating Session...")
self._session = self._interface.alchemy.Session() self._session = self._interface.alchemy.Session()
return self._session return self._session
@ -34,11 +35,13 @@ class CommandData:
if self._session: if self._session:
log.warning("Session had to be created to be committed") log.warning("Session had to be created to be committed")
# noinspection PyUnresolvedReferences # noinspection PyUnresolvedReferences
log.debug("Committing Session...")
await ru.asyncify(self.session.commit) await ru.asyncify(self.session.commit)
async def session_close(self): async def session_close(self):
"""Asyncronously close the :attr:`.session` of this object.""" """Asyncronously close the :attr:`.session` of this object."""
if self._session is not None: if self._session is not None:
log.debug("Closing Session...")
await ru.asyncify(self._session.close) await ru.asyncify(self._session.close)
async def reply(self, text: str) -> None: async def reply(self, text: str) -> None:
@ -71,9 +74,7 @@ class CommandData:
Parameters: Parameters:
alias: the Alias to search for.""" alias: the Alias to search for."""
return await ru.asyncify( return await Alias.find_user(self._interface.alchemy, self.session, alias)
Alias.find_user(self._interface.alchemy, self.session, alias)
)
@contextlib.asynccontextmanager @contextlib.asynccontextmanager
async def keyboard(self, text, keys: List["KeyboardKey"]): async def keyboard(self, text, keys: List["KeyboardKey"]):

View file

@ -1,7 +1,16 @@
from .apistar import ApiStar from .apistar import ApiStar
from .jsonapi import api_response, api_success, api_error from .jsonapi import api_response, api_success, api_error
from .apidata import ApiData from .apidata import ApiData
from .apierrors import ApiError, MissingParameterError, NotFoundError, ForbiddenError from .apierrors import \
ApiError, \
NotFoundError, \
ForbiddenError, \
BadRequestError, \
ParameterError, \
MissingParameterError, \
InvalidParameterError, \
NotImplementedError, \
UnsupportedError
__all__ = [ __all__ = [
@ -13,4 +22,13 @@ __all__ = [
"ApiError", "ApiError",
"MissingParameterError", "MissingParameterError",
"NotFoundError", "NotFoundError",
"ApiError",
"NotFoundError",
"ForbiddenError",
"BadRequestError",
"ParameterError",
"MissingParameterError",
"InvalidParameterError",
"NotImplementedError",
"UnsupportedError",
] ]

View file

@ -1,7 +1,11 @@
import logging
from .apierrors import MissingParameterError from .apierrors import MissingParameterError
from royalnet.backpack.tables.tokens import Token from royalnet.backpack.tables.tokens import Token
from royalnet.backpack.tables.users import User from royalnet.backpack.tables.users import User
from .apierrors import * from .apierrors import *
import royalnet.utils as ru
log = logging.getLogger(__name__)
class ApiData(dict): class ApiData(dict):
@ -19,7 +23,7 @@ class ApiData(dict):
raise ForbiddenError("'token' is invalid") raise ForbiddenError("'token' is invalid")
return token return token
async def user(self) -> Token: async def user(self) -> User:
return (await self.token()).user return (await self.token()).user
@property @property
@ -27,5 +31,20 @@ class ApiData(dict):
if self._session is None: if self._session is None:
if self.star.alchemy is None: if self.star.alchemy is None:
raise UnsupportedError("'alchemy' is not enabled on this Royalnet instance") raise UnsupportedError("'alchemy' is not enabled on this Royalnet instance")
log.debug("Creating Session...")
self._session = self.star.alchemy.Session() self._session = self.star.alchemy.Session()
return self._session return self._session
async def session_commit(self):
"""Asyncronously commit the :attr:`.session` of this object."""
if self._session:
log.warning("Session had to be created to be committed")
# noinspection PyUnresolvedReferences
log.debug("Committing Session...")
await ru.asyncify(self.session.commit)
async def session_close(self):
"""Asyncronously close the :attr:`.session` of this object."""
if self._session is not None:
log.debug("Closing Session...")
await ru.asyncify(self._session.close)

View file

@ -10,7 +10,19 @@ class ForbiddenError(ApiError):
pass pass
class MissingParameterError(ApiError): class BadRequestError(ApiError):
pass
class ParameterError(BadRequestError):
pass
class MissingParameterError(ParameterError):
pass
class InvalidParameterError(ParameterError):
pass pass

View file

@ -7,6 +7,7 @@ from ..pagestar import PageStar
from .jsonapi import api_error, api_success from .jsonapi import api_error, api_success
from .apidata import ApiData from .apidata import ApiData
from .apierrors import * from .apierrors import *
from royalnet.utils import sentry_exc
class ApiStar(PageStar, ABC): class ApiStar(PageStar, ABC):
@ -18,19 +19,24 @@ class ApiStar(PageStar, ABC):
data = await request.json() data = await request.json()
except JSONDecodeError: except JSONDecodeError:
data = {} data = {}
apidata = ApiData(data, self)
try: try:
response = await self.api(ApiData(data, self)) response = await self.api(apidata)
except NotFoundError as e: except NotFoundError as e:
return api_error(e, code=404) return api_error(e, code=404)
except ForbiddenError as e: except ForbiddenError as e:
return api_error(e, code=403) return api_error(e, code=403)
except NotImplementedError as e: except NotImplementedError as e:
return api_error(e, code=501) return api_error(e, code=501)
except ApiError as e: except BadRequestError as e:
return api_error(e, code=400) return api_error(e, code=400)
except Exception as e: except Exception as e:
sentry_exc(e)
return api_error(e, code=500) return api_error(e, code=500)
return api_success(response) else:
return api_success(response)
finally:
await apidata.session_close()
async def api(self, data: dict) -> dict: async def api(self, data: dict) -> dict:
raise NotImplementedError() raise NotImplementedError()

View file

@ -47,7 +47,7 @@ class Serf:
} }
except ImportError as e: except ImportError as e:
log.error(f"{e.__class__.__name__} during the import of {pack_name}:\n" log.error(f"{e.__class__.__name__} during the import of {pack_name}:\n"
f"{traceback.format_exception(*sys.exc_info())}") f"{''.join(traceback.format_exception(*sys.exc_info()))}")
log.info(f"Packs: {len(packs)} imported") log.info(f"Packs: {len(packs)} imported")
self.alchemy: Optional[ra.Alchemy] = None self.alchemy: Optional[ra.Alchemy] = None