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 royalnet.utils import asyncify
from royalnet.alchemy.errors import TableNotFoundError
@ -6,7 +6,7 @@ from sqlalchemy import create_engine
from sqlalchemy.engine import Engine
from sqlalchemy.schema import Table
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
@ -26,7 +26,7 @@ class Alchemy:
raise NotImplementedError("sqlite databases aren't supported, as they can't be used in multithreaded"
" applications")
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._tables: Dict[str, Table] = {}
for table in tables:
@ -38,7 +38,7 @@ class Alchemy:
self._tables[name] = bound_table
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.
Args:

View file

@ -2,13 +2,16 @@
from .api_royalnet_version import ApiRoyalnetVersionStar
from .api_login_royalnet import ApiLoginRoyalnetStar
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!
available_page_stars = [
ApiRoyalnetVersionStar,
ApiLoginRoyalnetStar,
ApiTokenInfoStar,
ApiTokenPasswdStar,
ApiTokenCreateStar,
]
# 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.ext.declarative import declared_attr
import royalnet.utils as ru
from .users import User
class Token:
@ -30,10 +29,18 @@ class Token:
def expired(self):
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
def generate(cls, user, expiration_delta: datetime.timedelta):
def generate(cls, alchemy, user, expiration_delta: datetime.timedelta):
# 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
def json(self) -> dict:

View file

@ -26,6 +26,7 @@ class CommandData:
if self._session is None:
if self._interface.alchemy is None:
raise UnsupportedError("'alchemy' is not enabled on this Royalnet instance")
log.debug("Creating Session...")
self._session = self._interface.alchemy.Session()
return self._session
@ -34,11 +35,13 @@ class CommandData:
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)
async def reply(self, text: str) -> None:
@ -71,9 +74,7 @@ class CommandData:
Parameters:
alias: the Alias to search for."""
return await ru.asyncify(
Alias.find_user(self._interface.alchemy, self.session, alias)
)
return await Alias.find_user(self._interface.alchemy, self.session, alias)
@contextlib.asynccontextmanager
async def keyboard(self, text, keys: List["KeyboardKey"]):

View file

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

View file

@ -1,7 +1,11 @@
import logging
from .apierrors import MissingParameterError
from royalnet.backpack.tables.tokens import Token
from royalnet.backpack.tables.users import User
from .apierrors import *
import royalnet.utils as ru
log = logging.getLogger(__name__)
class ApiData(dict):
@ -19,7 +23,7 @@ class ApiData(dict):
raise ForbiddenError("'token' is invalid")
return token
async def user(self) -> Token:
async def user(self) -> User:
return (await self.token()).user
@property
@ -27,5 +31,20 @@ class ApiData(dict):
if self._session is None:
if self.star.alchemy is None:
raise UnsupportedError("'alchemy' is not enabled on this Royalnet instance")
log.debug("Creating Session...")
self._session = self.star.alchemy.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
class MissingParameterError(ApiError):
class BadRequestError(ApiError):
pass
class ParameterError(BadRequestError):
pass
class MissingParameterError(ParameterError):
pass
class InvalidParameterError(ParameterError):
pass

View file

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

View file

@ -47,7 +47,7 @@ class Serf:
}
except ImportError as e:
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")
self.alchemy: Optional[ra.Alchemy] = None