diff options
Diffstat (limited to 'animism-align/cli/app')
| -rw-r--r-- | animism-align/cli/app/controllers/crud_controller.py | 6 | ||||
| -rw-r--r-- | animism-align/cli/app/controllers/upload_controller.py | 5 | ||||
| -rw-r--r-- | animism-align/cli/app/controllers/user_controller.py | 38 | ||||
| -rw-r--r-- | animism-align/cli/app/server/web.py | 4 | ||||
| -rw-r--r-- | animism-align/cli/app/settings/app_cfg.py | 14 | ||||
| -rw-r--r-- | animism-align/cli/app/sql/models/user.py | 2 | ||||
| -rw-r--r-- | animism-align/cli/app/sql/versions/202103051807_make_username_unique.py | 41 | ||||
| -rw-r--r-- | animism-align/cli/app/utils/auth_utils.py | 31 |
8 files changed, 140 insertions, 1 deletions
diff --git a/animism-align/cli/app/controllers/crud_controller.py b/animism-align/cli/app/controllers/crud_controller.py index 595825d..4fcb77d 100644 --- a/animism-align/cli/app/controllers/crud_controller.py +++ b/animism-align/cli/app/controllers/crud_controller.py @@ -1,6 +1,7 @@ from flask import request, jsonify from flask_classful import FlaskView, route from werkzeug.datastructures import MultiDict +from flask_jwt import jwt_required from app.sql.common import db, Session from app.server.helpers import parse_search_args, parse_sort_args @@ -28,6 +29,7 @@ class CrudView(FlaskView): def on_destroy(self, session, item): pass + @jwt_required() def index(self): """ List all {model}s @@ -53,6 +55,7 @@ class CrudView(FlaskView): session.close() return jsonify(res) + @jwt_required() def get(self, id: int): """ Fetch a single {model}. @@ -72,6 +75,7 @@ class CrudView(FlaskView): session.close() return jsonify(result) + @jwt_required() def post(self): """ Create a new {model}. @@ -99,6 +103,7 @@ class CrudView(FlaskView): session.close() return jsonify(res) + @jwt_required() def put(self, id: int): """ Update a {model}. @@ -133,6 +138,7 @@ class CrudView(FlaskView): session.close() return jsonify(res) + @jwt_required() def delete(self, id: int): """ Delete a {model}. diff --git a/animism-align/cli/app/controllers/upload_controller.py b/animism-align/cli/app/controllers/upload_controller.py index 3b6e661..f363b0d 100644 --- a/animism-align/cli/app/controllers/upload_controller.py +++ b/animism-align/cli/app/controllers/upload_controller.py @@ -1,5 +1,6 @@ from flask import request, jsonify from flask_classful import FlaskView, route +from flask_jwt import jwt_required from werkzeug.datastructures import MultiDict from werkzeug.utils import secure_filename import os @@ -13,6 +14,7 @@ from app.utils.file_utils import sha256_stream, sha256_tree, VALID_IMAGE_EXTS from app.server.decorators import APIError class UploadView(FlaskView): + @jwt_required() def index(self): """ List all uploaded files. @@ -28,6 +30,7 @@ class UploadView(FlaskView): session.close() return jsonify(response) + @jwt_required() def get(self, id): """ Fetch a single upload. @@ -41,6 +44,7 @@ class UploadView(FlaskView): session.close() return jsonify(response) + @jwt_required() def post(self): """ Upload a new file. @@ -119,6 +123,7 @@ class UploadView(FlaskView): session.close() return jsonify(response) + @jwt_required() def delete(self, id): """ Delete an uploaded file. diff --git a/animism-align/cli/app/controllers/user_controller.py b/animism-align/cli/app/controllers/user_controller.py new file mode 100644 index 0000000..8d14b98 --- /dev/null +++ b/animism-align/cli/app/controllers/user_controller.py @@ -0,0 +1,38 @@ +from flask import request, jsonify, redirect +from flask_classful import route +from werkzeug.datastructures import MultiDict + +from app.sql.common import db, Session +from app.sql.models.user import User, UserForm +from app.controllers.crud_controller import CrudView + +from flask_jwt import current_identity + +class UserView(CrudView): + model = User + form = UserForm + + def on_create(self, session, form, item): + if not current_identity.is_admin: + raise ValueError("Unauthorized") + if 'password' in form: + item.password = encrypt_password(form['password']) + if 'settings' in form: + item.settings = form['settings'] + + def on_update(self, session, form, item): + if not current_identity.is_admin: + if item.id != current_identity.id: + raise ValueError("Unauthorized") + if current_identity.is_admin != item.is_admin: + raise ValueError("Unauthorized") + if 'password' in form: + item.password = encrypt_password(form['password']) + if 'settings' in form: + item.settings = form['settings'] + + def on_destroy(self, session, form, item): + if not current_identity.is_admin: + raise ValueError("Unauthorized") + if item.id == current_identity.id: + raise ValueError("Unauthorized") diff --git a/animism-align/cli/app/server/web.py b/animism-align/cli/app/server/web.py index 890bd35..58754a2 100644 --- a/animism-align/cli/app/server/web.py +++ b/animism-align/cli/app/server/web.py @@ -14,6 +14,7 @@ logging.getLogger().addHandler(logging.StreamHandler()) from flask import Flask, Blueprint, send_from_directory, request from app.sql.common import db, connection_url +from app.utils.auth_utils import setup_jwt from app.settings import app_cfg from app.controllers.annotation_controller import AnnotationView @@ -33,9 +34,12 @@ def create_app(script_info=None): app.config['SQLALCHEMY_DATABASE_URI'] = connection_url app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False app.config['SERVER_NAME'] = app_cfg.SERVER_NAME + app.config['SECRET_KEY'] = app_cfg.TOKEN_SECRET + app.config['JWT_AUTH_URL_RULE'] = '/api/v1/auth/login' app.url_map.strict_slashes = False db.init_app(app) + setup_jwt(app) AnnotationView.register(app, route_prefix='/api/v1/') ParagraphView.register(app, route_prefix='/api/v1/') diff --git a/animism-align/cli/app/settings/app_cfg.py b/animism-align/cli/app/settings/app_cfg.py index dfd0154..ceb9e43 100644 --- a/animism-align/cli/app/settings/app_cfg.py +++ b/animism-align/cli/app/settings/app_cfg.py @@ -33,6 +33,7 @@ load_dotenv(dotenv_path=fp_env) CLICK_GROUPS = { 'peaks': 'commands/peaks', 'site': 'commands/site', + 'admin': 'commands/admin', 'db': '', 'flask': '', 'viewer': '', @@ -84,6 +85,19 @@ except Exception as e: pass # ----------------------------------------------------------------------------- +# Le crypto +# ----------------------------------------------------------------------------- + +TOKEN_SECRET = os.getenv("TOKEN_SECRET") or None +if TOKEN_SECRET is None: + import random + raise ValueError("Please set a token secret, e.g. " + hex(random.getrandbits(64 * 8)).replace('0x','')) + +TOKEN_SECRET_BYTES = bytearray() +TOKEN_SECRET_BYTES.extend(map(ord, TOKEN_SECRET)) + + +# ----------------------------------------------------------------------------- # Exports # ----------------------------------------------------------------------------- diff --git a/animism-align/cli/app/sql/models/user.py b/animism-align/cli/app/sql/models/user.py index bbc6eef..85549da 100644 --- a/animism-align/cli/app/sql/models/user.py +++ b/animism-align/cli/app/sql/models/user.py @@ -12,7 +12,7 @@ class User(Base): """Table for storing the user list""" __tablename__ = 'user' id = Column(Integer, primary_key=True) - username = Column(String(256, convert_unicode=True), nullable=False) + username = Column(String(256, convert_unicode=True), nullable=False, unique=True) password = Column(String(256, convert_unicode=True), nullable=False) is_admin = Column(Boolean, default=False) settings = Column(JSON, default={}, nullable=True) diff --git a/animism-align/cli/app/sql/versions/202103051807_make_username_unique.py b/animism-align/cli/app/sql/versions/202103051807_make_username_unique.py new file mode 100644 index 0000000..e8f4985 --- /dev/null +++ b/animism-align/cli/app/sql/versions/202103051807_make_username_unique.py @@ -0,0 +1,41 @@ +"""make username unique + +Revision ID: 135ba3ff136a +Revises: 5de5fdfbe69a +Create Date: 2021-03-05 18:07:36.955364 + +""" +from alembic import op +import sqlalchemy as sa +import sqlalchemy_utc +from sqlalchemy.dialects import mysql + +# revision identifiers, used by Alembic. +revision = '135ba3ff136a' +down_revision = '5de5fdfbe69a' +branch_labels = None +depends_on = None + + +def upgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.alter_column('episode', 'episode_number', + existing_type=mysql.INTEGER(), + nullable=True) + op.alter_column('episode', 'release_date', + existing_type=mysql.VARCHAR(collation='utf8mb4_unicode_ci', length=256), + nullable=True) + op.create_unique_constraint(None, 'user', ['username']) + # ### end Alembic commands ### + + +def downgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.drop_constraint(None, 'user', type_='unique') + op.alter_column('episode', 'release_date', + existing_type=mysql.VARCHAR(collation='utf8mb4_unicode_ci', length=256), + nullable=False) + op.alter_column('episode', 'episode_number', + existing_type=mysql.INTEGER(), + nullable=False) + # ### end Alembic commands ### diff --git a/animism-align/cli/app/utils/auth_utils.py b/animism-align/cli/app/utils/auth_utils.py new file mode 100644 index 0000000..71974e3 --- /dev/null +++ b/animism-align/cli/app/utils/auth_utils.py @@ -0,0 +1,31 @@ +from flask_jwt import JWT + +import hmac +import hashlib +from app.settings import app_cfg + +from app.sql.common import db, Session, User + +def encrypt_password(cleartext): + clearbytes = bytearray() + clearbytes.extend(map(ord, cleartext)) + return hmac.new(app_cfg.TOKEN_SECRET_BYTES, clearbytes, hashlib.sha256).hexdigest() + +def authenticate(username, password): + session = Session() + password = encrypt_password(password) + user = session.query(User).filter(User.username == username).first() + session.close() + if user and hmac.compare_digest(user.password.encode('utf-8'), password.encode('utf-8')): + return user + return None + +def identity(payload): + session = Session() + user_id = payload['identity'] + user = session.query(User).get(user_id) + session.close() + return user + +def setup_jwt(app): + return JWT(app, authenticate, identity) |
