From 0e869bd55acc8005050336fe56b665c8a9b4741e Mon Sep 17 00:00:00 2001 From: Daniel Tsvetkov Date: Sun, 9 May 2021 00:05:26 +0200 Subject: [PATCH] csrf, openid_connect --- bootstrap/config.py | 2 + oshipka/persistance/__init__.py | 64 ++++++++++++++++--- oshipka/webapp/default_routes.py | 27 +++++++- oshipka/webapp/templates/delete_instance.html | 1 + requirements.txt | 3 + vm_gen/templates/html/_create.html | 1 + vm_gen/templates/html/_update.html | 1 + vm_gen/templates/html/navigation.html | 1 + vm_gen/templates/model_acl_py | 21 ++++++ 9 files changed, 111 insertions(+), 10 deletions(-) create mode 100644 vm_gen/templates/model_acl_py diff --git a/bootstrap/config.py b/bootstrap/config.py index 22140f2..5d5b279 100644 --- a/bootstrap/config.py +++ b/bootstrap/config.py @@ -23,3 +23,5 @@ STATIC_FOLDER = os.path.join(basepath, "webapp", "static") MAKEDIRS = [ DATA_DIR, STATIC_DATA_DIR, MEDIA_DIR, TASKS_DIR, TASKS_IN_DIR, TASKS_PROC_DIR, TASKS_BUF_DIR, ] + +APP_BASE_URL = "http://localhost:5000" diff --git a/oshipka/persistance/__init__.py b/oshipka/persistance/__init__.py index 09aac49..226abd4 100644 --- a/oshipka/persistance/__init__.py +++ b/oshipka/persistance/__init__.py @@ -1,3 +1,4 @@ +import base64 import csv import datetime import json @@ -8,31 +9,35 @@ from importlib import import_module from json import JSONEncoder from uuid import uuid4 +import onetimepass from flask import request -from flask_security import current_user - -from config import SQLALCHEMY_DATABASE_URI, MAKEDIRS, DATABASE_FILE, SEARCH_INDEX_PATH, STATIC_DATA_DIR, MEDIA_DIR, basepath from flask_migrate import Migrate -from flask_migrate import upgrade as migrate_upgrade from flask_migrate import init as migrate_init +from flask_migrate import upgrade as migrate_upgrade from flask_security import RoleMixin, UserMixin from flask_security import Security, SQLAlchemyUserDatastore -from flask_security.utils import encrypt_password, hash_password +from flask_security import current_user +from flask_security.utils import hash_password from flask_sqlalchemy import SQLAlchemy +from flask_wtf import CSRFProtect from markupsafe import escape, Markup from sqlalchemy import Boolean from sqlalchemy import TypeDecorator from sqlalchemy.ext.declarative import declared_attr, DeclarativeMeta from sqlalchemy.orm.collections import InstrumentedList from sqlalchemy_utils import Choice -from tww.lib import solve_query, resolve_timezone, dt_tz_translation, time_ago, get_utcnow +from tww.lib import solve_query, resolve_timezone, dt_tz_translation, time_ago +from werkzeug.security import generate_password_hash, check_password_hash from whooshalchemy import IndexService + +from config import SQLALCHEMY_DATABASE_URI, MAKEDIRS, DATABASE_FILE, SEARCH_INDEX_PATH, STATIC_DATA_DIR, MEDIA_DIR, \ + basepath from oshipka.util.strings import camel_case_to_snake_case from vm_gen.vm_gen import order_from_process_order db = SQLAlchemy() migrate = Migrate() - +csrf = CSRFProtect() SHARING_TYPE_TYPES_TYPE_PUBLIC = "PUBLIC" SHARING_TYPE_TYPES_TYPE_AUTHZ = "AUTHZ" @@ -64,7 +69,6 @@ roles_users = db.Table('roles_users', db.Column('user_id', db.Integer(), db.ForeignKey('user.id')), db.Column('role_id', db.Integer(), db.ForeignKey('role.id'))) - class ModelJsonEncoder(JSONEncoder): def default(self, o): if isinstance(o, datetime.datetime): @@ -190,6 +194,49 @@ class User(db.Model, ModelController, UserMixin): backref=db.backref('users', lazy='dynamic')) +class U2FCredential(db.Model, ModelController): + name = db.Column(db.Unicode) + date_added = db.Column(db.DateTime) + device = db.Column(db.Unicode) + + user_sso_id = db.Column(db.Integer, db.ForeignKey('user_sso.id')) + user_sso = db.relationship('UserSSO', + backref=db.backref("u2f_credentials"), + ) + + +class UserSSO(db.Model, ModelController): + username = db.Column(db.Unicode, unique=True) + email = db.Column(db.Unicode, unique=True) + password_hash = db.Column(db.Unicode) + + otp_secret = db.Column(db.String(16)) + + def __init__(self, **kwargs): + super(UserSSO, self).__init__(**kwargs) + if self.otp_secret is None: + # generate a random secret + self.otp_secret = base64.b32encode(os.urandom(10)).decode('utf-8') + + @property + def password(self): + raise AttributeError('password is not a readable attribute') + + @password.setter + def password(self, password): + self.password_hash = generate_password_hash(password) + + def verify_password(self, password): + return check_password_hash(self.password_hash, password) + + def get_totp_uri(self, client_name): + return 'otpauth://totp/{client_name}:{username}?secret={secret}&issuer={client_name}' \ + .format(username=self.username, secret=self.otp_secret, client_name=client_name) + + def verify_totp(self, token): + return onetimepass.valid_totp(token, self.otp_secret) + + security = Security() user_datastore = SQLAlchemyUserDatastore(db, User, Role) @@ -335,6 +382,7 @@ def init_db(app): app.register_blueprint(oshipka_bp) db.init_app(app) + csrf.init_app(app) migrate.init_app(app, db) security.init_app(app, user_datastore) _init_translations(app) diff --git a/oshipka/webapp/default_routes.py b/oshipka/webapp/default_routes.py index 80fcb8a..61174e3 100644 --- a/oshipka/webapp/default_routes.py +++ b/oshipka/webapp/default_routes.py @@ -1,9 +1,32 @@ -from flask import send_from_directory +import urllib + +import requests +from flask import send_from_directory, redirect, request, url_for from oshipka.webapp import oshipka_bp -from config import MEDIA_DIR +from config import MEDIA_DIR, APP_BASE_URL +# TODO: VULNZ - EVERYONE HAS ACCESS TO THIS @oshipka_bp.route('/media/') def get_media(filepath): return send_from_directory(MEDIA_DIR, filepath) + + +SSO_BASE_URL = 'http://localhost:5008' + + +@oshipka_bp.route('/sso') +def sso(): + callback_url = APP_BASE_URL + url_for('oshipka_bp.open_id_connect_code') + return redirect(SSO_BASE_URL + '/authenticate?callback={}'.format(urllib.parse.quote(callback_url))) + + +@oshipka_bp.route('/open_id_connect_code') +def open_id_connect_code(): + code = request.args.get('code') + response = requests.get( + SSO_BASE_URL + "/token", + data={'code': code}, + ) + return 'got response for token: {}'.format(response.status_code) diff --git a/oshipka/webapp/templates/delete_instance.html b/oshipka/webapp/templates/delete_instance.html index c47da47..447a011 100644 --- a/oshipka/webapp/templates/delete_instance.html +++ b/oshipka/webapp/templates/delete_instance.html @@ -3,6 +3,7 @@ {% block content %}

