aboutsummaryrefslogtreecommitdiff
"""OAuth2 Token"""
import uuid
import datetime
from dataclasses import dataclass
from functools import cached_property
from typing import Optional

from authlib.oauth2.rfc6749 import TokenMixin
from pymonad.tools import monad_from_none_or_value
from pymonad.maybe import Just, Maybe, Nothing

from gn_auth.auth.db import sqlite3 as db
from gn_auth.auth.errors import NotFoundError
from gn_auth.auth.authentication.users import User, user_by_id

from .oauth2client import client, OAuth2Client


# pylint: disable=[too-many-instance-attributes]
@dataclass(frozen=True)
class OAuth2Token(TokenMixin):
    """Implement Tokens for OAuth2."""
    token_id: uuid.UUID
    client: OAuth2Client
    token_type: str
    access_token: str
    refresh_token: Optional[str]
    scope: str
    revoked: bool
    issued_at: datetime.datetime
    expires_in: int
    user: User

    @cached_property
    def expires_at(self):
        """Return the time when the token expires."""
        return self.issued_at + datetime.timedelta(seconds=self.expires_in)

    # pylint: disable=[redefined-outer-name]
    def check_client(self, client: OAuth2Client) -> bool:
        """Check whether the token is issued to given `client`."""
        return client.client_id == self.client.client_id

    def get_expires_in(self) -> int:
        """Return the `expires_in` value for the token."""
        return self.expires_in

    def get_scope(self) -> str:
        """Return the valid scope for the token."""
        return self.scope

    def is_expired(self) -> bool:
        """Check whether the token is expired."""
        return self.expires_at < datetime.datetime.now()

    def is_revoked(self):
        """Check whether the token has been revoked."""
        return self.revoked


def __token_from_resultset__(conn: db.DbConnection, rset) -> Maybe:
    try:
        the_user = user_by_id(conn, uuid.UUID(rset["user_id"]))
    except NotFoundError as _nfe:
        the_user = None
    return client(
        conn, uuid.UUID(rset["client_id"]), the_user
    ).then(
        lambda client: OAuth2Token(
            token_id=uuid.UUID(rset["token_id"]),
            client=client,
            token_type=rset["token_type"],
            access_token=rset["access_token"],
            refresh_token=rset["refresh_token"],
            scope=rset["scope"],
            revoked=(rset["revoked"] == 1),
            issued_at=datetime.datetime.fromtimestamp(
                rset["issued_at"]),
            expires_in=rset["expires_in"],
            user=the_user  # type: ignore
        ) if bool(the_user) else
        Nothing
    )


def token_by_access_token(conn: db.DbConnection, token_str: str) -> Maybe:
    """Retrieve token by its token string"""
    with db.cursor(conn) as cursor:
        cursor.execute("SELECT * FROM oauth2_tokens WHERE access_token=?",
                       (token_str,))
        return monad_from_none_or_value(
            Nothing, Just, cursor.fetchone()
        ).then(
            lambda res: __token_from_resultset__(
                conn, res
            )
        )


def token_by_refresh_token(conn: db.DbConnection, token_str: str) -> Maybe:
    """Retrieve token by its token string"""
    with db.cursor(conn) as cursor:
        cursor.execute(
            "SELECT * FROM oauth2_tokens WHERE refresh_token=?",
            (token_str,))
        return monad_from_none_or_value(
            Nothing, Just, cursor.fetchone()
        ).then(
            lambda res: __token_from_resultset__(conn, res)
        )


def revoke_token(token: OAuth2Token) -> OAuth2Token:
    """
    Return a new token derived from `token` with the `revoked` field set to
    `True`.
    """
    return OAuth2Token(
        token_id=token.token_id, client=token.client,
        token_type=token.token_type, access_token=token.access_token,
        refresh_token=token.refresh_token, scope=token.scope, revoked=True,
        issued_at=token.issued_at, expires_in=token.expires_in, user=token.user)


def save_token(conn: db.DbConnection, token: OAuth2Token) -> None:
    """Save/Update the token."""
    with db.cursor(conn) as cursor:
        cursor.execute(
            ("INSERT INTO oauth2_tokens VALUES (:token_id, :client_id, "
             ":token_type, :access_token, :refresh_token, :scope, :revoked, "
             ":issued_at, :expires_in, :user_id) "
             "ON CONFLICT (token_id) DO UPDATE SET "
             "refresh_token=:refresh_token, revoked=:revoked, "
             "expires_in=:expires_in "
             "WHERE token_id=:token_id"),
            {
                "token_id": str(token.token_id),
                "client_id": str(token.client.client_id),
                "token_type": token.token_type,
                "access_token": token.access_token,
                "refresh_token": token.refresh_token,
                "scope": token.scope,
                "revoked": 1 if token.revoked else 0,
                "issued_at": int(token.issued_at.timestamp()),
                "expires_in": token.expires_in,
                "user_id": str(token.user.user_id)
            })