diff options
author | 2018-03-29 04:52:29 -0400 | |
---|---|---|
committer | 2018-03-29 09:52:29 +0100 | |
commit | 1889f3248ae83cc773a5443c6ba0f62834d73b7b (patch) | |
tree | 7603d1da7f64006075cf3a8b58548e1a49ca6840 /pysite | |
parent | Reorder resources.json (diff) |
Oauth (#45)
* Creating a OAUTH login for the site. We still need to have a way to fill in credentials however.
Signed-off-by: Zwork101 <[email protected]>
* How ya like me now Travis?
Signed-off-by: Zwork101 <[email protected]>
* Fix slight error in database insertion
* Revert "Fix slight error in database insertion" (wrong branch)
This reverts commit 9ac6cbb
* Don't have snekchek working, but I'll try again.
Signed-off-by: Zwork101 <[email protected]>
* Please enter the commit message for your changes. Lines starting
* Adding Oauth2 login to site.
* Add prefix
* Add prefix
* This never happened
* Flipping Travis
I still can't get snekchek to work locally.
* Added a whole bunch of stuff, ready to be used.
Signed-off-by: Zwork101 <[email protected]>
* Making sessions more secure, and future safe.
Signed-off-by: Zwork101 <[email protected]>
* Adding some quick stuff
Signed-off-by: Zwork101 <[email protected]>
* Appease the flake8 gods
Signed-off-by: Zwork101 <[email protected]>
* Appease the flake8 gods
Signed-off-by: Zwork101 <[email protected]>
* Whoops
Signed-off-by: Zwork101 <[email protected]>
* Add comments, fix user_data function.
Signed-off-by: Zwork101 <[email protected]>
* Whooops, forgot to flake :/
Signed-off-by: Zwork101 <[email protected]>
* Make it look nicer, thanks Aperture.
Signed-off-by: Zwork101 <[email protected]>
* Fixing login issues and added button
* Add a OauthMixin to allow for easy data access. Clean stuff up.
Signed-off-by: Zwork101 <[email protected]>
* Fix a test, and use self.assertEqual rather then self.assertEquals!
Signed-off-by: Zwork101 <[email protected]>
* Please don't ask how that happened.
Signed-off-by: Zwork101 <[email protected]>
* Added some tests, moved a bunch of stuff around. Mainly cleaned
stuff up.
Signed-off-by: Zwork101 <[email protected]>
* Add a ton of tests, try to please the coverall gods :D, moved some
code into a function for testing.
Signed-off-by: Zwork101 <[email protected]>
* Just some stupid stuff I missed.
Signed-off-by: Zwork101 <[email protected]>
* Fix an issue with the test, and add docs
Signed-off-by: Zwork101 <[email protected]>
* Remove pointless function. (join_user)
Signed-off-by: Zwork101 <[email protected]>
* Fix test consistency
Diffstat (limited to 'pysite')
-rw-r--r-- | pysite/base_route.py | 4 | ||||
-rw-r--r-- | pysite/constants.py | 21 | ||||
-rw-r--r-- | pysite/database.py | 8 | ||||
-rw-r--r-- | pysite/mixins.py | 48 | ||||
-rw-r--r-- | pysite/oauth.py | 77 | ||||
-rw-r--r-- | pysite/route_manager.py | 20 | ||||
-rw-r--r-- | pysite/views/main/index.py | 3 | ||||
-rw-r--r-- | pysite/views/main/logout.py | 15 | ||||
-rw-r--r-- | pysite/views/tests/index.py | 1 |
9 files changed, 186 insertions, 11 deletions
diff --git a/pysite/base_route.py b/pysite/base_route.py index 4e1a63a7..71a4c894 100644 --- a/pysite/base_route.py +++ b/pysite/base_route.py @@ -6,9 +6,10 @@ from flask import Blueprint, Response, jsonify, render_template from flask.views import MethodView from pysite.constants import ErrorCodes +from pysite.mixins import OauthMixin -class BaseView(MethodView): +class BaseView(MethodView, OauthMixin): """ Base view class with functions and attributes that should be common to all view classes. @@ -27,6 +28,7 @@ class BaseView(MethodView): """ context["current_page"] = self.name context["view"] = self + context["logged_in"] = self.logged_in return render_template(template_names, **context) diff --git a/pysite/constants.py b/pysite/constants.py index c84ca245..7df4674e 100644 --- a/pysite/constants.py +++ b/pysite/constants.py @@ -1,7 +1,7 @@ # coding=utf-8 from enum import Enum, IntEnum -import os +from os import environ class ErrorCodes(IntEnum): @@ -22,6 +22,17 @@ ADMIN_ROLE = 267628507062992896 MODERATOR_ROLE = 267629731250176001 HELPER_ROLE = 267630620367257601 +SERVER_ID = 267624335836053506 + +DISCORD_API_ENDPOINT = "https://discordapp.com/api" + +DISCORD_OAUTH_REDIRECT = "/auth/discord" +DISCORD_OAUTH_AUTHORIZED = "/auth/discord/authorized" +DISCORD_OAUTH_ID = environ.get('DISCORD_OAUTH_ID', '') +DISCORD_OAUTH_SECRET = environ.get('DISCORD_OAUTH_SECRET', '') +DISCORD_OAUTH_SCOPE = 'identify email guilds.join' +OAUTH_DATABASE = "oauth_data" + ERROR_DESCRIPTIONS = { # 5XX 500: "The server encountered an unexpected error ._.", @@ -46,9 +57,9 @@ ERROR_DESCRIPTIONS = { } # PaperTrail logging -PAPERTRAIL_ADDRESS = os.environ.get("PAPERTRAIL_ADDRESS") or None -PAPERTRAIL_PORT = int(os.environ.get("PAPERTRAIL_PORT") or 0) +PAPERTRAIL_ADDRESS = environ.get("PAPERTRAIL_ADDRESS") or None +PAPERTRAIL_PORT = int(environ.get("PAPERTRAIL_PORT") or 0) # DataDog logging -DATADOG_ADDRESS = os.environ.get("DATADOG_ADDRESS") or None -DATADOG_PORT = int(os.environ.get("DATADOG_PORT") or 0) +DATADOG_ADDRESS = environ.get("DATADOG_ADDRESS") or None +DATADOG_PORT = int(environ.get("DATADOG_PORT") or 0) diff --git a/pysite/database.py b/pysite/database.py index add76923..4c2153fe 100644 --- a/pysite/database.py +++ b/pysite/database.py @@ -103,9 +103,11 @@ class RethinkDB: self.log.debug(f"Table created: '{table_name}'") return True - def delete(self, table_name: str, primary_key: Optional[str] = None, - durability: str = "hard", return_changes: Union[bool, str] = False - ) -> Union[Dict[str, Any], None]: + def delete(self, + table_name: str, + primary_key: Union[str, None] = None, + durability: str="hard", + return_changes: Union[bool, str] = False) -> dict: """ Delete one or all documents from a table. This can only delete either the contents of an entire table, or a single document. diff --git a/pysite/mixins.py b/pysite/mixins.py index 059f871d..5b1a780f 100644 --- a/pysite/mixins.py +++ b/pysite/mixins.py @@ -6,7 +6,7 @@ from _weakref import ref from pysite.database import RethinkDB -class DBMixin(): +class DBMixin: """ Mixin for classes that make use of RethinkDB. It can automatically create a table with the specified primary key using the attributes set at class-level. @@ -59,3 +59,49 @@ class DBMixin(): @property def db(self) -> RethinkDB: return self._db() + + +class OauthMixin: + """ + Mixin for the classes that need access to a logged in user's information. This class should be used + to grant route's access to user information, such as name, email, id, ect. + + There will almost never be a need for someone to inherit this, as BaseView does that for you. + + This class will add 3 properties to your route: + + * logged_in (bool): True if user is registered with the site, False else wise. + + * user_data (dict): A dict that looks like this: + + { + "user_id": Their discord ID, + "username": Their discord username (without discriminator), + "discriminator": Their discord discriminator, + "email": Their email, in which is connected to discord + } + + user_data returns None, if the user isn't logged in. + + * oauth (OauthBackend): The instance of pysite.oauth.OauthBackend, connected to the RouteManager. + """ + + @classmethod + def setup(cls: "OauthMixin", manager: "pysite.route_manager.RouteManager", blueprint: Blueprint): + + if hasattr(super(), "setup"): + super().setup(manager, blueprint) # pragma: no cover + + cls._oauth = ref(manager.oauth_backend) + + @property + def logged_in(self) -> bool: + return self.user_data is not None + + @property + def user_data(self) -> dict: + return self.oauth.user_data() + + @property + def oauth(self): + return self._oauth() diff --git a/pysite/oauth.py b/pysite/oauth.py new file mode 100644 index 00000000..8370b713 --- /dev/null +++ b/pysite/oauth.py @@ -0,0 +1,77 @@ +import logging +from uuid import uuid4, uuid5 + +from flask import session +from flask_dance.consumer.backend import BaseBackend +from flask_dance.contrib.discord import discord + + +from pysite.constants import DISCORD_API_ENDPOINT, OAUTH_DATABASE + + +class OauthBackend(BaseBackend): + """ + This is the backend for the oauth + + This is used to manage users that have completed + an oauth dance. It contains 3 functions, get, set, + and delete, however we only use set. + + Inherits: + flake_dance.consumer.backend.BaseBackend + pysite.mixins.DBmixin + + Properties: + key: The app's secret, we use it too make session IDs + """ + + def __init__(self, manager): + super().__init__() + self.db = manager.db + self.key = manager.app.secret_key + self.db.create_table(OAUTH_DATABASE, primary_key="id") + + def get(self, *args, **kwargs): # Not used + pass + + def set(self, blueprint, token): + + user = self.get_user() + self.join_discord(token["access_token"], user["id"]) + sess_id = str(uuid5(uuid4(), self.key)) + self.add_user(token, user, sess_id) + + def delete(self, blueprint): # Not used + pass + + def add_user(self, token_data: dict, user_data: dict, session_id: str): + session["session_id"] = session_id + + self.db.insert(OAUTH_DATABASE, {"id": session_id, + "access_token": token_data["access_token"], + "refresh_token": token_data["refresh_token"], + "expires_at": token_data["expires_at"], + "snowflake": user_data["id"]}) + + self.db.insert("users", {"user_id": user_data["id"], + "username": user_data["username"], + "discriminator": user_data["discriminator"], + "email": user_data["email"]}) + + def get_user(self) -> dict: + resp = discord.get(DISCORD_API_ENDPOINT + "/users/@me") # 'discord' is a request.Session with oauth information + if resp.status_code != 200: + logging.warning("Unable to get user information: " + str(resp.json())) + return resp.json() + + def user_data(self): + user_id = session.get("session_id") + if user_id: # If the user is logged in, get user info. + creds = self.db.get(OAUTH_DATABASE, user_id) + if creds: + return self.db.get("users", creds["snowflake"]) + + def logout(self): + sess_id = session.get("session_id") + if sess_id and self.db.get(OAUTH_DATABASE, sess_id): # If user exists in db, + self.db.delete(OAUTH_DATABASE, sess_id) # remove them (at least, their session) diff --git a/pysite/route_manager.py b/pysite/route_manager.py index 72517a3c..9ecd3ced 100644 --- a/pysite/route_manager.py +++ b/pysite/route_manager.py @@ -5,10 +5,15 @@ import logging import os from flask import Blueprint, Flask +from flask_dance.contrib.discord import make_discord_blueprint from flask_sockets import Sockets from pysite.base_route import APIView, BaseView, ErrorView, RouteView +from pysite.constants import ( + DISCORD_OAUTH_ID, DISCORD_OAUTH_SCOPE, DISCORD_OAUTH_SECRET, DISCORD_OAUTH_REDIRECT, DISCORD_OAUTH_AUTHORIZED +) from pysite.database import RethinkDB +from pysite.oauth import OauthBackend from pysite.websockets import WS TEMPLATES_PATH = "../templates" @@ -31,6 +36,21 @@ class RouteManager: self.app.before_request(self.db.before_request) self.app.teardown_request(self.db.teardown_request) + # Load the oauth blueprint + self.oauth_backend = OauthBackend(self) + self.oauth_blueprint = make_discord_blueprint( + DISCORD_OAUTH_ID, + DISCORD_OAUTH_SECRET, + DISCORD_OAUTH_SCOPE, + '/', + login_url=DISCORD_OAUTH_REDIRECT, + authorized_url=DISCORD_OAUTH_AUTHORIZED, + backend=self.oauth_backend + ) + self.log.debug(f"Loading Blueprint: {self.oauth_blueprint.name}") + self.app.register_blueprint(self.oauth_blueprint) + self.log.debug("") + # Load the main blueprint self.main_blueprint = Blueprint("main", __name__) self.log.debug(f"Loading Blueprint: {self.main_blueprint.name}") diff --git a/pysite/views/main/index.py b/pysite/views/main/index.py index 210eb057..8d0cb349 100644 --- a/pysite/views/main/index.py +++ b/pysite/views/main/index.py @@ -1,5 +1,6 @@ # coding=utf-8 from pysite.base_route import RouteView +from pysite.constants import DISCORD_OAUTH_REDIRECT class IndexView(RouteView): @@ -7,4 +8,4 @@ class IndexView(RouteView): name = "index" def get(self): - return self.render("main/index.html") + return self.render("main/index.html", login_url=DISCORD_OAUTH_REDIRECT) diff --git a/pysite/views/main/logout.py b/pysite/views/main/logout.py new file mode 100644 index 00000000..fce30972 --- /dev/null +++ b/pysite/views/main/logout.py @@ -0,0 +1,15 @@ +from flask import redirect, session + +from pysite.base_route import RouteView + + +class LogoutView(RouteView): + name = "logout" + path = "/auth/logout" + + def get(self): + if self.logged_in: + # remove user's session + del session["session_id"] + self.oauth.logout() + return redirect("/") diff --git a/pysite/views/tests/index.py b/pysite/views/tests/index.py index 3071bf0e..2a55a112 100644 --- a/pysite/views/tests/index.py +++ b/pysite/views/tests/index.py @@ -7,6 +7,7 @@ from pysite.base_route import RouteView from pysite.constants import ValidationTypes from pysite.decorators import api_params + SCHEMA = Schema([{"test": str}]) REQUIRED_KEYS = ["test"] |