Delete {{ model_view.model_name }}:{{ instance.id }} ?

+
diff --git a/requirements.txt b/requirements.txt index 8d0e860..847efae 100644 --- a/requirements.txt +++ b/requirements.txt @@ -24,9 +24,12 @@ inflect==4.1.0 itsdangerous==1.1.0 Jinja2==2.11.1 MarkupSafe==1.1.1 +onetimepass==1.0.1 passlib==1.7.2 pathtools==0.1.2 pycparser==2.20 +pyqrcode==1.2.1 +python-u2flib-server==5.0.1 pytz==2019.3 pyyaml==5.3.1 six==1.14.0 diff --git a/vm_gen/templates/html/_create.html b/vm_gen/templates/html/_create.html index 817b8e9..31de5de 100644 --- a/vm_gen/templates/html/_create.html +++ b/vm_gen/templates/html/_create.html @@ -1,4 +1,5 @@
+ [%- for column in columns %] diff --git a/vm_gen/templates/html/_update.html b/vm_gen/templates/html/_update.html index 06c55b7..65b46a1 100644 --- a/vm_gen/templates/html/_update.html +++ b/vm_gen/templates/html/_update.html @@ -1,4 +1,5 @@ +
[%- for column in columns %] diff --git a/vm_gen/templates/html/navigation.html b/vm_gen/templates/html/navigation.html index a5f8938..c0912b5 100644 --- a/vm_gen/templates/html/navigation.html +++ b/vm_gen/templates/html/navigation.html @@ -15,5 +15,6 @@ {{ _("Logout") }} | {% else %} {{ _("Login") }} | + {{ _("SSO") }} {% endif %} \ No newline at end of file diff --git a/vm_gen/templates/model_acl_py b/vm_gen/templates/model_acl_py new file mode 100644 index 0000000..0bb457b --- /dev/null +++ b/vm_gen/templates/model_acl_py @@ -0,0 +1,21 @@ +from sqlalchemy_utils import ChoiceType +from oshipka.persistance import db, ModelController +from oshipka.persistance import SHARING_TYPE_TYPES, SHARING_TYPE_TYPES_TYPE_PUBLIC + +class [[ name ]]Acl(db.Model, ModelController): + user_id = db.Column(db.Integer, db.ForeignKey('user.id')) + user = db.relationship('User', backref=db.backref("[[ name|camel_to_snake ]]_acls")) + + instance_id = db.Column(db.Integer, db.ForeignKey('[[ name|camel_to_snake ]].id')) + instance = db.relationship('[[ name ]]', backref=db.backref("[[ name|camel_to_snake|pluralize ]]")) + + acl_type = db.Column(ChoiceType(SHARING_TYPE_TYPES), default=SHARING_TYPE_TYPES_TYPE_PUBLIC) + + _read = db.Column(db.Boolean, default=True) + _update = db.Column(db.Boolean, default=True) + _delete = db.Column(db.Boolean, default=True) + + [% for column in columns %] + [[ column.name ]]__read = db.Column(db.Boolean, default=True) + [[ column.name ]]__write = db.Column(db.Boolean, default=True) + [%- endfor %]